PHP DomDocument не может обрабатывать символы utf-8 (☆)

Веб-сервер обслуживает ответы в кодировке 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?

3 ответа

Решение

DOMDocument::loadHTML() ожидает строку HTML.

HTML использует ISO-8859-1 кодировка (ISO Латинский алфавит № 1) по умолчанию согласно его спецификациям. Это дольше, см. 6.1. Набор символов документа HTML. На самом деле это больше поддержка по умолчанию для Windows-1252 в общих веб-браузерах.

Я возвращаюсь к этому далеко, потому что PHP DOMDocument основан на libxml, и это приносит HTMLparser, который разработан для HTML 4.0.

Я бы сказал, что можно с уверенностью предположить, что вы можете загрузить ISO-8859-1 закодированная строка.

Ваша строка UTF-8 закодирован. Превратите все символы выше 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, определяющим набор символов. Это также рекомендуется для строк HTML, которые недоступны через веб-сервер (например, сохраняются на диске или внутри строки, как в вашем примере). Веб-сервер обычно устанавливает это как заголовок ответа.

Если вас не волнуют неуместные предупреждения, вы можете просто добавить их перед строкой:

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

Согласно спецификациям HTML 2.0, элементы, которые могут появляться только в <head> раздел документа, будет автоматически размещен там. Вот что и здесь происходит. Вывод (pretty-print):

<!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>

Есть более быстрое решение для этого, после загрузки вашего 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
<?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>
Другие вопросы по тегам