MySQL 服务器调优

2007 年 7 月 30 日

如今,开发人员不断地开发和部署使用 LAMP(Linux®、Apache、MySQL 和 PHP/Perl)架构的应用程序。但是,服务器管理员常常对应用程序本身没有什么控制能力,因为应用程序是别人编写的。这份 共三部分的系列文章 将讨论许多服务器配置问题,这些配置会影响应用程序的性能。本文是本系列文章的第三部分,也是最后一部分,将重点讨论为实现最高效率而对数据库层进行的调优。

关于 MySQL 调优

有 3 种方法可以加快 MySQL 服务器的运行速度,效率从低到高依次为:

  1. 替换有问题的硬件。
  2. 对 MySQL 进程的设置进行调优。
  3. 对查询进行优化。

迁移到 DB2
您正在寻找一种干净利落、无成本的方法用来从 MySQL 迁移到 IBM® DB2® 吗?“从 MySQL 或 PostgreSQL 迁移到 DB2 Express-C” 介绍了如何使用文中提供的迁移工具来简单地实现这种转换。可以 下载 免费的 DB2 Express-C 并开始体验。

替换有问题的硬件通常是我们的第一考虑,主要原因是数据库会占用大量资源。不过这种解决方案也就仅限于此了。实际上,您通常可以让中央处理器(CPU)或磁盘速度加倍,也可以让内存增大 4 到 8 倍。

第二种方法是对 MySQL 服务器(也称为 mysqld)进行调优。对这个进程进行调优意味着适当地分配内存,并让 mysqld 了解将会承受何种类型的负载。加快磁盘运行速度不如减少所需的磁盘访问次数。类似地,确保 MySQL 进程正确操作就意味着它花费在服务查询上的时间要多于花费在处理后台任务(如处理临时磁盘表或打开和关闭文件)上的时间。对 mysqld 进行调优是本文的重点。

最好的方法是确保查询已经进行了优化。这意味着对表应用了适当的索引,查询是按照可以充分利用 MySQL 功能的方式来编写的。尽管本文并没有包含查询调优方面的内容(很多著作中已经针对这个主题进行了探讨),不过它会配置 mysqld 来报告可能需要进行调优的查询。

虽然已经为这些任务指派了次序,但是仍然要注意硬件和 mysqld 的设置以利于适当地调优查询。机器速度慢也就罢了,我曾经见过速度很快的机器在运行设计良好的查询时由于负载过重而失败,因为 mysqld 被大量繁忙的工作所占用而不能服务查询。



回页首

记录慢速查询

在一个 SQL 服务器中,数据表都是保存在磁盘上的。索引为服务器提供了一种在表中查找特定数据行的方法,而不用搜索整个表。当必须要搜索整个表时,就称为表扫描。通常来说,您可能只希望获得表中数据的一个子集,因此全表扫描会浪费大量的磁盘 I/O,因此也就会浪费大量时间。当必须对数据进行连接时,这个问题就更加复杂了,因为必须要对连接两端的多行数据进行比较。

当 然,表扫描并不总是会带来问题;有时读取整个表反而会比从中挑选出一部分数据更加有效(服务器进程中查询规划器用来作出这些决定)。如果索引的使用效率很 低,或者根本就不能使用索引,则会减慢查询速度,而且随着服务器上的负载和表大小的增加,这个问题会变得更加显著。执行时间超过给定时间范围的查询就称为慢速查询

您可以配置 mysqld 将这些慢速查询记录到适当命名的慢速查询日志中。管理员然后会查看这个日志来帮助他们确定应用程序中有哪些部分需要进一步调查。清单 1 给出了要启用慢速查询日志需要在 my.cnf 中所做的配置。

清单 1. 启用 MySQL 慢速查询日志

[mysqld]
; enable the slow query log, default 10 seconds
log-slow-queries
; log queries taking longer than 5 seconds
long_query_time = 5
; log queries that don’t use indexes even if they take less than long_query_time
; MySQL 4.1 and newer only
log-queries-not-using-indexes

这三个设置一起使用,可以记录执行时间超过 5 秒和没有使用索引的查询。请注意有关 log-queries-not-using-indexes 的警告:您必须使用 MySQL 4.1 或更高版本。慢速查询日志都保存在 MySQL 数据目录中,名为 hostname-slow.log。如果希望使用一个不同的名字或路径,可以在 my.cnf 中使用 log-slow-queries = /new/path/to/file 实现此目的。

阅读慢速查询日志最好是通过 mysqldumpslow 命令进行。指定日志文件的路径,就可以看到一个慢速查询的排序后的列表,并且还显示了它们在日志文件中出现的次数。一个非常有用的特性是 mysqldumpslow 在比较结果之前,会删除任何用户指定的数据,因此对同一个查询的不同调用被计为一次;这可以帮助找出需要工作量最多的查询。



回页首

对查询进行缓存

很多 LAMP 应用程序都严重依赖于数据库,但却会反复执行相同的查询。每次执行查询时,数据库都必须要执行相同的工作 —— 对查询进行分析,确定如何执行查询,从磁盘中加载信息,然后将结果返回给客户机。MySQL 有一个特性称为查询缓存,它将(后面会用到的)查询结果保存在内存中。在很多情况下,这会极大地提高性能。不过,问题是查询缓存在默认情况下是禁用的。

query_cache_size = 32M 添加到 /etc/my.conf 中可以启用 32MB 的查询缓存。

监视查询缓存

在启用查询缓存之后,重要的是要理解它是否得到了有效的使用。MySQL 有几个可以查看的变量,可以用来了解缓存中的情况。清单 2 给出了缓存的状态。

清单 2. 显示查询缓存的统计信息

mysql> SHOW STATUS LIKE ‘qcache%’;
+————————-+————+
| Variable_name | Value |
+————————-+————+
| Qcache_free_blocks | 5216 |
| Qcache_free_memory | 14640664 |
| Qcache_hits | 2581646882 |
| Qcache_inserts | 360210964 |
| Qcache_lowmem_prunes | 281680433 |
| Qcache_not_cached | 79740667 |
| Qcache_queries_in_cache | 16927 |
| Qcache_total_blocks | 47042 |
+————————-+————+
8 rows in set (0.00 sec)

这些项的解释如表 1 所示。

表 1. MySQL 查询缓存变量
变量名 说明 Qcache_free_blocks 缓存中相邻内存块的个数。数目大说明可能有碎片。FLUSH QUERY CACHE 会对缓存中的碎片进行整理,从而得到一个空闲块。 Qcache_free_memory 缓存中的空闲内存。 Qcache_hits 每次查询在缓存中命中时就增大。 Qcache_inserts 每次插入一个查询时就增大。命中次数除以插入次数就是不中比率;用 1 减去这个值就是命中率。在上面这个例子中,大约有 87% 的查询都在缓存中命中。 Qcache_lowmem_prunes 缓存出现内存不足并且必须要进行清理以便为更多查询提供空间的次数。这个数字最好长时间来看;如果这个数字在不断增长,就表示可能碎片非常严重,或者内存很少。(上面的 free_blocksfree_memory 可以告诉您属于哪种情况)。 Qcache_not_cached 不适合进行缓存的查询的数量,通常是由于这些查询不是 SELECT 语句。 Qcache_queries_in_cache 当前缓存的查询(和响应)的数量。 Qcache_total_blocks 缓存中块的数量。

通常,间隔几秒显示这些变量就可以看出区别,这可以帮助确定缓存是否正在有效地使用。运行 FLUSH STATUS 可以重置一些计数器,如果服务器已经运行了一段时间,这会非常有帮助。

使用非常大的查询缓存,期望可以缓存所有东西,这种想法非常诱人。由于 mysqld 必须要对缓存进行维护,例如当内存变得很低时执行剪除,因此服务器可能会在试图管理缓存时而陷入困境。作为一条规则,如果 FLUSH QUERY CACHE 占用了很长时间,那就说明缓存太大了。



回页首

强制限制

您可以在 mysqld 中强制一些限制来确保系统负载不会导致资源耗尽的情况出现。清单 3 给出了 my.cnf 中与资源有关的一些重要设置。

清单 3. MySQL 资源设置

set-variable=max_connections=500
set-variable=wait_timeout=10
max_connect_errors = 100

连接最大个数是在第一行中进行管理的。与 Apache 中的 MaxClients 类似,其想法是确保只建立服务允许数目的连接。要确定服务器上目前建立过的最大连接数,请执行 SHOW STATUS LIKE 'max_used_connections'

第 2 行告诉 mysqld 终止所有空闲时间超过 10 秒的连接。在 LAMP 应用程序中,连接数据库的时间通常就是 Web 服务器处理请求所花费的时间。有时候,如果负载过重,连接会挂起,并且会占用连接表空间。如果有多个交互用户或使用了到数据库的持久连接,那么将这个值设 低一点并不可取!

最后一行是一个安全的方法。如果一个主机在连接到服务器时有问题,并重试很多次后放弃,那么这个主机就会被锁定,直到 FLUSH HOSTS 之后才能运行。默认情况下,10 次失败就足以导致锁定了。将这个值修改为 100 会给服务器足够的时间来从问题中恢复。如果重试 100 次都无法建立连接,那么使用再高的值也不会有太多帮助,可能它根本就无法连接。



回页首

缓冲区和缓存

MySQL 支持超过 100 个的可调节设置;但是幸运的是,掌握少数几个就可以满足大部分需要。查找这些设置的正确值可以通过 SHOW STATUS 命令查看状态变量,从中可以确定 mysqld 的运作情况是否符合我们的预期。给缓冲区和缓存分配的内存不能超过系统中的现有内存,因此调优通常都需要进行一些妥协。

MySQL 可调节设置可以应用于整个 mysqld 进程,也可以应用于单个客户机会话。

服务器端的设置

每个表都可以表示为磁盘上的一个文件,必须先打开,后读取。为了加快从文件中读取数据的过程,mysqld 对这些打开文件进行了缓存,其最大数目由 /etc/mysqld.conf 中的 table_cache 指定。清单 4 给出了显示与打开表有关的活动的方式。

清单 4. 显示打开表的活动

mysql> SHOW STATUS LIKE ‘open%tables’;
+—————+——-+
| Variable_name | Value |
+—————+——-+
| Open_tables | 5000 |
| Opened_tables | 195 |
+—————+——-+
2 rows in set (0.00 sec)

清单 4 说明目前有 5,000 个表是打开的,有 195 个表需要打开,因为现在缓存中已经没有可用文件描述符了(由于统计信息在前面已经清除了,因此可能会存在 5,000 个打开表中只有 195 个打开记录的情况)。如果 Opened_tables 随着重新运行 SHOW STATUS 命令快速增加,就说明缓存命中率不够。如果 Open_tablestable_cache 设置小很多,就说明该值太大了(不过有空间可以增长总不是什么坏事)。例如,使用 table_cache = 5000 可以调整表的缓存。

与表的缓存类似,对于线程来说也有一个缓存。 mysqld 在接收连接时会根据需要生成线程。在一个连接变化很快的繁忙服务器上,对线程进行缓存便于以后使用可以加快最初的连接。

清单 5 显示如何确定是否缓存了足够的线程。

清单 5. 显示线程使用统计信息

mysql> SHOW STATUS LIKE ‘threads%’;
+——————-+——–+
| Variable_name | Value |
+——————-+——–+
| Threads_cached | 27 |
| Threads_connected | 15 |
| Threads_created | 838610 |
| Threads_running | 3 |
+——————-+——–+
4 rows in set (0.00 sec)

此处重要的值是 Threads_created,每次 mysqld 需要创建一个新线程时,这个值都会增加。如果这个数字在连续执行 SHOW STATUS 命令时快速增加,就应该尝试增大线程缓存。例如,可以在 my.cnf 中使用 thread_cache = 40 来实现此目的。

关键字缓冲区保存了 MyISAM 表的索引块。理想情况下,对于这些块的请求应该来自于内存,而不是来自于磁盘。清单 6 显示了如何确定有多少块是从磁盘中读取的,以及有多少块是从内存中读取的。

清单 6. 确定关键字效率

mysql> show status like ‘%key_read%’;
+——————-+———–+
| Variable_name | Value |
+——————-+———–+
| Key_read_requests | 163554268 |
| Key_reads | 98247 |
+——————-+———–+
2 rows in set (0.00 sec)

Key_reads 代表命中磁盘的请求个数, Key_read_requests 是总数。命中磁盘的读请求数除以读请求总数就是不中比率 —— 在本例中每 1,000 个请求,大约有 0.6 个没有命中内存。如果每 1,000 个请求中命中磁盘的数目超过 1 个,就应该考虑增大关键字缓冲区了。例如,key_buffer = 384M 会将缓冲区设置为 384MB。

临时表可以在更高级的查询中使用,其中数据在进一步进行处理(例如 GROUP BY 字句)之前,都必须先保存到临时表中;理想情况下,在内存中创建临时表。但是如果临时表变得太大,就需要写入磁盘中。清单 7 给出了与临时表创建有关的统计信息。

清单 7. 确定临时表的使用

mysql> SHOW STATUS LIKE ‘created_tmp%’;
+————————-+——-+
| Variable_name | Value |
+————————-+——-+
| Created_tmp_disk_tables | 30660 |
| Created_tmp_files | 2 |
| Created_tmp_tables | 32912 |
+————————-+——-+
3 rows in set (0.00 sec)

每次使用临时表都会增大 Created_tmp_tables;基于磁盘的表也会增大 Created_tmp_disk_tables。对于这个比率,并没有什么严格的规则,因为这依赖于所涉及的查询。长时间观察 Created_tmp_disk_tables 会显示所创建的磁盘表的比率,您可以确定设置的效率。 tmp_table_sizemax_heap_table_size 都可以控制临时表的最大大小,因此请确保在 my.cnf 中对这两个值都进行了设置。

每个会话的设置

下面这些设置针对于每个会话。在设置这些数字时要十分谨慎,因为它们在乘以可能存在的连接数时候,这些选项表示大量的内存!您可以通过代码修改会话中的这些数字,或者在 my.cnf 中为所有会话修改这些设置。

当 MySQL 必须要进行排序时,就会在从磁盘上读取数据时分配一个排序缓冲区来存放这些数据行。如果要排序的数据太大,那么数据就必须保存到磁盘上的临时文件中,并再次进行排序。如果 sort_merge_passes 状态变量很大,这就指示了磁盘的活动情况。清单 8 给出了一些与排序相关的状态计数器信息。

清单 8. 显示排序统计信息

mysql> SHOW STATUS LIKE "sort%";
+——————-+———+
| Variable_name | Value |
+——————-+———+
| Sort_merge_passes | 1 |
| Sort_range | 79192 |
| Sort_rows | 2066532 |
| Sort_scan | 44006 |
+——————-+———+
4 rows in set (0.00 sec)

如果 sort_merge_passes 很大,就表示需要注意 sort_buffer_size。例如, sort_buffer_size = 4M 将排序缓冲区设置为 4MB。

MySQL 也会分配一些内存来读取表。理想情况下,索引提供了足够多的信息,可以只读入所需要的行,但是有时候查询(设计不佳或数据本性使然)需要读取表中大量数据。要理解这种行为,需要知道运行了多少个 SELECT 语句,以及需要读取表中的下一行数据的次数(而不是通过索引直接访问)。实现这种功能的命令如清单 9 所示。

清单 9. 确定表扫描比率

mysql> SHOW STATUS LIKE "com_select";
+—————+——–+
| Variable_name | Value |
+—————+——–+
| Com_select | 318243 |
+—————+——–+
1 row in set (0.00 sec)

mysql> SHOW STATUS LIKE "handler_read_rnd_next";
+———————–+———–+
| Variable_name | Value |
+———————–+———–+
| Handler_read_rnd_next | 165959471 |
+———————–+———–+
1 row in set (0.00 sec)

Handler_read_rnd_next / Com_select 得出了表扫描比率 —— 在本例中是 521:1。如果该值超过 4000,就应该查看 read_buffer_size,例如 read_buffer_size = 4M。如果这个数字超过了 8M,就应该与开发人员讨论一下对这些查询进行调优了!



回页首

3 个必不可少的工具

尽管在了解具体设置时,SHOW STATUS 命令会非常有用,但是您还需要一些工具来解释 mysqld 所提供的大量数据。我发现有 3 个工具是必不可少的;在 参考资料 一节中您可以找到相应的链接。

大部分系统管理员都非常熟悉 top 命令,它为任务所消耗的 CPU 和内存提供了一个不断更新的视图。 mytoptop 进行了仿真;它为所有连接上的客户机以及它们正在运行的查询提供了一个视图。mytop 还提供了一个有关关键字缓冲区和查询缓存效率的实时数据和历史数据,以及有关正在运行的查询的统计信息。这是一个很有用的工具,可以查看系统中(比如 10 秒钟之内)的状况,您可以获得有关服务器健康信息的视图,并显示导致问题的任何连接。

mysqlard 是一个连接到 MySQL 服务器上的守护程序,负责每 5 分钟搜集一次数据,并将它们存储到后台的一个 Round Robin Database 中。有一个 Web 页面会显示这些数据,例如表缓存的使用情况、关键字效率、连接上的客户机以及临时表的使用情况。尽管 mytop 提供了服务器健康信息的快照,但是 mysqlard 则提供了长期的健康信息。作为奖励,mysqlard 使用自己搜集到的一些信息针对如何对服务器进行调优给出一些建议。

搜集 SHOW STATUS 信息的另外一个工具是 mysqlreport。其报告要远比 mysqlard 更加复杂,因为需要对服务器的每个方面都进行分析。这是对服务器进行调优的一个非常好的工具,因为它对状态变量进行适当计算来帮助确定需要修正哪些问题。



回页首

结束语

分享这篇文章……

digg 将本文提交到 Digg del.icio.us 发布到 del.icio.us

本 文介绍了对 MySQL 进行调优的一些基础知识,并对这个针对 LAMP 组件进行调优的 3 部分系列文章进行了总结。调优很大程度上需要理解组件的工作原理,确定它们是否正常工作,进行一些调整,并重新评测。每个组件 —— Linux、Apache、PHP 或 MySQL —— 都有各种各样的需求。分别理解各个组件可以帮助减少可能会导致应用程序速度变慢的瓶颈。

参考资料

学习

获得产品和技术

  • 尽管已经出版了 3 年之久了, High Performance MySQL 仍然是非常有价值的一本书。作者也有一个 Web 站点介绍 有关 MySQL 的各种文章
  • mytop 告诉您目前 MySQL 服务器上都在进行什么操作,并提供一些关键的统计信息。在发现数据库有问题时,应该首先求助于这个程序。
  • mysqlard 会给出 MySQL 服务器一个关键性能指示器的图形表示,并给出一些调优建议。
  • mysqlreport 是一个必须的工具。它为您分析 SHOW STATUS 变量。
  • MySQL 文章如果没有提供到 phpMyAdmin 的链接,就说不上完整。尽管已经给出了对状态变量的一些解释,但是这个产品的强大之处在于如何简化管理任务。
  • 定购 SEK for Linux,共包含两张 DVD,其中有用于 Linux 的最新 IBM 试用软件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
  • 利用可直接从 developerWorks 下载的 IBM 试用软件 在 Linux 上构建您的下一个开发项目。

讨论

关于作者

Sean Walberg 的照片

从 1994 开始,Sean Walberg 就一直在学术、企业和 Internet 服务提供者环境中从事 Linux 和 UNIX 系统的研究。在过去几年里,他撰写了大量有关系统管理的文章。您可以通过 与 Sean 联系。

php5 xml学习之xslt

看了下,php5的XSLT十分简单,举例子说明之.

首先是XML

<?xml version=’1.0′ ?>
<contacts>
<contact idx="37">
<name>Ramsey White II</name>
<category>Family</category>
<phone type="home">301-555-1212</phone>
<meta id="x634724" />
</contact>
<contact idx="42">
<name>Stratis Kakadelis</name>
<category>Friends</category>
<phone type="home">240-555-1212</phone>
<phone type="work">410-555-7676</phone>
<email>skak@example.com</email>
<meta id="y49302" />
</contact>
<contact idx="57">
<name>Kelly Williamson</name>
<category>Friends</category>
<phone type="cell">443-555-9999</phone>
<email>kwill@example.com</email>
<email>dynky@tech.example.com</email>
<meta id="w4r302" />
</contact>
</contacts> 之后是XSLT

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<xsl:template match="contacts">
<html><head><title>Contacts!</title></head><body>
<div style="border: 2px solid blue; padding: 5px;">
<h1>Contacts:</h1>
<xsl:apply-templates />
</div></body></html>
</xsl:template>
<xsl:template match="contact">
<div style="border: 1px solid black; margin: 20px; padding: 5px;">
<h2><xsl:value-of select="name" /></h2>
<p>
Home Phone: <xsl:value-of select="phone[@type=’home’]" /><br />
Work Phone: <xsl:value-of select="phone[@type=’work’]" /><br />
Cell Phone: <xsl:value-of select="phone[@type=’cell’]" /><br />
</p>
</div>
</xsl:template>
</xsl:stylesheet>
最后是调用的php,这里用的是dom

<?php
// Using the DOM extension, load the XML file into memory:
$dom = new DOMDocument();
$dom->load(‘contacts.xml’);
// Now also load the XSL file as well:
$xsl = new DOMDocument();
$xsl->load(‘contacts.xsl’);
// Create a new XSLT Processor
$proc = new XSLTProcessor;
// Import the XSL styles into this processor
$proc->importStyleSheet($xsl);
// Now transform the XML file and echo it to the screen!
echo $proc->transformToXML($dom);
?>

解析大型或复杂 XML 文档的 PHP5 技术

简介

PHP5 提供了更多的 XML 解析技术。James Clark 的 Expat SAX 解析器(现在以 libxml2 为基础)不再是惟一功能完备的解析器。经常需要使用完全符合 W3C 标准的 DOM 解析器进行解析。无论第 1 部分(请参阅 参考资料) 介绍的 SimpleXML 还是比 SAX 更简单更快捷的 XMLReader 都提供了另外的解析方法。所有这些 XML 扩展现在都以 GNOME 项目的 libxml2 库为基础。这个统一的库考虑了不同扩展之间的互操作性。本文将介绍 PHP5 XML 解析技术,特别是大型、复杂 XML 文档的解析。还介绍了关于解析技术的一些背景知识,何种方法最适合于何种类型的 XML 文档,如果要作出选择,则应依据何种标准。



回页首

SimpleXML

请访问 面向 Perl 和 PHP 开发人员的 XML:您可以通过该专题来了解更多与 Perl 和 PHP 相关的 XML 技术。

第 1 部分介绍了 XML 的基本信息,主要介绍简单的、入门级的应用程序编程接口(Application Programming Interfaces,API)。通过例子说明对于处理简单、可预测并且不大的 XML 文档,SimpleXML(必要的时候与文档对象模型(DOM)结合使用)是一种理想的选择。

XML 和 PHP5

可扩展标记语言(Extensible Markup Language,XML)不仅被看作是一种标记语言,而且是一种基于文本的数据存储格式,它提供了基于文本的方法来应用和描述信息的树状结构。

PHP5 提供了一些全新的和重新编写的 XML 解析扩展。其中包括将整个 XML 文档加载到内存中的 SimpleXML、DOM 和 XSLT 处理程序。也有每次把 XML 文档的一部分加载到内存中的 Simple API for XML (SAX) 和 XMLReader。SAX 的功能和在 PHP4 中没有变化,但不再以 expat 库为基础而改用了 libxml2 库。如果通过其他语言熟悉了 DOM,则与以前的版本相比,在 PHP5 中使用 DOM 编程将简单得多。



回页首

XML 解析基础

解析 XML 有两种基本的方式:树和流。树解析方式需要将整个 XML 文档加载到内存中。树文件结构允许随机访问文档元素和编辑 XML。树型解析的例子包括 DOM 和 SimpleXML。这些解析器都在内存中以不同但可互操作的格式共享树状结构。和树解析方式不同,流解析不需要将整个文档加载到内存中。这里的流和流音 频中的流意思很相近。其用途和目的都一样,就是每次提交少量数据以节约带宽和内存。在流解析中,只能访问当前解析的节点,并且不能将 XML 作为一个文档来编辑。流解析器的例子包括 XMLReader 和 SAX。



回页首

基于树的解析器

之所以称为基于树的解析器,是因为它们将整个 XML 文档加载到内存中,并把文档的根作为主干,把所有的儿子、孙子和它们的后代以及属性作为分支。最熟悉的基于树的解析器是 DOM。编码最简单的基于树的解析器是 SimpleXML。后面对两者都将作出介绍。

使用 DOM 解析

根据 W3C 的定义,DOM 标准是 “……一种平台和语言中立的接口,能够让程序和脚本动态地访问和更新文档的内容、结构和样式。” GNOME 项目的 libxml2 库用 C 实现了 DOM 及其全部方法。因为所有的 PHP5 XML 扩展都基于 libxml2,所以彼此之间具有完全的互操作性。这种互操作性大大增强了它们的功能。比方说,可以使用流解析器 XMLReader 获取一个元素,将其导入 DOM,然后用 XPath 提取数据。这就大大增加了灵活性。清单 5 给出了一个例子。

DOM 是基于树的解析器。DOM 很容易理解和使用,因为其内存结构与原始 XML 文档相似。DOM 通过创建对象树来向应用程序传递信息,它完全复制了 XML 文件的元素树,每个 XML 元素都是树上的一个节点。DOM 是一种 W3C 标准,由于和其他编程语言的一致性,对于开发人员来说,为 DOM 增加了不少权威性。因为 DOM 要创建整个文档的树,要占用大量内存和处理器时间。

使用 DOM

如果由于受设计或者其他因素的限制必须在解析器领域内耍点小聪明的话,则仅仅从灵活的角度来看应该选择 DOM。使用 DOM 可以构建、修改、查询、验证和转换 XML 文档。可以利用所有的 DOM 方法和属性。多数 DOM level 2 方法的实现都有适当的属性支持。由于非凡的灵活性,使用 DOM 可以解析任意复杂的文档。但是要记住,如果要把很大的 XML 文档一次加载到内存中,则取得灵活性的代价相当高昂。

清单 1 中的例子用 DOM 解析文档,通过 getElementById 检索一个元素。引用 ID 之前需要设置 validateOnParse=true 来验证文档。根据 DOM 标准,它要求 DTD 定义一个 ID 类型的属性 ID。

清单 1. 使用 DOM 处理基本文档
<?php

$doc = new DomDocument;

// We must validate the document before referring to the id
$doc->validateOnParse = true;
$doc->Load(‘basic.xml’);

echo "The element whose id is myelement is: " .
$doc->getElementById(‘myelement’)->tagName . "n";

?>

getElementsByTagName() 函数返回类 DOMNodeList 的一个新实例,包含使用给定标记名的元素。当然这个列表需要遍历。迭代 getElementsByTagName() 返回的 NodeList 的同时修改文档结构,会影响迭代的 NodeList(参见清单 2)。不需要验证。

清单 2. DOM getElementsByTagName 方法
DOMDocument {
DOMNodeList getElementsByTagName(string name);
}

清单 3 中的例子结合使用了 DOM 和 XPath。

清单 3. 使用 DOM 和 XPath 进行解析
<?php

$doc = new DOMDocument;

// We don’t want to bother with white spaces
$doc->preserveWhiteSpace = false;

$doc->Load(‘book.xml’);

$xpath = new DOMXPath($doc);

// We start from the root element
$query = ‘//book/chapter/para/informaltable/tgroup/tbody/row/entry[. = "en"]’;

$entries = $xpath->query($query);

foreach ($entries as $entry) {
echo "Found {$entry->previousSibling->previousSibling->nodeValue}," .
" by {$entry->previousSibling->nodeValue}n";
}
?>

说了 DOM 的这么多优点之后,为了强调我的观点,最后再给出一个错误使用 DOM 的例子,然后在后面的例子中解决这个问题。清单 4 中的例子将一个很大的文件加载到 DOM 中,只是为了用 DomXpath 从一个属性中提取数据。

清单 4. 错误使用 DOM 和 XPath,用于大型 XML 文档
<?php

// Parsing a Large Document with DOM and DomXpath
// First create a new DOM document to parse
$dom = new DomDocument();

// This document is huge and we don’t really need anything from the tree
// This huge document uses a huge amount of memory
$dom->load("tooBig.xml");
$xp = new DomXPath($dom);
$result = $xp->query("/blog/entries/entry[@ID = 5225]/title") ;
print $result->item(0)->nodeValue ."n";

?>

后面清单 5 中的例子仍然使用 DOM 和 XPath,但每次让 XMLReader 用 expand() 传递一个元素的数据。通过这种方法就能将 XMLReader 传递的节点转换成 DOMElement

清单 5. 正确使用 DOM 和 XPath,用于大型 XML 文档
<?php

// Parsing a large document with XMLReader with Expand – DOM/DOMXpath
$reader = new XMLReader();

$reader->open("tooBig.xml");

while ($reader->read()) {
switch ($reader->nodeType) {
case (XMLREADER::ELEMENT):
if ($reader->localName == "entry") {
if ($reader->getAttribute("ID") == 5225) {
$node = $reader->expand();
$dom = new DomDocument();
$n = $dom->importNode($node,true);
$dom->appendChild($n);
$xp = new DomXpath($dom);
$res = $xp->query("/entry/title");
echo $res->item(0)->nodeValue;
}
}
}
}

?>

使用 SimpleXML 解析

SimpleXML 扩展是另外一种 XML 文档解析方法。SimpleXML 扩展需要用到 PHP5 并包括内置的 XPath 支持。SimpleXML 最适合处理不复杂的、基本的 XML 数据。如果 XML 文档不是很复杂、层次不深、没有混合内容,则与 DOM 相比 SimpleXML 更简单,正如其名称所暗示的那样。如果处理的文档结构是已知的,就会更加直观。

使用 SimpleXML

SimpleXML 具有 DOM 的很多优点,但是编码更加简单。它允许轻松地访问 XML 树,具有内置的验证机制和 XPath 支持,能够与 DOM 互操作,为其提供读写 XML 文档的支持。可以简单迅速地处理使用 SimpleXML 解析的文档。但是要记住,和 DOM 一样,SimpleXML 的易用性和灵活性的代价也是无法向内存中加载大型 XML 文档。

清单 6 中的代码从示例 XML 中提取 <plot>。

清单 6. 提取 plot 文本
<?php
$xmlstr = <<<XML
<?xml version=’1.0′ standalone=’yes’?>
<books>
<book>
<title>Great American Novel<title>
<plot>
Cliff meets Lovely Woman. Loyal Dog sleeps, but
wakes up to bark at mailman.
</plot>
<success type="bestseller">4<success>
<success type="bookclubs">9<success>
</book>
<books>
XML;
?>
<?php

$xml = new SimpleXMLElement($xmlstr);
echo $xml->book[0]->plot; // "Cliff meets Lovely Woman. …"
?>

另一方面,也许还要提取分为多行的地址。如果同一个父元素中存在同一元素的多个实例,通常需要使用迭代技术。清单 7 中的代码演示了此项功能。

清单 7. 提取元素的多个实例
<?php
$xmlstr = <<<XML
<xml version=’1.0′ standalone=’yes’?>
<books>
<book>
<title>Great American Novel<title>
<plot>
Cliff meets Lovely Woman.
<plot>
<success type="bestseller">4<success>
<success type="bookclubs">9</success>
<book>
<book>
<title>Man Bites Dog</title>
<plot>
Reporter invents a prize-winning story.
</plot>
<success type="bestseller">22<success>
<success type="bookclubs">3<success>
<book>
</books>
XML;
?>
<php

$xml = new SimpleXMLElement($xmlstr);

foreach ($xml->book as $book) {
echo $book->plot, ‘<br />’;
}
?

除了读取元素名称及其值以外,SimpleXML 也能访问元素的属性。清单 8 所示的代码中,可以像访问数组元素一样访问元素的属性。

清单 8. SimpleXML 访问元素的属性的演示
<?php
$xmlstr = <<<XML
<?xml version=’1.0′ standalone=’yes’?>
<books>
<book>
<title>Great American Novel</title>
<plot>
Cliff meets Lovely Woman.
</plot>
<success type="bestseller">4</success>
<success type="bookclubs">9</success>
</book>
<book>
<title>Man Bites Dog</title>
<plot>
Reporter invents a prize-winning story.
<plot>
<success type="bestseller">22<success>
<success type="bookclubs">3</success>
</book>
<books>
XML;
?>
<?php

$xml = new SimpleXMLElement($xmlstr);

foreach ($xml->book[0]->success as $success) {
switch((string) $success[‘type’]) {
case ‘bestseller’:
echo $success, ‘ months on bestseller list<br />’;
break;
case ‘bookclubs’:
echo $success, ‘ bookclub listings<br />’;
break;
}
}

?>

最后一个例子(参见清单 9)结合使用了 SimpleXML 以及 DOM 和 XMLReader。通过 XMLReader,每次使用 expand() 传递一个元素的数据。这样就能将 XMLReader 传递的节点转换成 DOMElement 再传递给 SimpleXML 了。

清单 9. 结合 SimpleXML 以及 DOM 和 XMLReader 解析大型 XML 文档

<?php

// Parsing a large document with Expand and SimpleXML
$reader = new XMLReader();

$reader->open("tooBig.xml");

while ($reader->read()) {
switch ($reader->nodeType) {
case (XMLREADER::ELEMENT):
if ($reader->localName == "entry") {
if ($reader->getAttribute("ID") == 5225) {
$node = $reader->expand();
$dom = new DomDocument();
$n = $dom->importNode($node,true);
$dom->appendChild($n);
$sxe = simplexml_import_dom($n);
echo $sxe->title;
}
}
}
}

?>



回页首

基于流的解析器

之所以称为基于流的解析器,是因为它们采用和流式音频同样的原理来解析 XML 流,处理一个特殊节点,完成之后则将其完全忘掉。XMLReader 是一种 pull 解析器,其编码方法和数据库查询结果表的游标中非常类似。因此更容易处理不熟悉的或者不可预测的 XML 文件。

使用 XMLReader 解析

XMLReader 扩展是一种基于流的解析器,其类型通常被称为游标类型或者 pull 类型解析器。XMLReader 根据请求从 XML 文档中获取信息。它是基于派生自 C# XmlTextReader 的 API。PHP 5.1 默认包含并启用它,基于 libxml2。在 PHP 5.1 之前,默认不启用 XMLReader 扩展,但是可从 PECL(请参阅 参考资料 中的相关链接)下载。XMLReader 支持名称空间和验证,包括 DTD 和 Relaxed NG。

使用 XMLReader

XMLReader 作为一种流解析器,非常适合解析大型 XML 文档,编码比 SAX 更简单,而且通常速度也快。这是理想的流解析器。

清单 10 中的例子使用 XMLReader 解析大型 XML 文档。

清单 10. XMLReader 解析大型 XML 文件
<?php

$reader = new XMLReader();
$reader->open("tooBig.xml");
while ($reader->read()) {
switch ($reader->nodeType) {
case (XMLREADER::ELEMENT):
if ($reader->localName == "entry") {
if ($reader->getAttribute("ID") == 5225) {
while ($reader->read()) {
if ($reader->nodeType == XMLREADER::ELEMENT) {
if ($reader->localName == "title") {
$reader->read();
echo $reader->value;
break;
}
if ($reader->localName == "entry") {
break;
}
}
}
}
}
}
}
?>

使用 SAX 解析

Simple API for XML (SAX) 是一种流解析器。事件与读入的 XML 文档相关联,因此 SAX 以回调的方式编码。元素打开关闭标记、元素内容、实体和解析错误都有对应的事件。使用 SAX 解析器而不是 XMLReader 的主要原因在于 SAX 解析器有时候效率更高一些,而且通常更被人们熟悉。SAX 解析器的主要缺点是代码很复杂,比 XMLReader 代码编写起来更难。

使用 SAX

SAX 对于那些曾经在 PHP4 中处理过 XML 的人来说可能比较熟悉,PHP5 中的 SAX 扩展与过去的版本兼容。由于也是流解析器,因此非常适合处理大型文件,但是比不上 XMLReader。

清单 11 中的例子使用 SAX 解析大型 XML 文档。

清单 11. 使用 SAX 解析大型 XML 文件
<?php

//This class contains all the callback methods that will actually
//handle the XML data.
class SaxClass {
private $hit = false;
private $titleHit = false;

//callback for the start of each element
function startElement($parser_object, $elementname, $attribute) {
if ($elementname == "entry") {
if ( $attribute[‘ID’] == 5225) {
$this->hit = true;
} else {
$this->hit = false;
}
}
if ($this->hit && $elementname == "title") {
$this->titleHit = true;
} else {
$this->titleHit =false;
}
}

//callback for the end of each element
function endElement($parser_object, $elementname) {
}

//callback for the content within an element
function contentHandler($parser_object,$data)
{
if ($this->titleHit) {
echo trim($data)."<br />";
}
}
}

//Function to start the parsing once all values are set and
//the file has been opened
function doParse($parser_object) {
if (!($fp = fopen("tooBig.xml", "r")));

//loop through data
while ($data = fread($fp, 4096)) {
//parse the fragment
xml_parse($parser_object, $data, feof($fp));
}
}

$SaxObject = new SaxClass();
$parser_object = xml_parser_create();
xml_set_object ($parser_object, $SaxObject);

//Don’t alter the case of the data
xml_parser_set_option($parser_object, XML_OPTION_CASE_FOLDING, false);

xml_set_element_handler($parser_object,"startElement","endElement");
xml_set_character_data_handler($parser_object, "contentHandler");

doParse($parser_object);

?>



回页首

结束语

共享这篇文章……

digg 将本文提交到 Digg del.icio.us 发布到 del.icio.us

PHP5 提供了多种改进的解析技术。最常见的有 DOM,它现在完全与 W3C 标准兼容,适合复杂但是相对较小的文档。SimpleXML 适合简单而且不太大的 XML 文档,XMLReader 比 SAX 更简单、更快,是适合大型文档的流解析器。

在PHP5中使用DOM控制XML

PHP5中增强了XML的支持,使用DOM扩展了XML操作的能耐。这些函数作为 PHP5 核心的一部分,无需被安装即可使用。 6KQLinux联盟
   6KQLinux联盟
   下面的例子简单的演示了DOM对XML的操作,详细解释请看代码中的注释 6KQLinux联盟
   6KQLinux联盟
   <? 6KQLinux联盟
   /************************************************ 6KQLinux联盟
   ** use XML in PHP5 6KQLinux联盟
   ** reference site: 6KQLinux联盟
   ** http://cn.php.net/manual/zh/ref.dom.php 6KQLinux联盟
   ** the follow codes need PHP5 support 6KQLinux联盟
   ** 6KQLinux联盟
   *************************************************/ 6KQLinux联盟
   6KQLinux联盟
   6KQLinux联盟
   //首先要创建一个DOMDocument对象 6KQLinux联盟
   $dom = new DomDocument(); 6KQLinux联盟
   //然后载入XML文件 6KQLinux联盟
   $dom -> load("test.xml"); 6KQLinux联盟
   6KQLinux联盟
   //输出XML文件 6KQLinux联盟
   //header("Content-type: text/xml;charset=gb2312"); 6KQLinux联盟
   //echo $dom -> saveXML(); 6KQLinux联盟
   6KQLinux联盟
   //保存XML文件,返回值为int(文件大小,以字节为单位) 6KQLinux联盟
   //$dom -> save("newfile.xml"); 6KQLinux联盟
   6KQLinux联盟
   echo "<hr/>取得所有的title元素:<hr/>"; 6KQLinux联盟
   $titles = $dom -> getElementsByTagName("title"); 6KQLinux联盟
   foreach ($titles as $node) 6KQLinux联盟
   { 6KQLinux联盟
   echo $node -> textContent . "<br/>"; 6KQLinux联盟
   //这样也可以 6KQLinux联盟
   //echo $node->firstChild->data . "<br/>"; 6KQLinux联盟
   } 6KQLinux联盟
   6KQLinux联盟
   /* 6KQLinux联盟
   echo "<hr/>从根结点遍历所有结点:<br/>"; 6KQLinux联盟
   foreach ($dom->documentElement->childNodes as $items) { 6KQLinux联盟
   //如果节点是一个元素(nodeType == 1)并且名字是item就继续循环 6KQLinux联盟
   if ($items->nodeType == 1 && $items->nodeName == "item") { 6KQLinux联盟
   foreach ($items->childNodes as $titles) { 6KQLinux联盟
   //如果节点是一个元素,并且名字是title就打印它. 6KQLinux联盟
   if ($titles->nodeType == 1 && $titles->nodeName == "title") { 6KQLinux联盟
   print $titles->textContent . "n"; 6KQLinux联盟
   } 6KQLinux联盟
   } 6KQLinux联盟
   } 6KQLinux联盟
   } 6KQLinux联盟
   */ 6KQLinux联盟
   6KQLinux联盟
   //使用XPath查询数据 6KQLinux联盟
   echo "<hr/>使用XPath查询的title节点结果:<hr/>"; 6KQLinux联盟
   $xpath = new domxpath($dom); 6KQLinux联盟
   $titles = $xpath->query("/rss/channel/item/title"); 6KQLinux联盟
   foreach ($titles as $node) 6KQLinux联盟
   { 6KQLinux联盟
   echo $node->textContent."<br/>"; 6KQLinux联盟
   } 6KQLinux联盟
   /* 6KQLinux联盟
   这样和使用getElementsByTagName()方法差不多,但是Xpath要强大的多 6KQLinux联盟
   深入一点可能是这样: 6KQLinux联盟
   /rss/channel/item[position() = 1]/title 返回第一个item元素的所有 6KQLinux联盟
   /rss/channel/item/title[@id = ’23’] 返回所有含有id属性并且值为23的title 6KQLinux联盟
   /rss/channel/&folder&/title 返回所有articles元素下面的title(译者注:&folder&代表目录深度) 6KQLinux联盟
   */ 6KQLinux联盟
   6KQLinux联盟
   6KQLinux联盟
   //向DOM中写入新数据 6KQLinux联盟
   $item = $dom->createElement("item"); 6KQLinux联盟
   $title = $dom->createElement("title"); 6KQLinux联盟
   $titleText = $dom->createTextNode("title text"); 6KQLinux联盟
   $title->appendChild($titleText); 6KQLinux联盟
   $item->appendChild($title); 6KQLinux联盟
   $dom->documentElement->getElementsByTagName(‘channel’)->item(0)->appendChild($item); 6KQLinux联盟
   6KQLinux联盟
   //从DOM中删除节点 6KQLinux联盟
   //$dom->documentElement->RemoveChild($dom->documentElement->getElementsByTagName("channel")->item(0)); 6KQLinux联盟
   //或者使用xpath查询出节点再删除 6KQLinux联盟
   //$dom->documentElement->RemoveChild($xpath->query("/rss/channel")->item(0)); 6KQLinux联盟
   //$dom->save("newfile.xml"); 6KQLinux联盟
   6KQLinux联盟
   //从DOM中修改节点数据 6KQLinux联盟
   //修改第一个title的文件 6KQLinux联盟
   //这个地方比较笨,新创建一个节点,然后替换旧的节点。如果哪位朋友有其他好的方法请一定要告诉我 6KQLinux联盟
   $firstTitle = $xpath->query("/rss/channel/item/title")->item(0); 6KQLinux联盟
   $newTitle = $dom->createElement("title"); 6KQLinux联盟
   $newTitle->appendChild(new DOMText("This’s the new title text!!!")); 6KQLinux联盟
   $firstTitle->parentNode->replaceChild($newTitle, $firstTitle); 6KQLinux联盟
   //修改属性 6KQLinux联盟
   //$firstTitle = $xpath->query("/rss/channel/item/title")->item(0); 6KQLinux联盟
   //$firstTitle->setAttribute("orderby", "4"); 6KQLinux联盟
   $dom->save("newfile.xml"); 6KQLinux联盟
   6KQLinux联盟
   echo "<hr/><a href="newfile.xml">查看newfile.xml</a>"; 6KQLinux联盟
   6KQLinux联盟
   //下面的代码获得并解析php.net的首页,将返第一个title元素的内容。 6KQLinux联盟
   /* 6KQLinux联盟
   $dom->loadHTMLFile("http://www.php.net/"); 6KQLinux联盟
   $title = $dom->getElementsByTagName("title"); 6KQLinux联盟
   print $title->item(0)->textContent; 6KQLinux联盟
   */ 6KQLinux联盟
   ?> 6KQLinux联盟
   6KQLinux联盟
   下面是test.xml文件代码: 6KQLinux联盟
   6KQLinux联盟
   <?xml version="1.0" encoding="gb2312"?> 6KQLinux联盟
   <rss version="2.0"> 6KQLinux联盟
   <channel> 6KQLinux联盟
   <title>javascript</title> 6KQLinux联盟
   <link>http://blog.csdn.net/zhongmao/category/29515.aspx</link> 6KQLinux联盟
   <description>javascript</description> 6KQLinux联盟
   <language>zh-chs</language> 6KQLinux联盟
   <generator>.text version 0.958.2004.2001</generator> 6KQLinux联盟
   <item> 6KQLinux联盟
   <creator>zhongmao</creator> 6KQLinux联盟
   <title orderby="1">out put Excel used javascript</title> 6KQLinux联盟
   <link>http://blog.csdn.net/zhongmao/archive/2004/09/15/105385.aspx</link> 6KQLinux联盟
   <pubdate>wed, 15 sep 2004 13:32:00 gmt</pubdate> 6KQLinux联盟
   <guid>http://blog.csdn.net/zhongmao/archive/2004/09/15/105385.aspx</guid> 6KQLinux联盟
   <comment>http://blog.csdn.net/zhongmao/comments/105385.aspx</comment> 6KQLinux联盟
   <comments>http://blog.csdn.net/zhongmao/archive/2004/09/15/105385.aspx#feedback6KQLinux联盟
</comments> 6KQLinux联盟
   <comments>2</comments> 6KQLinux联盟
   <commentrss>http://blog.csdn.net/zhongmao/comments/commentrss/105385.aspx6KQLinux联盟
</commentrss> 6KQLinux联盟
   <ping>http://blog.csdn.net/zhongmao/services/trackbacks/105385.aspx</ping> 6KQLinux联盟
   <description>test description</description> 6KQLinux联盟
   </item> 6KQLinux联盟
   <item> 6KQLinux联盟
   <creator>zhongmao</creator> 6KQLinux联盟
   <title orderby="2">out put word used javascript</title> 6KQLinux联盟
   <link>http://blog.csdn.net/zhongmao/archive/2004/08/06/67161.aspx</link> 6KQLinux联盟
   <pubdate>fri, 06 aug 2004 16:33:00 gmt</pubdate> 6KQLinux联盟
   <guid>http://blog.csdn.net/zhongmao/archive/2004/08/06/67161.aspx</guid> 6KQLinux联盟
   <comment>http://blog.csdn.net/zhongmao/comments/67161.aspx</comment> 6KQLinux联盟
   <comments>http://blog.csdn.net/zhongmao/archive/2004/08/06/67161.aspx#feedback6KQLinux联盟
</comments> 6KQLinux联盟
   <comments>0</comments> 6KQLinux联盟
   <commentrss>http://blog.csdn.net/zhongmao/comments/commentrss/67161.aspx6KQLinux联盟
</commentrss> 6KQLinux联盟
   <ping>http://blog.csdn.net/zhongmao/services/trackbacks/67161.aspx</ping> 6KQLinux联盟
   <description>test word description</description> 6KQLinux联盟
   </item> 6KQLinux联盟
   <item> 6KQLinux联盟
   <creator>zhongmao</creator> 6KQLinux联盟
   <title orderby="3">xmlhttp</title> 6KQLinux联盟
   <link>http://blog.csdn.net/zhongmao/archive/2004/08/02/58417.aspx</link> 6KQLinux联盟
   <pubdate>mon, 02 aug 2004 10:11:00 gmt</pubdate> 6KQLinux联盟
   <guid>http://blog.csdn.net/zhongmao/archive/2004/08/02/58417.aspx</guid> 6KQLinux联盟
   <comment>http://blog.csdn.net/zhongmao/comments/58417.aspx</comment> 6KQLinux联盟
   <comments>http://blog.csdn.net/zhongmao/archive/2004/08/02/58417.aspx#feedback6KQLinux联盟
</comments> 6KQLinux联盟
   <comments>0</comments> 6KQLinux联盟
   <commentrss>http://blog.csdn.net/zhongmao/comments/commentrss6KQLinux联盟
/58417.aspx</commentrss> 6KQLinux联盟
   <ping>http://blog.csdn.net/zhongmao/services/trackbacks/58417.aspx</ping> 6KQLinux联盟
   <description>xmlhttpaaa asd bb cc dd</description> 6KQLinux联盟
   </item> 6KQLinux联盟
   </channel> 6KQLinux联盟
   </rss>

如何使用PHP DOM创建动态XML文件

当处理基于XML应用程序时,开发者经常需要建立XML编码数据结构。例如,Web中基于用户输入的XML状态模板,服务器请求XML语句,以及基于运行时间参数的客户响应。

尽管XML数据结构的构建比较费时,但如果使用成熟的PHP DOM应用程序接口,一切都会变得简单明了。本文将向你介绍PHP DOM应用程序接口的主要功能,演示如何生成一个正确的XML完整文件并将其保存到磁盘中。

创建文档类型声明

一般而言,XML声明放在文档顶部。在PHP中声明十分简单:只需实例化一个DOM文档类的对象并赋予它一个版本号。查看程序清单A

程序清单 A

<?php
// create doctype
$dom = new DOMDocument("1.0");

// display document in browser as plain text
// display document in browser as plain text
// for readability purposes
header("Content-Type: text/plain");

// save and display tree
echo $dom->saveXML();
?>

请注意DOM文档对象的saveXML()方法。稍后我再详细介绍这一方法,现在你只需要简单认识到它用于输出XML文档的当前快照到一个文件或浏览器。在本例,为增强可读性,我已经将ASCII码文本直接输出至浏览器。在实际应用中,可将以text/XML头文件发送到浏览器。

如在浏览器中查看输出,你可看到如下代码:

<?xml version="1.0"?>

添加元素和文本节点

XML真正强大的功能是来自其元素与封装的内容。幸运的是,一旦你初始化DOM文档,很多操作变得很简单。此过程包含如下两步骤:

  • 对想添加的每一元素或文本节点,通过元素名或文本内容调用DOM文档对象的createElement()或createTextNode()方法。这将创建对应于元素或文本节点的新对象。
  • 通过调用节点的appendChild()方法,并把其传递给上一步中创建的对象,并在XML文档树中将元素或文本节点添加到父节点。

以下范例将清楚地演示这2步骤,请查看程序清单B

程序清单 B

<?php
// create doctype
$dom = new DOMDocument("1.0");

// display document in browser as plain text
// for readability purposes
header("Content-Type: text/plain");

// create root element
$root = $dom->createElement("toppings");
$dom->appendChild($root);

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create text node
$text = $dom->createTextNode("pepperoni");
$item->appendChild($text);

// save and display tree
echo $dom->saveXML();
?>

这 里,我首先创建一个名字为<toppings>的根元素,并使它归于XML头文件中。然后,我建立名为<item>的元素并使它 归于根元素。最后,我又创建一个值为“pepperoni”的文本节点并使它归于<item>元素。最终结果如下:

<?xml version="1.0"?>
<toppings>
?<item>pepperoni</item>
</toppings>

如果你想添加另外一个topping,只需创建另外一个<item>并添加不同的内容,如程序清单C所示。

程序清单C

<?php
// create doctype
$dom = new DOMDocument("1.0");

// display document in browser as plain text
// for readability purposes
header("Content-Type: text/plain");

// create root element
$root = $dom->createElement("toppings");
$dom->appendChild($root);

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create text node
$text = $dom->createTextNode("pepperoni");
$item->appendChild($text);

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create another text node
$text = $dom->createTextNode("tomato");
$item->appendChild($text);

// save and display tree
echo $dom->saveXML();
?>

以下是执行程序清单C后的输出:

<?xml version="1.0"?>
<toppings>
?<item>pepperoni</item>
?<item>tomato</item>
</toppings>

添加属性

通过使用属性,你也可以添加适合的信息到元素。对于PHP DOM API,添加属性需要两步:首先用DOM文档对象的createAttribute()方法创建拥有此属性名字的节点,然后将文档节点添加到拥有属性值的属性节点。详见程序清单D。

程序清单 D

<?php
// create doctype
$dom = new DOMDocument("1.0");

// display document in browser as plain text
// for readability purposes
header("Content-Type: text/plain");
// create root element
$root = $dom->createElement("toppings");
$dom->appendChild($root);

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create text node
$text = $dom->createTextNode("pepperoni");
$item->appendChild($text);

// create attribute node
$price = $dom->createAttribute("price");
$item->appendChild($price);

// create attribute value node
$priceValue = $dom->createTextNode("4");
$price->appendChild($priceValue);

// save and display tree
echo $dom->saveXML();
?>

输出如下所示:

<?xml version="1.0"?>
<toppings>
?<item price="4">pepperoni</item>
</toppings>

添加CDATA模块和过程向导

虽然不经常使用CDATA模块和过程向导,但是通过调用DOM文档对象的createCDATASection()createProcessingInstruction()方法 PHP API 也能很好地支持CDATA和过程向导,请见程序清单E。

程序清单 E

<?php
// create doctype
// create doctype
$dom = new DOMDocument("1.0");

// display document in browser as plain text
// for readability purposes
header("Content-Type: text/plain");

// create root element
$root = $dom->createElement("toppings");
$dom->appendChild($root);

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create text node
$text = $dom->createTextNode("pepperoni");
$item->appendChild($text);

// create attribute node
$price = $dom->createAttribute("price");
$item->appendChild($price);

// create attribute value node
$priceValue = $dom->createTextNode("4");
$price->appendChild($priceValue);

// create CDATA section
$cdata = $dom->createCDATASection(" Customer requests that pizza be sliced into 16 square pieces ");
$root->appendChild($cdata);

// create PI
$pi = $dom->createProcessingInstruction("pizza", "bake()");
$root->appendChild($pi);

// save and display tree
echo $dom->saveXML();
?>

输出如下所示:

<?xml version="1.0"?>
<toppings>
<item price="4">pepperoni</item>
<![CDATA[
      Customer requests that pizza be sliced into 16 square pieces
]]>
<?pizza bake()?>
</toppings>

保存结果

一旦已经实现你的目标,就可以将结果保存在一个文件或存储于PHP的变量。通过调用带有文件名的save()方法可以将结果保存在文件中,而通过调用saveXML()方法可存储于PHP的变量。请参考以下实例(程序清单F):

程序清单 F

<?php
// create doctype
$dom = new DOMDocument("1.0");

// create root element
$root = $dom->createElement("toppings");
$dom->appendChild($root);
$dom->formatOutput=true;

// create child element
$item = $dom->createElement("item");
$root->appendChild($item);

// create text node
$text = $dom->createTextNode("pepperoni");
$item->appendChild($text);

// create attribute node
$price = $dom->createAttribute("price");
$item->appendChild($price);

// create attribute value node
$priceValue = $dom->createTextNode("4");
$price->appendChild($priceValue);

// create CDATA section
$cdata = $dom->createCDATASection(" Customer requests that pizza be

sliced into 16 square pieces ");
$root->appendChild($cdata);

// create PI
$pi = $dom->createProcessingInstruction("pizza", "bake()");
$root->appendChild($pi);

// save tree to file
$dom->save("order.xml");

// save tree to string
$order = $dom->save("order.xml");
?>

希望你能在本文发现有用的东西,并在以后使用XML时应用到这些方法。

用 PHP 读取和编写 XML DOM

Jack Herrington (), 高级软件工程师, "Code Generation Network"

2006 年 2 月 06 日

有许多技术可用于用 PHP 读取和编写 XML。本文提供了三种方法读取 XML:使用 DOM 库、使用 SAX 解析器和使用正则表达式。还介绍了使用 DOM 和 PHP 文本模板编写 XML。

用 PHP 读取和编写可扩展标记语言(XML)看起来可能有点恐怖。实际上,XML 和它的所有相关技术可能是恐怖的,但是用 PHP 读取和编写 XML 不一定是项恐怖的任务。首先,需要学习一点关于 XML 的知识 —— 它是什么,用它做什么。然后,需要学习如何用 PHP 读取和编写 XML,而有许多种方式可以做这件事。

本文提供了 XML 的简短入门,然后解释如何用 PHP 读取和编写 XML。

什么是 XML?

XML 是一种数据存储格式。它没有定义保存什么数据,也没有定义数据的格式。XML 只是定义了标记和这些标记的属性。格式良好的 XML 标记看起来像这样:

<name>Jack Herrington</name>

这个 <name> 标记包含一些文本:Jack Herrington。

不包含文本的 XML 标记看起来像这样:

<powerUp />

用 XML 对某件事进行编写的方式不止一种。例如,这个标记形成的输出与前一个标记相同:

<powerUp></powerUp>

也可以向 XML 标记添加属性。例如,这个 <name> 标记包含 firstlast 属性:

<name first="Jack" last="Herrington" />

也可以用 XML 对特殊字符进行编码。例如,& 符号可以像这样编码:

&

包含标记和属性的 XML 文件如果像示例一样格式化,就是格式良好的,这意味着标记是对称的,字符的编码正确。清单 1 是一份格式良好的 XML 的示例。

清单 1. XML 图书列表示例
<books>
<book>
<author>Jack Herrington</author>
<title>PHP Hacks</title>
<publisher>O’Reilly</publisher>
</book>
<book>
<author>Jack Herrington</author>
<title>Podcasting Hacks</title>
<publisher>O’Reilly</publisher>
</book>
</books>

清单 1 中的 XML 包含一个图书列表。父标记 <books> 包含一组 <book> 标记,每个 <book> 标记又包含 <author><title><publisher> 标记。

当 XML 文档的标记结构和内容得到外部模式文件的验证后,XML 文档就是正确的。模式文件可以用不同的格式指定。对于本文来说,所需要的只是格式良好的 XML。

如果觉得 XML 看起来很像超文本标记语言(HTML),那么就对了。XML 和 HTML 都是基于标记的语言,它们有许多相似之处。但是,要着重指出的是:虽然 XML 文档可能是格式良好的 HTML,但不是所有的 HTML 文档都是格式良好的 XML。换行标记(br)是 XML 和 HTML 之间区别的一个好例子。这个换行标记是格式良好的 HTML,但不是格式良好的 XML:

<p>This is a paragraph<br>
With a line break</p>

这个换行标记是格式良好的 XML 和 HTML:

<p>This is a paragraph<br />
With a line break</p>

如果要把 HTML 编写成同样是格式良好的 XML,请遵循 W3C 委员会的可扩展超文本标记语言(XHTML)标准(参见 参考资料)。所有现代的浏览器都能呈现 XHTML。而且,还可以用 XML 工具读取 XHTML 并找出文档中的数据,这比解析 HTML 容易得多。



回页首

使用 DOM 库读取 XML

读取格式良好的 XML 文件最容易的方式是使用编译成某些 PHP 安装的文档对象模型 (DOM)库。DOM 库把整个 XML 文档读入内存,并用节点树表示它,如图 1 所示。

图 1. 图书 XML 的 XML DOM 树
图书 XML 的 XML DOM 树

树顶部的 books 节点有两个 book 子标记。在每本书中,有 authorpublishertitle 几个节点。authorpublishertitle 节点分别有包含文本的文本子节点。

读取图书 XML 文件并用 DOM 显示内容的代码如清单 2 所示。

清单 2. 用 DOM 读取图书 XML
<?php
$doc = new DOMDocument();
$doc->load( ‘books.xml’ );

$books = $doc->getElementsByTagName( "book" );
foreach( $books as $book )
{
$authors = $book->getElementsByTagName( "author" );
$author = $authors->item(0)->nodeValue;

$publishers = $book->getElementsByTagName( "publisher" );
$publisher = $publishers->item(0)->nodeValue;

$titles = $book->getElementsByTagName( "title" );
$title = $titles->item(0)->nodeValue;

echo "$title – $author – $publishern";
}
?>

脚本首先创建一个 new DOMdocument 对象,用 load 方法把图书 XML 装入这个对象。之后,脚本用 getElementsByName 方法得到指定名称下的所有元素的列表。

book 节点的循环中,脚本用 getElementsByName 方法获得 authorpublishertitle 标记的 nodeValuenodeValue 是节点中的文本。脚本然后显示这些值。

可以在命令行上像这样运行 PHP 脚本:

% php e1.php
PHP Hacks - Jack Herrington - O'Reilly
Podcasting Hacks - Jack Herrington - O'Reilly
%

可以看到,每个图书块输出一行。这是一个良好的开始。但是,如果不能访问 XML DOM 库该怎么办?



回页首

用 SAX 解析器读取 XML

读取 XML 的另一种方法是使用 XML Simple API(SAX)解析器。PHP 的大多数安装都包含 SAX 解析器。SAX 解析器运行在回调模型上。每次打开或关闭一个标记时,或者每次解析器看到文本时,就用节点或文本的信息回调用户定义的函数。

SAX 解析器的优点是,它是真正轻量级的。解析器不会在内存中长期保持内容,所以可以用于非常巨大的文件。缺点是编写 SAX 解析器回调是件非常麻烦的事。清单 3 显示了使用 SAX 读取图书 XML 文件并显示内容的代码。

清单 3. 用 SAX 解析器读取图书 XML
<?php
$g_books = array();
$g_elem = null;

function startElement( $parser, $name, $attrs )
{
global $g_books, $g_elem;
if ( $name == ‘BOOK’ ) $g_books []= array();
$g_elem = $name;
}

function endElement( $parser, $name )
{
global $g_elem;
$g_elem = null;
}

function textData( $parser, $text )
{
global $g_books, $g_elem;
if ( $g_elem == ‘AUTHOR’ ||
$g_elem == ‘PUBLISHER’ ||
$g_elem == ‘TITLE’ )
{
$g_books[ count( $g_books ) – 1 ][ $g_elem ] = $text;
}
}

$parser = xml_parser_create();

xml_set_element_handler( $parser, "startElement", "endElement" );
xml_set_character_data_handler( $parser, "textData" );

$f = fopen( ‘books.xml’, ‘r’ );

while( $data = fread( $f, 4096 ) )
{
xml_parse( $parser, $data );
}

xml_parser_free( $parser );

foreach( $g_books as $book )
{
echo $book[‘TITLE’]." – ".$book[‘AUTHOR’]." – ";
echo $book[‘PUBLISHER’]."n";
}
?>

脚本首先设置 g_books 数组,它在内存中容纳所有图书和图书信息,g_elem 变量保存脚本目前正在处理的标记的名称。然后脚本定义回调函数。在这个示例中,回调函数是 startElementendElementtextData。在打开和关闭标记的时候,分别调用 startElementendElement 函数。在开始和结束标记之间的文本上面,调用 textData

在这个示例中,startElement 标记查找 book 标记,在 book 数组中开始一个新元素。然后,textData 函数查看当前元素,看它是不是 publishertitleauthor 标记。如果是,函数就把当前文本放入当前图书。

为了让解析继续,脚本用 xml_parser_create 函数创建解析器。然后,设置回调句柄。之后,脚本读取文件并把文件的大块内容发送到解析器。在文件读取之后,xml_parser_free 函数删除解析器。脚本的末尾输出 g_books 数组的内容。

可以看到,这比编写 DOM 的同样功能要困难得多。如果没有 DOM 库也没有 SAX 库该怎么办?还有替代方案么?



回页首

用正则表达式解析 XML

可以肯定,即使提到这个方法,有些工程师也会批评我,但是确实可以用正则表达式解析 XML。清单 4 显示了使用 preg_ 函数读取图书文件的示例。

清单 4. 用正则表达式读取 XML
<?php
$xml = "";
$f = fopen( ‘books.xml’, ‘r’ );
while( $data = fread( $f, 4096 ) ) { $xml .= $data; }
fclose( $f );

preg_match_all( "/<book>(.*?)</book>/s",
$xml, $bookblocks );

foreach( $bookblocks[1] as $block )
{
preg_match_all( "/<author>(.*?)</author>/",
$block, $author );
preg_match_all( "/<title>(.*?)</title>/",
$block, $title );
preg_match_all( "/<publisher>(.*?)</publisher>/",
$block, $publisher );
echo( $title[1][0]." – ".$author[1][0]." – ".
$publisher[1][0]."n" );
}
?>

请注意这个代码有多短。开始时,它把文件读进一个大的字符串。然后用一个 regex 函数读取每个图书项目。最后用 foreach 循环,在每个图书块间循环,并提取出 author、title 和 publisher。

那么,缺陷在哪呢?使用正则表达式代码读取 XML 的问题是,它并没先进行检查,确保 XML 的格式良好。这意味着在读取之前,无法知道 XML 是否格式良好。而且,有些格式正确的 XML 可能与正则表达式不匹配,所以日后必须修改它们。

我从不建议使用正则表达式读取 XML,但是有时它是兼容性最好的方式,因为正则表达式函数总是可用的。不要用正则表达式读取直接来自用户的 XML,因为无法控制这类 XML 的格式或结构。应当一直用 DOM 库或 SAX 解析器读取来自用户的 XML。



回页首

用 DOM 编写 XML

读取 XML 只是公式的一部分。该怎样编写 XML 呢?编写 XML 最好的方式就是用 DOM。清单 5 显示了 DOM 构建图书 XML 文件的方式。

清单 5. 用 DOM 编写图书 XML
<?php
$books = array();
$books [] = array(
‘title’ => ‘PHP Hacks’,
‘author’ => ‘Jack Herrington’,
‘publisher’ => "O’Reilly"
);
$books [] = array(
‘title’ => ‘Podcasting Hacks’,
‘author’ => ‘Jack Herrington’,
‘publisher’ => "O’Reilly"
);

$doc = new DOMDocument();
$doc->formatOutput = true;

$r = $doc->createElement( "books" );
$doc->appendChild( $r );

foreach( $books as $book )
{
$b = $doc->createElement( "book" );

$author = $doc->createElement( "author" );
$author->appendChild(
$doc->createTextNode( $book[‘author’] )
);
$b->appendChild( $author );

$title = $doc->createElement( "title" );
$title->appendChild(
$doc->createTextNode( $book[‘title’] )
);
$b->appendChild( $title );

$publisher = $doc->createElement( "publisher" );
$publisher->appendChild(
$doc->createTextNode( $book[‘publisher’] )
);
$b->appendChild( $publisher );

$r->appendChild( $b );
}

echo $doc->saveXML();
?>

在脚本的顶部,用一些示例图书装入了 books 数组。这个数据可以来自用户也可以来自数据库。

示例图书装入之后,脚本创建一个 new DOMDocument,并把根节点 books 添加到它。然后脚本为每本书的 author、title 和 publisher 创建节点,并为每个节点添加文本节点。每个 book 节点的最后一步是重新把它添加到根节点 books

脚本的末尾用 saveXML 方法把 XML 输出到控制台。(也可以用 save 方法创建一个 XML 文件。)脚本的输出如清单 6 所示。

清单 6. DOM 构建脚本的输出
% php e4.php
<?xml version="1.0"?>
<books>
<book>
<author>Jack Herrington</author>
<title>PHP Hacks</title>
<publisher>O’Reilly</publisher>
</book>
<book>
<author>Jack Herrington</author>
<title>Podcasting Hacks</title>
<publisher>O’Reilly</publisher>
</book>
</books>
%

使用 DOM 的真正价值在于它创建的 XML 总是格式正确的。但是如果不能用 DOM 创建 XML 时该怎么办?



回页首

用 PHP 编写 XML

如果 DOM 不可用,可以用 PHP 的文本模板编写 XML。清单 7 显示了 PHP 如何构建图书 XML 文件。

清单 7. 用 PHP 编写图书 XML
<?php
$books = array();
$books [] = array(
‘title’ => ‘PHP Hacks’,
‘author’ => ‘Jack Herrington’,
‘publisher’ => "O’Reilly"
);
$books [] = array(
‘title’ => ‘Podcasting Hacks’,
‘author’ => ‘Jack Herrington’,
‘publisher’ => "O’Reilly"
);
?>
<books>
<?php

foreach( $books as $book )
{
?>
<book>
<title><?php echo( $book[‘title’] ); ?></title>
<author><?php echo( $book[‘author’] ); ?>
</author>
<publisher><?php echo( $book[‘publisher’] ); ?>
</publisher>
</book>
<?php
}
?>
</books>

脚本的顶部与 DOM 脚本类似。脚本的底部打开 books 标记,然后在每个图书中迭代,创建 book 标记和所有的内部 titleauthorpublisher 标记。

这种方法的问题是对实体进行编码。为了确保实体编码正确,必须在每个项目上调用 htmlentities 函数,如清单 8 所示。

清单 8. 使用 htmlentities 函数对实体编码

<books>
<?php

foreach( $books as $book )
{
$title = htmlentities( $book[‘title’], ENT_QUOTES );
$author = htmlentities( $book[‘author’], ENT_QUOTES );
$publisher = htmlentities( $book[‘publisher’], ENT_QUOTES );
?>
<book>
<title><?php echo( $title ); ?></title>
<author><?php echo( $author ); ?> </author>
<publisher><?php echo( $publisher ); ?>
</publisher>
</book>
<?php
}
?>
</books>

这就是用基本的 PHP 编写 XML 的烦人之处。您以为自己创建了完美的 XML,但是在试图使用数据的时候,马上就会发现某些元素的编码不正确。



回页首

结束语

XML 周围总有许多夸大之处和混淆之处。但是,并不像您想像的那么难 —— 特别是在 PHP 这样优秀的语言中。在理解并正确地实现了 XML 之后,就会发现有许多强大的工具可以使用。XPath 和 XSLT 就是这样两个值得研究的工具。

参考资料

学习

获得产品和技术

  • 请访问 PHP.net,了解关于 PHP 的最新新闻、找到下载,并向其他用户学习。
  • 了解 Expat XML Parser,这个解析器用来向 PHP 提供 SAX 解析器功能。
  • 利用 IBM 试用软件 改造您的下一个开放源码开发项目,可以下载也可以通过 DVD 得到。

讨论

关于作者

Jack D. Herrington 是有 20 多年经验的高级软件工程师。他是三本书的作者:Code Generation in ActionPodcasting Hacks 和即将发表的 PHP Hacks。他还撰写了 30 多篇文章。