Apache Xerces-Perl 评估与替代方案指南
以源码与发行包盘点为依据评估 Apache Xerces-Perl(XML::Xerces 2.7.0-0)的现状、组成与 API 面貌,并实测 XML::LibXML 在 Ubuntu 22.04 上的解析、XPath、DOM 创建、DTD 与 XSD 校验能力,给出可落地的迁移建议。
Apache Xerces-Perl 评估与替代方案指南
目标:回答两个问题。其一,Apache Xerces-Perl(CPAN 发行名 XML::Xerces)这个 2006 年后停更的 Perl 绑定项目,现状如何、能力边界在哪里——本文以发行包源码实测盘点为准,不凭转述;其二,如果今天的 Ubuntu 22.04 服务器需要 XML 解析、DOM 操作与 DTD/XML Schema 校验能力,用什么替代、迁移成本有多大——本文用 XML::LibXML 的逐项实测作答。安装环节的完整失败实录见《Apache Xerces-Perl 在 Ubuntu 22.04 安装实录》,本文不重复编译细节,只引用结论。
一、环境信息
本文评估与对照实测均在 Ubuntu 22.04 服务器上以 root 用户在 /root 下执行。基础环境与《Apache Xerces-Perl 在 Ubuntu 22.04 安装实录》相同:perl 5.34.0(thread-multi)、gcc 11.4.0、SWIG 4.0.2、apt 版 libxerces-c-dev 3.2.3。评估对象为发行包源码目录 /root/XML-Xerces-2.7.0-0(从 Apache 官方归档下载,md5 实测 e5bd871026bee1b291c5634e6e4daf35);对照方案为 apt 仓库的 XML::LibXML。
二、项目现状评估
2.1 版本时间线:一次发布即终点
Apache 官方归档 xerces/p 目录的完整清单实测:
$ curl -sS https://archive.apache.org/dist/xerces/p/ | grep -oE 'XML-Xerces[^"<]*' | sort -u
XML-Xerces-2.7.0-0.tar.gz
XML-Xerces-2.7.0-0.tar.gz.asc
发行包自带的 Changes 文件给出了完整版本线(head -30 节选):
Revision history for Perl extension XML::Xerces.
2.7.0-0 Sun Jan 1 08:44:27 IST 2006
- first public 2.7 release
- working with SWIG-1.3.28 pre-release
- removed all uses of XMLString::transcode() from the Perl
callback handler classes in Handler/
2.6.0-2
- first public 2.6 release
- all known memory leaks fixed
...
时间线很清晰:2003 年 2.3 系列频繁修复,2006 年 1 月 1 日发布 2.7.0-0,此后再无任何版本。归档只有 2.7.0-0 一个发行物,说明 2.7.0-0 既是最终版也是终点。
2.2 CPAN 可得性实测
CPAN 上的发行物与归档一致,cpanm 实测能取到并识别版本,但配置阶段失败(完整实录见《Apache Xerces-Perl 在 Ubuntu 22.04 安装实录》第五节):
# cpanm -v XML::Xerces
You have not defined any of the following environment variables:
XERCESCROOT
XERCES_LIB
XERCES_INCLUDE
...
Couldn't find XercesVersion.hpp in your include directory at Makefile.PL line 188.
! Configure failed for XML-Xerces-2.7.0-0. See /root/.cpanm/work/1792134263.10400/build.log for details.
发行物仍在、版本号 2.7.0-0 与归档一致,但配置阶段即失败,CPAN 通道实际不可用。
2.3 现代环境可用性结论
Ubuntu 22.04(Perl 5.34 + gcc 11)上的完整安装实测结论:无法构建出可用的 Xerces.so。三道独立断点依次为:预生成胶水层与 Perl 5.34 引导 ABI 不兼容(too many initializers for 'swig_command_info')、SWIG 4.0.2 无法解析 1.3 时代接口文件(Unknown SWIG preprocessor directive: $args)、SWIG 1.3.40 重新生成后又暴露类型描述符命名漂移(SWIGTYPE_p_XERCES_CPP_NAMESPACE__XMLException was not declared)。每条报错都经实测复现,细节见《Apache Xerces-Perl 在 Ubuntu 22.04 安装实录》第七、八节。评估定性:停更 20 年,现代环境不可用,应作为只读考古对象而非技术选型候选。
三、项目组成与代码体量盘点
解压发行包直接实测(ls -la 节选、时间为发行包内文件原始时间戳):
$ cd /root/XML-Xerces-2.7.0-0 && ls -la
total 2784
drwxr-xr-x 10 1000 1000 4096 Mar 14 2006 .
-rw-r--r-- 1 1000 1000 11572 Jan 1 2006 Changes
-rw-r--r-- 1 1000 1000 2802 Dec 20 2005 FAQ
-rw-r--r-- 1 1000 1000 1053 Dec 20 2005 INSTALL
-rw-r--r-- 1 1000 1000 9939 Mar 10 2006 Makefile.PL
-rw-r--r-- 1 1000 1000 13835 Dec 20 2005 README
-rw-r--r-- 1 1000 1000 2382945 Mar 13 2006 Xerces.cpp
-rw-r--r-- 1 1000 1000 4581 Mar 13 2006 Xerces.i
-rw-r--r-- 1 1000 1000 244206 Mar 13 2006 Xerces.pm
drwxr-xr-x 3 1000 1000 4096 Mar 14 2006 interface
drwxr-xr-x 2 1000 1000 4096 Mar 14 2006 samples
drwxr-xr-x 2 1000 1000 4096 Mar 14 2006 scripts
drwxr-xr-x 2 1000 1000 4096 Mar 14 2006 t
...
核心代码体量实测(wc -l):
| 文件 | 行数 | 说明 |
|---|---|---|
| Xerces.cpp | 72531 | SWIG 预生成的 C++ 胶水层(2.38 MB) |
| Xerces.pm | 6823 | SWIG 生成的 Perl 侧主模块(244 KB) |
| Xerces-extra.pm | 951 | 手写的 Perl 附加层(对象生命周期管理等) |
| Xerces.i | 207 | SWIG 主接口文件 |
| 合计 | 80512 | 四个文件合计 |
💡 技术拓展:Xerces-extra.pm 是一个很能说明绑定维护成本的细节——SWIG 自动生成的 7 万行之外,还有近千行手写 Perl 用来"修胶水":主要做对象记忆与清理(%REMEMBER 表)、错误对象包装(导出 error 函数)。任何绑定项目都要在"自动生成"与"手写适配"之间反复拉扯,这部分手写代码正是 SWIG 版本升级时最容易失配的环节。
测试与样例规模实测:t/ 目录 45 个测试文件(DOMParser、SAX2XMLReader、Schema、Grammar、UTF8、XMLURL 等覆盖面相当完整);samples/ 目录 25 个文件,含 9 个 Perl 样例(DOMCount.pl、DOMCreate.pl、DOMPrint.pl、SAX2Count.pl、SAXCount.pl、SEnumVal.pl、validator.pl、validator-dom.pl、DOM2hash.pl)与配套样例数据(personal.xml/personal.dtd/personal.xsd 及四种损坏样例)。Handler/ 子目录是 Perl 回调处理器的 C++ 实现源码,编译为静态库后与胶水层链接。
四、API 面貌:从源码盘点
4.1 包装的 API 面:95 个 Xerces-C 头文件
接口文件没有直接声明类,而是通过 %include 引入 Xerces-C 头文件让 SWIG 整体包装。统计实际引入的头文件:
$ grep -h '%include "xercesc' interface/*.i | sort -u | wc -l
95
$ grep -h '%include "xercesc' interface/*.i | sort -u | head -14
%include "xercesc/dom/DOMAttr.hpp"
%include "xercesc/dom/DOMBuilder.hpp"
%include "xercesc/dom/DOMCDATASection.hpp"
%include "xercesc/dom/DOMCharacterData.hpp"
%include "xercesc/dom/DOMComment.hpp"
%include "xercesc/dom/DOMDocument.hpp"
%include "xercesc/dom/DOMDocumentFragment.hpp"
%include "xercesc/dom/DOMDocumentRange.hpp"
%include "xercesc/dom/DOMDocumentTraversal.hpp"
%include "xercesc/dom/DOMDocumentType.hpp"
%include "xercesc/dom/DOMElement.hpp"
%include "xercesc/dom/DOMEntity.hpp"
%include "xercesc/dom/DOMEntityReference.hpp"
%include "xercesc/dom/DOMEntityResolver.hpp"
95 个头文件的分布:DOM 层 35 个(DOM1/2/3 全套,含 DOMWriter/DOMBuilder)、framework 层 19 个(MemBufInputSource、LocalFileInputSource、Wrapper4DOMInputSource 等输入输出抽象)、parsers 层 3 个(XercesDOMParser、SAXParser、AbstractDOMParser)、SAX1 9 个、SAX2 6 个(SAX2XMLReader、DefaultHandler、LexicalHandler、DeclHandler 等)、util 层 6 个(PlatformUtils、XMLException、XMLURL 等)、DTD/Schema 校验器 17 个。能力面覆盖:DOM 2/3、SAX 1/2、命名空间、DTD 校验、W3C XML Schema 校验、PSVI 回调——与 README 的声明一致。
4.2 Perl 侧回调接口:命名空间型模块与处理器子类
发行包的样例展示了两种典型用法。其一,Xerces.i 用 %module "XML::Xerces" 直接声明命名空间型模块名,Perl 侧 use XML::Xerces; 后以 XML::Xerces::类名 的方式访问全部类。其二,回调处理器通过继承 PerlErrorHandler 实现,samples/validator.pl 头部(前 35 行节选):
#!/usr/bin/perl -w
package MyErrorHandler;
use strict;
@ISA = qw(XML::Xerces::PerlErrorHandler);
sub warning {
my $line = $_[1]->getLineNumber;
my $column = $_[1]->getColumnNumber;
my $message = $_[1]->getMessage;
printf STDERR "%s:[%s]:%d:%d:%s:%s\n",
$main::PROGRAM,$main::FILE,$line, $column, 'W', $message;
}
sub error {
... 同样通过 getLineNumber/getColumnNumber/getMessage 组装报错行
}
sub fatal_error { ... }
1;
package main;
use strict;
use XML::Xerces;
这是典型的 SAX 风格校验器用法:子类化 PerlErrorHandler,重写 warning/error/fatal_error 三个回调,错误定位信息从 SAXParseException 对象上取。DOM 侧入口则通过 XercesDOMParser/DOMParser 类,samples/DOMCount.pl 的用法提示给出了 -v(校验方案 always/never/auto)、-n(命名空间处理)、-s(Schema 处理)三个经典开关。
4.3 文档化状态:没有 POD
$ grep -c '=item' Xerces.pm
0
$ grep -c '=head' Xerces.pm
0
生成的 Xerces.pm 完全没有 POD 文档;README 也直言文档缺口:"Documentation for this API are sadly not available in POD format, but the Xerces-C html documentation is available online"——使用者被迫读 C++ API 文档再脑内翻译成 Perl(手写的 Xerces-extra.pm 同样无文档)。
💡 技术拓展:接口文件 Xerces.i 头部注释透露了设计意图——"This is the Main Xerces-* interface file. It is designed to be language agnostic ... It is also modular":接口按 DOM/SAX/SAX2/framework/grammars 拆成 interface/ 下的模块文件(13 个顶层 .i 加 interface/Perl/ 下 7 个 Perl 专属 .i),意图是一套接口服务多种脚本语言。这套设计后来在 Xerces-Perl 停更后并没有复制到其它语言(Xerces-J/Xerces-C 各自走了独立路线),但接口分层 + 语言专属 typemaps 的组织方式至今仍是 SWIG 项目的标准做法。
五、现代替代方案:XML::LibXML 实测
XML::LibXML 是 libxml2 的 Perl 绑定,由 XML::LibXML 维护组持续更新,是当前 Perl 生态事实上的 XML 处理标准件。以下逐项实测其能力,样本与 XML::Xerces 曾提供的能力一一对应。
5.1 安装与版本
# apt-get install -y libxml-libxml-perl
...
# perl -MXML::LibXML -e 'printf "XML::LibXML %s (libxml2 %s)\n", $XML::LibXML::VERSION, XML::LibXML::LIBXML_VERSION()'
XML::LibXML 2.0134 (libxml2 20912)
apt 一条命令完成安装(包版本 2.0207+dfsg+really+2.0134-1);模块版本 2.0134,底层 libxml2 2.9.12。
5.2 样本数据
准备三份演示文件:books.xml(合法,含中文内容)、books.xsd(XML Schema)、books-bad.xml(不合法样本):
<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="books.xsd">
<book id="bk001" lang="zh">
<title>XML 实战</title>
<author>张三</author>
<price currency="CNY">59.00</price>
</book>
<book id="bk002" lang="en">
<title>Learning XML</title>
<author>Eric Ray</author>
<price currency="CNY">79.50</price>
</book>
</catalog>
books.xsd 要求每个 book 依次含 title、author、price(price 为 xs:decimal 且带必填 currency 属性),id/lang 属性必填,顺序约束由 xs:sequence 定义(schema 全文 27 行,此处不再赘录)。books-bad.xml 则把 bk009 的 author 去掉且 price 内容不是数字。另建一份 books.dtd 用于 DTD 校验对照:
<!ELEMENT catalog (book+)>
<!ELEMENT book (title, author, price)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT price (#PCDATA)>
<!ATTLIST book id CDATA #REQUIRED
lang CDATA #REQUIRED>
<!ATTLIST price currency CDATA #REQUIRED>
5.3 DOM 解析与 XPath
use strict; use warnings;
use open ':std', ':encoding(UTF-8)';
use XML::LibXML;
my $doc = XML::LibXML->load_xml(location => $ARGV[0]);
my $root = $doc->documentElement;
printf "root element: %s\n", $root->nodeName;
for my $book ($doc->findnodes('/catalog/book')) {
my $id = $book->getAttribute('id');
my $t = $book->findvalue('title');
my $p = $book->findvalue('price');
print "book $id: $t (price=$p)\n";
}
print "first book lang: ", $doc->findvalue('/catalog/book[1]/@lang'), "\n";
my $n = $doc->findvalue('count(/catalog/book)');
print "total books: $n\n";
实测输出(exit 0):
root element: catalog
book bk001: XML 实战 (price=59.00)
book bk002: Learning XML (price=79.50)
first book lang: zh
total books: 2
load_xml 一步完成文件读取与 DOM 构建,findnodes/findvalue 原生支持 XPath 1.0 全语法,中文内容正常输出。
5.4 DOM 创建与落盘
use strict; use warnings;
use XML::LibXML;
my $doc = XML::LibXML->createDocument('1.0', 'UTF-8');
my $cat = $doc->createElement('catalog');
$doc->setDocumentElement($cat);
for my $i (1..2) {
my $b = $doc->createElement('book');
$b->setAttribute(id => "bk10$i");
$b->setAttribute(lang => 'zh');
$b->appendTextChild(title => "Perl 与 XML 第${i}版");
my $p = $doc->createElement('price');
$p->setAttribute(currency => 'CNY');
$p->appendText(sprintf "%.2f", 45.0 + $i);
$b->appendChild($p);
$cat->appendChild($b);
}
$doc->toFile('/root/libxml-created.xml', 1);
print $doc->toString(1);
实测输出(exit 0,落盘文件内容一致):
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="bk101" lang="zh">
<title>Perl 与 XML 第1版</title>
<price currency="CNY">46.00</price>
</book>
...(bk102 条目结构相同:title 为 "Perl 与 XML 第2版"、price 47.00,略)
</catalog>
5.5 DTD 校验
校验脚本:
use strict; use warnings;
use open ':std', ':encoding(UTF-8)';
use XML::LibXML;
my ($xml, $dtdf) = @ARGV;
my $doc = XML::LibXML->load_xml(location => $xml, { recover => 0 });
my $dtd = XML::LibXML::Dtd->new('', $dtdf);
eval { $doc->validate($dtd) };
if ($@) { print "$xml: INVALID\n$@" } else { print "$xml: VALID\n" }
先校验带 XSI 属性的 books.xml,暴露一个典型坑:
# perl /root/libxml-dtd.pl /root/xmldemo/books.xml /root/xmldemo/books.dtd
/root/xmldemo/books.xml: INVALID
/root/xmldemo/books.xml:0: validity error : No declaration for attribute noNamespaceSchemaLocation of element catalog
/root/xmldemo/books.xml:0: validity error : No declaration for attribute xmlns:xsi of element catalog
XML 内容完全符合 DTD,却因 xmlns:xsi、xsi:noNamespaceSchemaLocation 两个 Schema 指向属性未在 DTD 中声明而被判非法——DTD 对 Schema 的存在一无所知。去掉 XSI 属性后的纯 DTD 版本(books-dtd.xml)实测:
# perl /root/libxml-dtd.pl /root/xmldemo/books-dtd.xml /root/xmldemo/books.dtd
/root/xmldemo/books-dtd.xml: VALID
非法样本(缺 author 且 DTD 未声明的 XSI 属性)则同时报出内容模型错误:
# perl /root/libxml-dtd.pl /root/xmldemo/books-bad.xml /root/xmldemo/books.dtd
/root/xmldemo/books-bad.xml: INVALID
...(前两行与上文相同的 xsi 属性报错,略)
/root/xmldemo/books-bad.xml:0: validity error : Element book content does not follow the DTD, expecting (title , author , price), got (title price )
5.6 XSD 校验
use strict; use warnings;
use open ':std', ':encoding(UTF-8)';
use XML::LibXML;
my ($xml, $xsdf) = @ARGV;
my $doc = XML::LibXML->load_xml(location => $xml, { recover => 0 });
my $schema = XML::LibXML::Schema->new(location => $xsdf);
eval { $schema->validate($doc) };
if ($@) { print "$xml: INVALID\n$@" } else { print "$xml: VALID\n" }
实测:
# perl /root/libxml-xsd.pl /root/xmldemo/books.xml /root/xmldemo/books.xsd
/root/xmldemo/books.xml: VALID
# perl /root/libxml-xsd.pl /root/xmldemo/books-bad.xml /root/xmldemo/books.xsd
/root/xmldemo/books-bad.xml: INVALID
/root/xmldemo/books-bad.xml:0: Schemas validity error : Element 'price': This element is not expected. Expected is ( author ).
XML::LibXML::Schema 支持 W3C XML Schema 全校验,错误信息给出元素级定位。
5.7 Reader 拉式流式解析
大文件场景对应 SAX 的替代是 libxml2 的 Reader(拉式解析,内存占用与 SAX 同级但控制流更直白):
use strict; use warnings;
use XML::LibXML::Reader;
my $reader = XML::LibXML::Reader->new(location => $ARGV[0]) or die "cannot open $ARGV[0]\n";
my $books = 0;
while ($reader->read) {
if ($reader->nodeType == XML_READER_TYPE_ELEMENT && $reader->name eq 'book') {
$books++;
my $id = $reader->getAttribute('id') // '';
print "element book id=$id\n";
}
}
print "pull-parse done, book elements: $books\n";
实测输出(exit 0):
element book id=bk001
element book id=bk002
pull-parse done, book elements: 2
六、能力对照与迁移建议
6.1 能力对照表
| 能力 | XML::Xerces 2.7.0-0 | XML::LibXML 2.0134 |
|---|---|---|
| Ubuntu 22.04 可安装性 | 不可用(编译失败,实测) | apt 一条命令,实测可用 |
| DOM 解析/操作 | DOM 2/3 全套(API 面完整) | libxml2 DOM,含 XPath 1.0 |
| SAX 解析 | SAX 1/2 全套回调 | XML::LibXML::SAX 兼容 XML::SAX |
| 流式解析 | SAX 回调模型 | Reader 拉式模型(实测可用) |
| DTD 校验 | 支持(API 面 + t/ 覆盖) | 支持(实测 VALID/INVALID 正确) |
| XML Schema 校验 | 支持(API 面 + t/ 覆盖) | 支持(实测 VALID/INVALID 正确) |
| 文档 | 无 POD,需读 C++ API 文档 | 完整 POD,perldoc 可查 |
6.2 迁移映射与建议
| XML::Xerces 用法 | XML::LibXML 对应 |
|---|---|
XML::Xerces::XMLPlatformUtils::Initialize | 无需调用,模块加载即完成初始化 |
XML::Xerces::XercesDOMParser->new + parse(uri) | XML::LibXML->load_xml(location => $uri) |
$parser->setValidationScheme($XML::Xerces::AbstractDOMParser::Val_Always) | load_xml($f, {no_blanks => 1}) + 显式 $doc->validate($dtd) 或 $schema->validate($doc) |
XML::Xerces::SAX2XMLReader + PerlContentCallbackHandler 子类 | XML::LibXML::SAX 或 XML::SAX::ParserFactory(保持 XML::SAX Handler 接口) |
XML::Xerces::DOMImplementation::createDocument | XML::LibXML->createDocument('1.0', 'UTF-8') + createElement/setDocumentElement |
$doc->getElementsByTagName | 同名方法存在,或直接 findnodes 用 XPath |
迁移建议按三个层次执行:其一,新代码直接使用 XML::LibXML,不存在任何理由为一个停更 20 年、无法编译的绑定投入环境定制;其二,存量代码按上表映射改造,重点是把"解析器级校验开关"迁移为"文档级显式校验"(XML::LibXML 把 DTD/XSD 校验做成独立调用,比 Xerces 的解析器选项模型更清晰);其三,回调类代码(SAX Handler)优先评估能否改为 DOM+XPath 或 Reader 拉式,除非确有大文件内存压力,XML::SAX 派生的 Handler 改造量最小。
6.3 已知坑(实测发现)
- UTF-8 输出层:未开
use open ':std', ':encoding(UTF-8)'时 print 中文会报Wide character in print——数据本身没问题,是 STDOUT 编码层问题,加一层编码声明即可; - XML::LibXML::Dtd->new 需要两个参数:
new(CLASS, external, system),外部 ID 传空串''、系统 ID 传文件路径;只传一个参数直接Usage报错退出; - DTD 校验与 XSI 属性互斥:文档头部带 xmlns:xsi/xsi:noNamespaceSchemaLocation 时 DTD 校验必然 INVALID,二选一使用,或为 DTD 补充属性声明;
- XML::Xerces 命名空间残留:若存量代码真的要跑在能编译出 XML::Xerces 的老机器上,注意其类命名空间是
XML::Xerces::而非XML::Xercesc::,由 Xerces.i 的%module "XML::Xerces"决定。
七、常见问题
Q1:XML::Xerces 真的一点都跑不起来吗?有没有"曲线"方案? Ubuntu 22.04 实测不能。三层变通(版本号补丁、自建 SWIG 1.3.40 重新生成胶水层等)可以推进到接近终点,但 SWIG 1.3.28 与 1.3.40 之间的类型描述符命名漂移叠加命名空间级联问题,使完整打通需要接口文件、生成代码、C++ 库三方手术,详见《Apache Xerces-Perl 在 Ubuntu 22.04 安装实录》第七节。除非有强约束的老系统环境,不建议尝试。
Q2:XML::LibXML 校验能力与 Xerces 相比有没有缩水? W3C XML Schema 校验两者都有完整实现。Xerces 家族额外提供 PSVI(Post-Schema-Validation-Infoset)回调,XML::LibXML 没有对应能力;需要 PSVI 的极端场景在 Perl 生态没有现成替代,通常意味着需求本身应该换工具栈(如迁移到 Java/Go 的 XML 库)。
Q3:XML::SAX 生态里还有别的 Xerces 系替代吗? XML::SAX 生态有 Expat(XML::Parser,基于 expat,不支持校验)、XML::SAX::PurePerl 等;带 Schema 校验能力的只有 libxml2 一线(XML::LibXML 与基于它的 XML::SAX 驱动)。因此"校验"需求事实上锁定了 XML::LibXML。
Q4:评估老项目时还有什么值得记取的教训? 两条。一是绑定项目的生命周期 = 库本体 + 接口生成器 + 目标语言 ABI 三者的交集,XML::Xerces 三者交集在 2006 年就已归零;二是"发行包还在 CPAN/归档上"不等于"还能用",本文的 cpanm 实测(发行物可取、配置失败)就是最好的注脚。
八、参考链接
- Apache 官方归档 Xerces-Perl 发行目录:https://archive.apache.org/dist/xerces/p/
- XML::Xerces CPAN 页面:https://metacpan.org/pod/XML::Xerces
- XML::LibXML CPAN 页面:https://metacpan.org/pod/XML::LibXML
- libxml2 官方站点:https://gitlab.gnome.org/GNOME/libxml2
- XML::SAX 规范(Perl SAX 2.1):https://metacpan.org/pod/XML::SAX