Skip to content

libxml

libxml 是 PHP XML 处理的底层 C 库,PHP 的 DOM、SimpleXML、XMLReader、XMLWriter、XSL 扩展都基于 libxml 构建。理解 libxml 的配置和高级功能有助于优化 XML 处理性能和解决各种 XML 解析问题。

前置知识

阅读本节前,建议先了解:DOM 操作SimpleXML

基础概念

libxml 在 PHP 中的角色

libxml2 是一个可移植的 XML C 解析器和工具包,提供了:

  • XML 1.0 和 XML 命名空间支持
  • DTD 验证
  • XPath 1.0 支持
  • XInclude 支持
  • HTML 解析器
  • XML Schema (XSD) 验证
  • RELAX NG 验证

PHP 中的 libxml 函数

PHP 提供了一组以 libxml_ 开头的函数来控制 libxml 的行为。

错误处理

获取 libxml 错误

php
<?php
declare(strict_types=1);

// 启用内部错误收集
libxml_use_internal_errors(true);

// 解析可能有错误的 XML
$doc = new DOMDocument();
$doc->loadXML($malformedXml);

// 获取错误
$errors = libxml_get_errors();
foreach ($errors as $error) {
    echo "级别: {$error->level}" . PHP_EOL;
    echo "代码: {$error->code}" . PHP_EOL;
    echo "消息: {$error->message}" . PHP_EOL;
    echo "文件: {$error->file}" . PHP_EOL;
    echo "行号: {$error->line}" . PHP_EOL;
    echo "列号: {$error->column}" . PHP_EOL;
    echo "---" . PHP_EOL;
}

// 清除错误
libxml_clear_errors();

// 恢复默认错误处理
libxml_use_internal_errors(false);

错误级别

php
<?php
declare(strict_types=1);

// 错误级别常量
// LIBXML_ERR_WARNING   - 警告
// LIBXML_ERR_ERROR     - 可恢复的错误
// LIBXML_ERR_FATAL     - 致命错误

libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadXML('<root><invalid>');

foreach (libxml_get_errors() as $error) {
    $level = match ($error->level) {
        LIBXML_ERR_WARNING => 'WARNING',
        LIBXML_ERR_ERROR   => 'ERROR',
        LIBXML_ERR_FATAL   => 'FATAL',
        default             => 'UNKNOWN',
    };
    echo "[{$level}] 行 {$error->line}: {$error->message}";
}

libxml_clear_errors();

自定义错误处理

php
<?php
declare(strict_types=1);

/**
 * 自定义 libxml 错误处理器
 */
class XmlErrorHandler
{
    private array $errors = [];

    public function __construct()
    {
        libxml_use_internal_errors(true);
    }

    public function hasErrors(): bool
    {
        return count(libxml_get_errors()) > 0;
    }

    public function getErrors(): array
    {
        $this->errors = libxml_get_errors();
        return $this->errors;
    }

    public function getFirstError(): ?LibXMLError
    {
        $errors = libxml_get_errors();
        return $errors[0] ?? null;
    }

    public function getLastErrorString(): string
    {
        $error = $this->getFirstError();
        if ($error === null) {
            return '';
        }
        return trim($error->message) . " at line {$error->line}";
    }

    public function clear(): void
    {
        libxml_clear_errors();
        $this->errors = [];
    }

    public function __destruct()
    {
        $this->clear();
        libxml_use_internal_errors(false);
    }
}

// 使用示例
$handler = new XmlErrorHandler();
$doc = new DOMDocument();
$doc->loadXML($xmlString);

if ($handler->hasErrors()) {
    echo "XML 解析错误: " . $handler->getLastErrorString() . PHP_EOL;
    // $handler->clear();
}

libxml 选项

常用选项

php
<?php
declare(strict_types=1);

// LIBXML_NOBLANKS       - 去除空白文本节点
// LIBXML_NOENT          - 替换实体引用
// LIBXML_NOERROR        - 不生成错误报告
// LIBXML_NOWARNING      - 不生成警告报告
// LIBXML_NONET           - 禁止网络访问(加载 DTD/XSD 等)
// LIBXML_NOCDATA         - 将 CDATA 节点合并为文本节点
// LIBXML_COMPACT         - 小节点分配优化(减少内存)
// LIBXML_PARSE_HUGE      - 放松解析限制(大文件)
// LIBXML_DTDATTR         - 默认 DTD 属性
// LIBXML_DTDLOAD         - 加载外部子集
// LIBXML_DTDVALID        - 使用 DTD 验证
// LIBXML_NOXMLDECL       - 不输出 XML 声明
// LIBXML_BIGLINES        - 支持超过 65535 行的文件

// DOM 使用
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NOBLANKS | LIBXML_NOERROR);
$doc->load('file.xml', LIBXML_NOBLANKS | LIBXML_COMPACT);

// SimpleXML 使用
$xml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$xml = simplexml_load_file('file.xml', 'SimpleXMLElement', LIBXML_NOCDATA);

// XMLReader 使用
$reader = new XMLReader();
$reader->open('file.xml', null, LIBXML_NOBLANKS);

选项详解

php
<?php
declare(strict_types=1);

// LIBXML_NONET - 安全选项,禁止网络访问
$doc->loadXML($xml, LIBXML_NONET);
// 防止 XXE 攻击:禁止加载外部实体

// LIBXML_NOENT - 替换实体
$xml = '<data>&amp;name&amp;</data>';
$doc->loadXML($xml, LIBXML_NOENT);
// &amp; 被替换为 &

// LIBXML_COMPACT - 内存优化
$doc->load('large.xml', LIBXML_COMPACT);
// 减少节点分配器内存使用

// LIBXML_PARSE_HUGE - 放松限制
$doc->loadXML($hugeXml, LIBXML_PARSE_HUGE);
// 解析超过默认限制(如 256KB 文本节点)的 XML

// 组合安全选项
$safeOptions = LIBXML_NONET | LIBXML_NOENT | LIBXML_DTDATTR;

XML 验证

DTD 验证

php
<?php
declare(strict_types=1);

// DTD 验证方式 1:通过 load 选项
$doc = new DOMDocument();
$result = $doc->load('document.xml', LIBXML_DTDVALID);

if ($result === false || !$doc->validate()) {
    echo "DTD 验证失败" . PHP_EOL;
    foreach (libxml_get_errors() as $error) {
        echo "  {$error->message}" . PHP_EOL;
    }
}

// DTD 验证方式 2:单独调用 validate()
$doc = new DOMDocument();
$doc->load('document.xml');

if ($doc->validate()) {
    echo "验证通过" . PHP_EOL;
}

XML Schema (XSD) 验证

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->load('data.xml');

libxml_use_internal_errors(true);

// Schema 验证
if ($doc->schemaValidate('schema.xsd')) {
    echo "XSD 验证通过" . PHP_EOL;
} else {
    echo "XSD 验证失败:" . PHP_EOL;
    foreach (libxml_get_errors() as $error) {
        echo "  [{$error->code}] {$error->message}" . PHP_EOL;
    }
}

libxml_clear_errors();

Relax NG 验证

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->load('data.xml');

libxml_use_internal_errors(true);

// Relax NG 验证(XML 格式)
if ($doc->relaxNGValidate('schema.rng')) {
    echo "Relax NG 验证通过" . PHP_EOL;
}

// Relax NG 验证(紧凑语法)
if ($doc->relaxNGValidateSource($rngContent)) {
    echo "验证通过" . PHP_EOL;
}

libxml_clear_errors();

XInclude 支持

php
<?php
declare(strict_types=1);

// XInclude 允许将外部 XML 片段包含到文档中

$mainXml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns:xi="http://www.w3.org/2001/XInclude">
    <header>文档标题</header>
    <xi:include href="chapter1.xml"/>
    <xi:include href="chapter2.xml"/>
    <footer>文档结尾</footer>
</document>
XML;

$doc = new DOMDocument();
$doc->loadXML($mainXml);

// 处理 XInclude
$doc->xinclude(LIBXML_NOERROR);

// 处理 XInclude(指定 base URI)
// $doc->xinclude(LIBXML_NOERROR, '/path/to/base/');

echo $doc->saveXML();

实体管理

防止 XXE 攻击

php
<?php
declare(strict_types=1);

// XXE(XML External Entity)攻击示例
$maliciousXml = <<<XML
<?xml version="1.0"?>
<!DOCTYPE foo [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
XML;

// 不安全的解析方式
// $doc->loadXML($maliciousXml); // 会读取 /etc/passwd

// 安全的解析方式
$safeOptions = LIBXML_NONET | LIBXML_NOENT;
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadXML($maliciousXml, $safeOptions);
// LIBXML_NONET 禁止网络访问
// 配合禁用外部实体加载可进一步加固

// 安全选项组合
function safeLoadXml(string $xml): ?DOMDocument
{
    $doc = new DOMDocument();

    // 禁用外部实体和 DTD 加载
    $previous = libxml_disable_entity_loader(true); // PHP < 8.0
    libxml_use_internal_errors(true);

    $loaded = $doc->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);

    $errors = libxml_get_errors();
    libxml_clear_errors();

    return $loaded ? $doc : null;
}

实战示例

XML 工具类

php
<?php
declare(strict_types=1);

/**
 * 通用 XML 工具类
 */
class XmlUtils
{
    /**
     * 安全加载 XML 文件
     */
    public static function loadFile(string $path, int $options = 0): DOMDocument
    {
        $doc = new DOMDocument();
        $doc->preserveWhiteSpace = false;
        $doc->formatOutput = true;

        libxml_use_internal_errors(true);

        $safeOptions = LIBXML_NONET | $options;
        $result = $doc->load($path, $safeOptions);

        if ($result === false) {
            $errors = self::collectErrors();
            throw new RuntimeException("XML 加载失败: " . implode('; ', $errors));
        }

        return $doc;
    }

    /**
     * 安全加载 XML 字符串
     */
    public static function loadString(string $xml, int $options = 0): DOMDocument
    {
        $doc = new DOMDocument();
        $doc->preserveWhiteSpace = false;
        $doc->formatOutput = true;

        libxml_use_internal_errors(true);

        $safeOptions = LIBXML_NONET | $options;
        $result = $doc->loadXML($xml, $safeOptions);

        if ($result === false) {
            $errors = self::collectErrors();
            throw new RuntimeException("XML 解析失败: " . implode('; ', $errors));
        }

        return $doc;
    }

    /**
     * 验证 XML
     */
    public static function validate(string $xmlPath, string $schemaPath): bool
    {
        $doc = self::loadFile($xmlPath);
        libxml_use_internal_errors(true);

        $valid = $doc->schemaValidate($schemaPath);

        if (!$valid) {
            return false;
        }

        return true;
    }

    /**
     * 格式化 XML 字符串
     */
    public static function format(string $xml): string
    {
        $doc = self::loadString($xml);
        return $doc->saveXML($doc->documentElement);
    }

    /**
     * 获取 libxml 版本
     */
    public static function getVersion(): string
    {
        return LIBXML_VERSION . ' (' . LIBXML_DOTTED_VERSION . ')';
    }

    /**
     * 收集错误信息
     */
    private static function collectErrors(): array
    {
        $messages = [];
        foreach (libxml_get_errors() as $error) {
            $messages[] = trim($error->message) . " at line {$error->line}";
        }
        libxml_clear_errors();
        return $messages;
    }
}

注意事项

性能优化

php
<?php
declare(strict_types=1);

// 1. LIBXML_COMPACT 减少内存
$doc->load('large.xml', LIBXML_COMPACT);

// 2. LIBXML_PARSE_HUGE 处理大文件
$doc->loadXML($hugeXml, LIBXML_PARSE_HUGE);

// 3. 关闭空白处理
$doc->preserveWhiteSpace = false;

// 4. 禁用错误收集(生产环境)
libxml_use_internal_errors(false);

// 5. 缓存 DTD/XSD
$doc->load('data.xml', LIBXML_DTDLOAD | LIBXML_DTDATTR);

版本信息

php
<?php
declare(strict_types=1);

echo "libxml 版本: " . LIBXML_VERSION . PHP_EOL;
echo "libxml 点分版本: " . LIBXML_DOTTED_VERSION . PHP_EOL;

最佳实践

  1. 始终使用 LIBXML_NONET:禁止网络访问防止 XXE
  2. 使用内部错误处理libxml_use_internal_errors(true) 避免输出到屏幕
  3. 大文件用 LIBXML_COMPACT:优化内存分配
  4. 生产环境验证 XML:使用 XSD 或 DTD 验证输入
  5. 清理错误缓存:处理完后调用 libxml_clear_errors()

下一节

继续学习:JSON 编解码

参考链接