PHP - 处理无效的XML

5

我正在使用SimpleXML加载一些xml文件(这些文件不是我编写/提供的,也不能真正更改其格式)。

偶尔(例如每50个左右的文件中有一个或两个),它们不转义任何特殊字符(主要是&,但有时也会出现其他随机无效字符)。这会导致php中的SimpleXML失败,我也不知道如何处理解析无效的XML。

我的第一个想法是将XML预处理为字符串,并将所有字段都放入CDATA中,以使其正常工作,但由于我需要处理的XML以属性字段中的所有数据为例,因此我无法使用CDATA的想法。 XML的示例:

 <Author v="By Someone & Someone" />

在使用SimpleXML加载XML之前,最好的处理方法是什么,以替换所有无效字符?


如果只有 & 这个符号,您在加载之前不可以转义它吗? - Dormilich
这不只是&无效的问题。 - Paul
3个回答

7
您需要的是能够利用libxml的内部错误定位无效字符并相应地进行转义的工具。以下是我编写的一个示例。查看libxml_get_errors()的结果以获取错误信息。
function load_invalid_xml($xml)
{
    $use_internal_errors = libxml_use_internal_errors(true);
    libxml_clear_errors(true);

    $sxe = simplexml_load_string($xml);

    if ($sxe)
    {
        return $sxe;
    }

    $fixed_xml = '';
    $last_pos  = 0;

    foreach (libxml_get_errors() as $error)
    {
        // $pos is the position of the faulty character,
        // you have to compute it yourself
        $pos = compute_position($error->line, $error->column);
        $fixed_xml .= substr($xml, $last_pos, $pos - $last_pos) . htmlspecialchars($xml[$pos]);
        $last_pos = $pos + 1;
    }
    $fixed_xml .= substr($xml, $last_pos);

    libxml_use_internal_errors($use_internal_errors);

    return simplexml_load_string($fixed_xml);
}

2
提供一些计算机位置的示例会很方便! - Phil Sturgeon

2
我认为创建compute_position函数的解决方法是在处理之前使xml字符串变平。 重写Josh发布的代码:
function load_invalid_xml($xml)
{
    $use_internal_errors = libxml_use_internal_errors(true);
    libxml_clear_errors(true);

    $sxe = simplexml_load_string($xml);

    if ($sxe)
    {
        return $sxe;
    }

    $fixed_xml = '';
    $last_pos  = 0;

    // make string flat
    $xml = str_replace(array("\r\n", "\r", "\n"), "", $xml);

    // get file encoding
    $encoding = mb_detect_encoding($xml);

    foreach (libxml_get_errors() as $error)
    {
        $pos = $error->column;
        $invalid_char = mb_substr($xml, $pos, 1, $encoding);
        $fixed_xml .= substr($xml, $last_pos, $pos - $last_pos) . htmlspecialchars($invalid_char);
        $last_pos = $pos + 1;
    }
    $fixed_xml .= substr($xml, $last_pos);

    libxml_use_internal_errors($use_internal_errors);

    return simplexml_load_string($fixed_xml);
}

我添加了编码内容,因为我在使用简单的数组[index]方法从字符串中获取字符时遇到了问题。

这应该都可以工作,但是不知道为什么,我发现$error->column给出的数字与它应该给出的数字不同。我尝试通过在xml中添加一些无效字符并检查它将返回什么值来调试它,但没有成功。

希望有人能告诉我这种方法的问题所在。


虽然您的方法在运行,但它并没有解决我特定的问题,导致了这个错误。 - Patrick

0

尽管这个问题已经存在10年了(当我打字时),我仍然遇到类似的XML解析问题(PHP8.1),这就是为什么我来到这里的原因。已经给出的答案很有帮助,但要么不完整,要么不一致,或者对我的问题和原始发帖人也不适用。

检查内部XML解析问题似乎是正确的,但是有735个错误代码(参见https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-xmlerror.html),因此需要更具适应性的解决方案。

我在上面使用了“不一致”一词,因为其他答案中最好的答案(@Adam Szmyd)将多字节字符串处理与非多字节字符串处理混合在一起。

以下代码以Adam的代码为基础,我为我的情况重新制定了它,我感觉可以根据实际遇到的问题进一步扩展。因此,我也不完全 - 抱歉!

这段代码的核心是将每个(在我的实现中只有1个)XML解析错误作为单独的情况进行处理。我遇到的错误是一个无法识别的HTML实体(&ccedil; - ç),所以我使用PHP实体替换来解决它。
function load_invalid_xml($xml)
{
    $use_internal_errors = libxml_use_internal_errors(true);
    libxml_clear_errors(true);

    $sxe = simplexml_load_string($xml);

    if ($sxe)
        return $sxe;

    $fixed_xml = '';
    $last_pos  = 0;

    // make string flat
    $xmlFlat = mb_ereg_replace( '(\r\n|\r|\n)', '', $xml );

    // Regenerate the error but using the flattened source so error offsets are directly relevant
    libxml_clear_errors();
    $xml_doc = @simplexml_load_string( $xmlFlat );

    foreach (libxml_get_errors() as $error)
    {
        $pos = $error->column - 1; // ->column appears to be 1 based, not 0 based

        switch( $error->code ) {

            case 26: // error undeclared entity
            case 27: // warning undeclared entity
                if ($pos >= 0) { // the PHP docs suggest this not always set (in which case ->column is == 0)

                    $left = mb_substr( $xmlFlat, 0, $pos );
                    $amp = mb_strrpos( $left, '&' );

                    if ($amp !== false) {

                        $entity = mb_substr( $left, $amp );
                        $fixed_xml .= mb_substr( $xmlFlat, $last_pos, $amp - $last_pos )
                            . html_entity_decode( $entity );
                        $last_pos = $pos;
                    }
                }
                break;

            default:
        }
    }
    $fixed_xml .= mb_substr($xml, $last_pos);

    libxml_use_internal_errors($use_internal_errors);

    return simplexml_load_string($fixed_xml);
}

你能否预先通过 preg_replace_callback 运行 XML,并在过滤掉4个允许的实体后对任何匹配项运行 html_entity_decode - miken32
这是一个方法,但我认为它取决于内容。我知道我正在处理的XML有些挑剔,多次处理编码的URL会导致URL出现问题,因此采取温和的方法,只“修正”导致解析器失败的那些东西。 - Mark Bradley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接