PHP DOMDocument处理utf-8字符失败

55

Web服务器正在使用UTF-8编码提供响应,所有文件都已保存为UTF-8编码,并且我所知道的所有设置都已设置为UTF-8编码。

这是一个快速测试输出是否有效的程序:

<?php
$html = <<<HTML
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Test!</title>
</head>
<body>
    <h1>☆ Hello ☆ World ☆</h1>
</body>
</html>
HTML;

$dom = new DOMDocument("1.0", "utf-8");
$dom->loadHTML($html);

header("Content-Type: text/html; charset=utf-8");
echo($dom->saveHTML());

程序的输出结果是:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Test!</title></head><body>
    <h1>&acirc;&#152;&#134; Hello &acirc;&#152;&#134; World &acirc;&#152;&#134;</h1>
</body></html>

渲染结果为:

★ 你好 ★ 世界 ★


我可能做错了什么?我需要更具体地告诉DOMDocument正确处理utf-8编码吗?


感谢您提出这个问题,类似的问题是:如何保留中文或其他外语而不将它们转换为代码?,但您可能会认为这是一种hack方法。 - hakre
相关:[PHP请求#47875-没有设置HTML输入编码的选项](https://bugs.php.net/bug.php?id=47875) - hakre
1
奇怪的是:php文档中写道: “DOM扩展使用UTF-8编码。对于ISO-8859-1编码的文本,请使用utf8_encode()和utf8_decode()进行处理,对于其他编码,请使用Iconv。” 参见:http://www.php.net/manual/en/intro.dom.php - juwens
HTML5的元字符集编码声明自libxml 2.8.0版本开始得到支持,因此问题中的代码示例现在可以正常工作。 - Alf Eaton
问题在于您指定了utf8,但&#152等不是用于utf8,而是ANSI。例如,“dagger”可以在http://hexutf8.com/?q=e280a0中找到。 - jar
3个回答

118

DOMDocument::loadHTML()函数需要一个HTML字符串作为参数。

HTML默认使用ISO-8859-1编码(即第一拉丁字母表),参照标准规定。这一点早在很久以前就有了,详见6.1 HTML文档字符集。但实际上,常用的网络浏览器更倾向于支持Windows-1252编码。

我提到这一点是因为PHP的DOMDocument基于libxml,它使用的是设计用于HTML 4.0的HTML解析器

因此可以认为,使用ISO-8859-1编码的字符串是安全的。

然而,你的字符串使用的是UTF-8编码。只需将所有ASCII码大于127 / h7F的字符转换成HTML实体,就可以顺利加载。如果不想自己手动转换,可以使用mb_convert_encoding函数并设置目标编码为HTML-ENTITIES

  • 那些已经有命名实体的字符,将得到所对应的命名实体。例如: € -> &euro;
  • 其他字符将被转换成相应的十进制数字实体,例如:☆ -> &#9734;

下面是一个代码示例,通过回调函数使该过程更加清晰可见:

$html = preg_replace_callback('/[\x{80}-\x{10FFFF}]/u', function($match) {
    list($utf8) = $match;
    $entity = mb_convert_encoding($utf8, 'HTML-ENTITIES', 'UTF-8');
    printf("%s -> %s\n", $utf8, $entity);
    return $entity;
}, $html);

以下是针对您字符串的示例输出:

☆ -> &#9734;
☆ -> &#9734;
☆ -> &#9734;
无论如何,那只是为了更深入地查看您的字符串。您要将它转换为loadHTML可以处理的编码方式。这可以通过将所有US-ASCII之外的内容转换为HTML实体来完成:
$us_ascii = mb_convert_encoding($utf_8, 'HTML-ENTITIES', 'UTF-8');

请注意确保您的输入实际上是UTF-8编码。如果您的输入中甚至出现混合编码(这在某些情况下可能会发生),mb_convert_encoding只能处理每个字符串一种编码。我已经概述了如何使用正则表达式更具体地进行字符串替换,因此现在暂时不再赘述。

另一个选择是提示编码。在您的情况下,可以通过修改文档并添加一个来实现。

<meta http-equiv="content-type" content="text/html; charset=utf-8">

这是指定字符集的Content-Type。对于不通过Web服务器(例如保存在磁盘上或内部字符串中,如您的示例中)提供的HTML字符串,这也是最佳实践。Web服务器通常将其设置为响应标头。

如果您不关心错误放置的警告,可以将其添加到字符串前面:

$dom = new DomDocument();
$dom->loadHTML('<meta http-equiv="content-type" content="text/html; charset=utf-8">'.$html);

根据HTML 2.0规范,只能出现在文档的<head>部分的元素将自动放置在那里。这也是此处发生的情况。输出结果(漂亮打印):

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta charset="utf-8">
    <title>Test!</title>
  </head>
  <body>
    <h1>☆ Hello ☆ World ☆</h1>    
  </body>
</html>

2
@hakre:太完美了!你解决了我的严重问题,现在我一点都不头疼了!! - Aliweb
1
很棒的答案,但您推荐使用哪种方法--使用mb_convert_encoding()还是在loadHTML()中添加meta标签? - Nate
1
@Nate:我会说这取决于情况。通常我不建议使用mb_convert_encoding(),但对于这种情况,我确实会这样做。然而,这仍然取决于您是想在自己的步骤中进行转换还是只想将其压入DOOMDocument::loadHTML()中,从而将元素泄漏到文档中。例如,如果该元素已经存在,我不知道会发生什么。我从未测试过这一点,但通常它“只是工作”(tm)。答案中的不同方法更多是为了解释。 - hakre
对于使用另一种方法的任何人,我建议检查下面DeZeA的答案,因为它效果更好,不会从html标签中删除类。 - Moshe Shaham

18

有更快的解决方法,在将您的html文档加载到DOMDocument后,只需设置(或者更好地说是重置)原始编码。以下是示例代码:

$dom = new DOMDocument();
$dom->loadHTML('<?xml encoding="UTF-8">' . $html);

foreach ($dom->childNodes as $item)
    if ($item->nodeType == XML_PI_NODE)
        $dom->removeChild($item);
$dom->encoding = 'UTF-8'; // reset original encoding

1
这比hakre添加元标记的版本效果更好,因为添加元标记会从HTML标记中删除类。 - Moshe Shaham
可能是这样..我在一个文本文件中有一些有用的代码片段。虽然这是DOMDocument类的一些标准用法,但我并不声称这是什么原创性的东西。 - DeZeA

11
<?php
  header("Content-type: text/html; charset=utf-8");
  $html = <<<HTML
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Test!</title>
</head>
<body>
    <h1>☆ Hello ☆ World ☆</h1>
</body>
</html>
HTML;

  $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
  $dom = new DOMDocument("1.0", "utf-8");
  $dom->loadHTML($html);

  header("Content-Type: text/html; charset=utf-8");
  echo($dom->saveHTML());

输出:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Test!</title></head><body>
    <h1>&#9734; Hello &#9734; World &#9734;</h1>
</body></html>

2
@powtac:这些变量实际上不需要那个 header 行。这里的所有非 us-ascii 字符都是实体。除非您指定一个(错误的)编码不共享 us-ascii,否则地球上的任何浏览器都将始终正确显示它们。但是请注意,这也不是错误的。 - hakre

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