分类:
2008-05-27 22:02:36
本文节选于《Programming PHP》第二版(中译名《PHP程序设计》第二版,Haohappy等译,电子工业出版社出版)
11.3 解析XML
Parsing XML
假设有一些用XML写成的书, 你想要建立一个书籍标题和作者的索引。你需要解析XML文件,识别出title元素和author元素及它们的内容。你可以用正则表达式和字符串函数(如 strtok())完成这个工作,但是有点复杂。最简单快捷的方法是使用PHP自带的XML解析器。
PHP有3个XML解析器——一个基于Expat C库的事件驱动型解析器;一个基于DOM的解析器和一个适合于解析简单XML文件的解析器,即SimpleXML。
最常用的解析器是基于事件的,可以在解析时不要求验证XML文档。也就是说你可以找出当前的XML标签及其包含的内容,但是却不能确定它们是否为正确文档结构中的正确标签。不过在实际中,通常这不是个大问题。
PHP的XML解析器是事件基于事件的,这意味着当解析器阅读文档时,它将针对各种事件调用不同的处理程序函数,如一个元素的开始和结束。
下面几节我们将讨论你可以提供的处理程序(handler),以及设置这些handler的函数和触发调用handler的事件。我们也提供了一个样本函数,用来在内存中生成XML文档图,该函数带有一个示例程序,可以很漂亮地输出XML。
11.3.1 元素处理器(Element Handler)
Element Handlers
当解析器遇到一个元素的开始标签或结束标签,它会调用起始或结束元素处理器(处理器即一个处理程序)。我们可通过xml_set_element_handler( )函数来设置该处理器:
xml_set_element_handler(parser, start_element, end_element);
start_element和end_element这两个参数是处理器函数的名称。
当XML解析器遇到元素的起始标签时,调用起始元素处理器。
my_start_element_handler(parser, element, attributes);
它传递三个参数:调用处理程序的XML解析器的引用、起始元素的名称和解析器遇到的元素的属性数组。考虑到速度,该属性数组也是按引用传递的。
示例11-2包含了起始元素处理器的代码,该处理程序只是简单地将元素名加粗输出,将属性用灰色输出。
示例11-2:起始元素处理程序
function start_element($inParser, $inName, &$inAttributes) {
$attributes = array( );
foreach($inAttributes as $key) {
$value = $inAttributes[$key];
$attributes[] = "$key=\"$value\" ";
}
echo '<' . $inName . ' ' . join(' ', $attributes) . '>';
}
当解析器到达元素结束标签时,就会调用结束元素处理器:
my_end_element_handler(parser, element);
它有两个参数:一个调用处理程序的XML解析器的引用和将结束的元素的名称。
示例11-3显示了一个格式化该元素的结束处理器:
示例11-3:结束元素处理器
function end_element($inParser, $inName) {
echo '</$inName>';
}
11.3.2 字符数据处理器(Character Data Handler)
Character Data Handler
元素之间的所有文本(如字符数据、XML术语中的CDATA)由字符数据处理程序处理。通过xml_set_character_data_handler( )函数设置的处理器会在遇到每一个字符数据块时被调用。
xml_set_character_data_handler(parser, handler);
字符数据处理器有两个参数:触发处理程序的XML解析器的引用、包含字符数据的字符串:
my_character_data_handler(parser, cdata);
示例11-4显示了一个简单的输出数据的字符数据处理器:
示例11-4:字符数据处理器
function character_data($inParser, $inData) {
echo $inData;
}
11.3.3 处理指令
Processing Instructions
在XML中处理指令(PI, Processing Instruction)用来将脚本或其他代码嵌入到文档中,PHP代码本身就可被看成是一种遵循XML格式的处理指令,它以标签风格标记代码。XML解析器在遇到处理指令时即调用相应的处理指令处理器。可以用xml_set_ processing_instruction_handler( )函数设置处理器:
xml_set_processing_instruction_handler (parser, handler);
处理指令的格式如下:
处理指令处理器(processing instruction handler)有3个参数:触发处理器的解析器的引用、目标名称(如“php”)和处理指令。
my_processing_instruction_handler(parser, target, instructions);
对处理指令做什么由你决定。一个技巧就是将PHP代码嵌入到XML文档中。当解析文档时,用eval()函数执行PHP代 码。示例11-5就是这样做的。当然你必须依赖正在处理的包含eval()的文档,因为eval()函数将运用所包含的任何代码――即使该代码会删除文件 或者把密码发送给黑客。
示例11-5:处理指令处理器
function processing_instruction($inParser, $inTarget, $inCode) {
if ($inTarget === 'php') {
eval($inCode);
}
}
11.3.4 实体处理器
Entity Handlers
XML中的实体是占位符(placeholder)。XML提供了5种标准实体(&, >, <, "和'),并且XML文档还可以定义它们自己的实体。大多数实体定义不会触发事件,在调用其他处理器前,XML解析 器会对文档中的大多数实体进行扩展。
PHP的XML库对外部实体(external entity)和未解析实体(unparsed entity)提供特别支持。外部实体的替换文本(replacement text)是由文件名或者URL表示的,而不是在XML文件中显式地给定的。你可以定义一个处理器,并在字符数据中出现外部实体时调用。但是否解析该文件 或URL的内容,则由用户自身决定。
未解析实体必须带有一个符号声明。你可以定义未解析实体和符号声明,则未解析实体将在调用字符数据处理器前被删除。
11.3.4.1 外部实体
外部实体允许XML文档包含其他XML文档。一个外部实体引用的处理器打开被引用的文件,然后解析该文件并且将结果包含在当前文档中。可用xml_set_external_entity_ ref_handler( )函数来设置处理器,该函数有两个参数:XML解析器的引用 和处理器函数的名称:
xml_set_external_entity_ref_handler(parser, handler);
外部实体处理器有5个参数:触发处理器的XML解析器、实体名称、分解实体标识符的基本URL(当前为空)、系统标识符(如文件名)和实体的公共标识符,实体声明如下:
$ok = my_ext_entity_handler(parser, entity, base, system, public);
如果外部实体处理器返回一个false值(或没有返回任何值),XML解析器将停止并报告一个XML_ERROR_EXTERNAL_ENTITY_HANDLING错误。如果返回true,则解析继续。
示例11-6介绍了如何解析外部引用的XML文档。我们定义了两个函数create_parser( ) 和parse( ),来创建XML解析器和进行解析工作。你可以用它们来解析顶层文档和由外部引用包含的任何文档。这样的函数将在后面的“使用解析器”一节中详细介绍。外 部实体引用处理器只是简单地确定哪些文件要传递给这些函数。
示例11-6:外部实体引用处理器
function external_entity_reference($inParser, $inNames, $inBase,
$inSystemID, $inPublicID) {
if($inSystemID) {
if(!list($parser, $fp) = create_parser($inSystemID)) {
echo "Error opening external entity $inSystemID \n";
return false;
}
return parse($parser, $fp);
}
return false;
}
11.3.4.2 未解析实体
未解析实体的声明要带有一个符号声明:
]>
使用xml_set_notation_decl_handler( )函数可注册一个符号声明处理器:
xml_set_notation_decl_handler(parser, handler);
调用处理器时要带有5个参数:
my_notation_handler(parser, notation, base, system, public);
Base参数是分解符号标识符的基本URL,可以设置system标识符或public标识符,但是不能两者同时设置。
使用xml_set_unparsed_entity_decl_handler( )函数可以注册一个未解析实体声明处理器:
xml_set_unparsed_entity_decl_handler(parser, handler);
这个处理器被调用时带有6个参数:
my_unp_entity_handler(parser, entity, base, system, public, notation);
notation参数确定该未解析实体关联的符号声明。
11.3.5 默认处理器
Default Handler
对任何其他事件,如XML声明和XML文档类型,将调用默认处理器(Default Handler)。可调用xml_set_default_handler( )函数来设置默认器:
xml_set_default_handler(parser, handler);
该处理器有两个参数:
my_default_handler(parser, text);
Text参数根据触发默认处理器的事件种类不同而具有不同的值。示例 11-7仅输出调用默认处理程序时的给定字符串。
示例11-7:Default handler
function default($inParser, $inData) {
echo "XML: Default handler called with '$inData'\n";
}
11.3.6 选项
Options
XML解析器有几个选项可用来控制源文档编码、目标文档编码和大小写形式。使用xml_parser_set_option( )可以设置这些选项:
xml_parser_set_option(parser, option, value);
类似地,使用xml_parser_get_option( )可得到XML解析器的选项设置信息:
$value = xml_parser_get_option(parser, option);
11.3.6.1 字符编码
PHP使用的XML解析器支持各种不同字符编码的Unicode数据。在内部,PHP字符串通常用UTF-8编码,但是XML解析器解析的文档可以是ISO-8859-1、US-ASCII或UTF-8。UTF-16目前还不支持。
在创建XML解析器时,你可以指定要解析的文件的编码。如果省略了,则假定源文件是按ISO-8859-1编码的。如果源文件中的字符超出了其编码范围,XML解析器将返回一个
错误并立即停止处理文档。
解析器的目标编码是XML解析器将数据传递给处理器程序函数时的编码。通常,它与源文件的编码一致,在XML解析器生存期的任何时候,目标编码都可以被改变。任何超出编码范围的字符都被问号(?)所取代。
使用常量XML_OPTION_TARGET_ENCODING可获得或者设置传递给回调函数的文本编码,其允许值为“ISO-8859-1”(默认值)、“US-ASCII”和“UTF-8”。
11.3.6.2 大小写
默认情况下,XML文档中的所有元素和属性名称都会被转换成大写形式。你可通过将XML_OPTION_CASE_FOLDING选项设为false来禁止这种转换(即使用区分大小写的名称)。
xml_parser_set_option(XML_OPTION_CASE_FOLDING, false);
11.3.7 使用解析器
Using the Parser
要使用XML解析器,可以先用xml_parser_create( )函数创建一个解析器,然后设置该解析器的处理器和选项,再用xml_parse( )函数将数据传递给解析器处理,直到数据处理完毕或者解析器返回一个错误。一旦处理完毕,调用xml_parser_free( )函数释放解析器。
xml_parser_create( )函数返回一个XML解析器:
$parser = xml_parser_create([encoding]);
可选的encoding参数指定将要解析的文本的编码(ISO-8859-1、US-ASCII或UTF-8)。
xml_parse( )函数如果解析成功会返回TRUE,失败则返回FALSE。
$success = xml_parse(parser, data[, final ]);
Data参数是要处理的XML字符串,为了使最后一块数据被解析,可选的final参数应设为true。
为了使处理嵌套文档变得简单,可以编写一个函数来创建解析器并设置其选项和处理器。该函数将选项和处理器设置放在同一个地方,而不是在外部实体引用的处理器中复制它们。 示例11-8 包含一个这样的函数。
示例11-8:创建解析器
function create_parser ($filename) {
$fp = fopen('filename', 'r');
$parser = xml_parser_create( );
xml_set_element_handler($parser, 'start_element', 'end_element');
xml_set_character_data_handler($parser, 'character_data');
xml_set_processing_instruction_handler($parser, 'processing_instruction');
xml_set_default_handler($parser, 'default');
return array($parser, $fp);
}
function parse ($parser, $fp) {
$blockSize = 4 * 1024; // read in 4 KB chunks
while($data = fread($fp, $blockSize)) { // read in 4 KB chunks
if(!xml_parse($parser, $data, feof($fp))) {
// an error occurred; tell the user where
echo 'Parse error: ' . xml_error_string($parser) . " at line " .
xml_get_current_line_number($parser));
return FALSE;
}
}
return TRUE;
}
if (list($parser, $fp) = create_parser('test.xml')) {
parse($parser, $fp);
fclose($fp);
xml_parser_free($parser);
}
11.3.8 错误
Errors
如果解析成功,xml_parse( )返回TRUE,否则返回FALSE。如果出错,可用xml_get_ error_code( )来获取该错误的标识性代码。
$err = xml_get_error_code( );
错误代码可能是以下之一:
XML_ERROR_NONE
XML_ERROR_NO_MEMORY
XML_ERROR_SYNTAX
XML_ERROR_NO_ELEMENTS
XML_ERROR_INVALID_TOKEN
XML_ERROR_UNCLOSED_TOKEN
XML_ERROR_PARTIAL_CHAR
XML_ERROR_TAG_MISMATCH
XML_ERROR_DUPLICATE_ATTRIBUTE
XML_ERROR_JUNK_AFTER_DOC_ELEMENT
XML_ERROR_PARAM_ENTITY_REF
XML_ERROR_UNDEFINED_ENTITY
XML_ERROR_RECURSIVE_ENTITY_REF
XML_ERROR_ASYNC_ENTITY
XML_ERROR_BAD_CHAR_REF
XML_ERROR_BINARY_ENTITY_REF
XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF
XML_ERROR_MISPLACED_XML_PI
XML_ERROR_UNKNOWN_ENCODING
XML_ERROR_INCORRECT_ENCODING
XML_ERROR_UNCLOSED_CDATA_SECTION
XML_ERROR_EXTERNAL_ENTITY_HANDLING
这些常量通常不是很有用。使用xml_error_string( )可以将错误代码转换成字符串,这样就可以在错误中直接使用了。
$message = xml_error_string(code);
例如:
$err = xml_get_error_code($parser);
if ($err != XML_ERROR_NONE) die(xml_error_string($err));
11.3.9 作为处理器的方法
Methods as Handlers
因为PHP中的函数和变量是全局性的,因此任何需要几个函数和变量的应用程序的组件都适合用面向对象设计的风格来编写。 XML解析通常会要求用变量记录解析过程中的位置(如“刚看见一个title元素的开始标签,因此要跟踪字符数据直到见到其结束标签”),并且显然必须编 写几个处理器函数来操作该状态,并做一些实际工作。把这些函数和变量封装到一个类中,我们就可以把它们和程序中的其他部分分离开来,并且很容易可以重用这 些代码。
使用xml_set_object( )函数可为解析器注册一个对象,然后XML解析器将在对象中寻找作为方法(method)而不是全局函数(global function)的处理器:
xml_set_object(object);
11.3.10 解析示例程序
Sample Parsing Application
现在我们编写一个程序来解析XML文件并显示其中不同类型的信息。示例 11-9中的XML文件包含了一组书籍信息:
示例11-9:books.xml文件
PHP as to Perl
这个PHP程序解析该文件并显示一个书目列表(只包括书的标题和作者),其内容如图11-1所示。每个书籍标题链接到一个页面,该页面包含书的完整信息。《Programming PHP》的细节信息如图11-2所示。
我们定义了一个类BookList,它的构造函数解析XML文件并建立一个记录列表。BookList有两个类方法来生成这些记录的输出:show_menu( )方法生成书籍列表,show_book( )方法显示某本特定的书的细节信息。
解析文件涉及到跟踪记录,如遇到哪一个元素,哪些元素对应于记录(book)和字段(title、author、isbn和 comment)。$record属性保存正在构建的当前记录,$current_field保存当前处理的字段名称(如title)。$records 属性则是当前我们已经读取的记录的集合数组。
图11-1:书籍列表
图11-2:书籍细节
两个关联数组$ field_type和$ends_record告诉我们哪些元素与记录中的字段相对应和哪个结束元素标志着记录的结束。$field_type的值是1 或2,分别对应于一个简单的字段(如title)或者一个值数组(如author)。这些数组在构造函数中被初始化。
处理器函数本身是很简单的。当 遇到一个元素的开始标签,首先判断它是否符合我们感兴趣的字段,如果是,则将current_field属性设为该字段名称,以使遇到其他字符数据(如该 书的标题)时知道要存入哪个字段。在获得字符数据后,我们就将其增添到current_field指定的当前记录的相应字段中。遇到元素的结束标签时,我 们检查它是否为记录的结束——如果是,则将当前记录添加到已完成记录的数组中。
示例11-10给出了一段PHP脚本,它可以同时处理书籍列表和书籍细节两个页面(译注1)。书籍列表中的每一项都链接到当前页的URL上,URL带有GET参数来确定要显示细节的书的ISBN号。
示例11-10:bookparse.php
class BookList {
var $parser;
var $record;
var $current_field = '';
var $field_type;
var $ends_record;
var $records;
function BookList ($filename) {
$this->parser = xml_parser_create( );
xml_set_object($this->parser, &$this);
xml_set_element_handler($this->parser, 'start_element', 'end_element');
xml_set_character_data_handler($this->parser, 'cdata');
// 1 = single field, 2 = array field, 3 = record container
$this->field_type = array('title' => 1,
'author' => 2,
'isbn' => 1,
'comment' => 1);
$this->ends_record = array('book' => true);
$x = join("", file($filename));
xml_parse($this->parser, $x);
xml_parser_free($this->parser);
}
function start_element ($p, $element, &$attributes) {
$element = strtolower($element);
if ($this->field_type[$element] != 0) {
$this->current_field = $element;
} else {
$this->current_field = '';
}
}
function end_element ($p, $element) {
$element = strtolower($element);
if ($this->ends_record[$element]) {
$this->records[] = $this->record;
$this->record = array( );
}
$this->current_field = '';
}
function cdata ($p, $text) {
if ($this->field_type[$this->current_field] === 2) {
$this->record[$this->current_field][] = $text;
} elseif ($this->field_type[$this->current_field] === 1) {
$this->record[$this->current_field] .= $text;
}
}
function show_menu( ) {
echo "
%s | %s |
---|