Цель статьи — систематизировать информацию о том, как PHP DOM XML extension работатет с кодировками (выпал случай разобраться).
Так как DOM extension сам по себе суть враппер вокруг libxml, то сказанное будет по большей части верно и для некоторых других DOM имплементаций (основанных на libxml).
1. DOMDocument::$encoding property не влияет ни на что, кроме операции дампа XML документа —
То есть совсем не влияет.
Вне зависимости от того, установлен ли этот атрибут в конструкторе:
Или же выставлен позже:
Внутреннее представление документа (в DOM объекте) — всегда в UTF-8.
2. Соответственно:
Всегда рассмативают входные данные как UTF-8 текст.
Более того, можно в этом убедиться, подав некорректную UTF-8 строку на вход
«DOMDocumentFragment::appendXML(): Entity: line 1: parser error: Input is not proper UTF-8, indicate encoding !»
(надо упомянуть, что для
Остальные из вышеперечисленных методов добавляют данные «as is», не производя никаких преобразований, и не выдают такого предупреждения. Проблемы возникнут позже (при попытке дампа документа):
«Warning: DOMDocument::saveXML(): output conversion failed due to conv error, ...»
3. При работе с XML деревом
Исключение — ситуация, когда мы сами же создали внутренне некорректную DOM структуру, добавляя узлы не в UTF-8 (наши же данные будут возвращены без изменений). Но, как указано выше, такой документ будет не сохранить.
И значения
4. Выставленное значение кодировки также никак не влияет и на поведение
Единственная возможность указать кодировку документа при парсинге — указать её в соответствующих тегах.
Для XML:
Для HTML:
При этом значение атрибута
Если кодировка в заголовке XML/HTML документа не указана, то документ парсится как
а
В силу большей формализованности XML, того, что кодировка XML документа указывается в самом первом теге, а также того, что кодировкой по умолчанию является охватывающая все наборы символов UTF-8, проблемы правильного преобразования для
Неприятность заключается в том, что
Препятствием служат:
Вторая крайне неприятная вещь — невозможно распознать, произошел ли «сбой» распознавания кодировки при парсинге HTML документа.
В обоих указанных случаях текст будет обрабатываться как текст в кодировке ISO-8859-1, затем преобразовываться на основании этого предположения в UTF-8 (при создании DOM дерева), но!
Таким образом мы не имеем возможности распознать, была ли кодировка «сбита» в процессе парсинга HTML.
В силу ряда причин (режим обработки незакрытых тегов, стандартных HTML entities и т.п.)
Что делает указанную неприятность просто таки фатальной => очень большой класс корректных HTML документов не может быть подгружен как DOMDocument объекты.
Работающий workaround:
Так как DOM extension сам по себе суть враппер вокруг libxml, то сказанное будет по большей части верно и для некоторых других DOM имплементаций (основанных на libxml).
1. DOMDocument::$encoding property не влияет ни на что, кроме операции дампа XML документа —
DOMDocument::saveXML()
.То есть совсем не влияет.
Вне зависимости от того, установлен ли этот атрибут в конструкторе:
$dom = new DOMDocument('1.0', 'Windows-1251');
Или же выставлен позже:
$dom = new DOMDocument('1.0');
$dom->encoding = 'Windows-1251';
Внутреннее представление документа (в DOM объекте) — всегда в UTF-8.
2. Соответственно:
DOMDocument::createTextNode()
DOMDocument::createComment()
DOMDocument::createCDATASection()
DOMDocumentFragment::appendXML()
Всегда рассмативают входные данные как UTF-8 текст.
Более того, можно в этом убедиться, подав некорректную UTF-8 строку на вход
DOMDocumentFragment::appendXML()
. Вне зависимости от выставленного значения кодировки результатом будет warning:«DOMDocumentFragment::appendXML(): Entity: line 1: parser error: Input is not proper UTF-8, indicate encoding !»
(надо упомянуть, что для
appendXML()
«indicate encoding» мы никак не можем, на входе требуется именно XML фрагмент, без XML заголовка).Остальные из вышеперечисленных методов добавляют данные «as is», не производя никаких преобразований, и не выдают такого предупреждения. Проблемы возникнут позже (при попытке дампа документа):
«Warning: DOMDocument::saveXML(): output conversion failed due to conv error, ...»
3. При работе с XML деревом
DOMNode::$nodeValue
, DOMText::$wholeText
, DOMCharacterData::$data
и DOMComment::$data
также отдают данные как есть, без каких бы то ни было преобразований. Т.е. в UTF-8 кодировке.Исключение — ситуация, когда мы сами же создали внутренне некорректную DOM структуру, добавляя узлы не в UTF-8 (наши же данные будут возвращены без изменений). Но, как указано выше, такой документ будет не сохранить.
DOMXPath
класс тоже всегда работает в UTF-8 режиме. Это касается и собственно XPath выражений, и полученного по запросу nodeset'а.И значения
DOMNode::$nodeValue
, и (несколько забегая вперед) результат работы DOMDocument::saveXML()
в режиме дампа отдельного узла/ветки всегда отдаются в UTF-8 кодировке.4. Выставленное значение кодировки также никак не влияет и на поведение
DOMDocument::loadXML()
/DOMDocument::loadHTML()
методов.Единственная возможность указать кодировку документа при парсинге — указать её в соответствующих тегах.
Для XML:
<?xml version="1.0" encoding="Windows-1251"?> ...
Для HTML:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=Windows-1251"> ...
При этом значение атрибута
encoding
DOMDocument
объекта будет overloaded соответствующим значением из заголовка.Если кодировка в заголовке XML/HTML документа не указана, то документ парсится как
- UTF-8 для
loadXML()
; - ISO-8859-1 для
loadHTML()
(!!!!) В полном соответствии со стандартом HTTP 1.1 (RFC2068, раздел 3.7.1);
а
DOMDocument::$encoding
выставляется в null
.В силу большей формализованности XML, того, что кодировка XML документа указывается в самом первом теге, а также того, что кодировкой по умолчанию является охватывающая все наборы символов UTF-8, проблемы правильного преобразования для
loadXML()
метода решаются проще, поэтому далее будут рассматриваться только вопросы парсинга HTML.Неприятность заключается в том, что
loadHTML()
не всегда «узнает» корректно выставленный Content-type HTTP-EQUIV мета тег.Препятствием служат:
- Любые non-ASCII символы, встретившиеся до Content-type HTTP-EQUIV мета тега;
- Несоответствие указанной в теге кодировки реальному положению вещей. К примеру, указываем, что
charset=UTF-8
, а сам HTML содержит хотя бы один невалидный UTF-8 «символ».
Вторая крайне неприятная вещь — невозможно распознать, произошел ли «сбой» распознавания кодировки при парсинге HTML документа.
В обоих указанных случаях текст будет обрабатываться как текст в кодировке ISO-8859-1, затем преобразовываться на основании этого предположения в UTF-8 (при создании DOM дерева), но!
DOMDocument::$encoding
будет «честно» соответствовать содержимому Content-type тега (например, Windows-1251).Таким образом мы не имеем возможности распознать, была ли кодировка «сбита» в процессе парсинга HTML.
В силу ряда причин (режим обработки незакрытых тегов, стандартных HTML entities и т.п.)
loadHTML()
метод не может быть заменен loadXML()
(с которым с точки зрения кодировок работать проще).Что делает указанную неприятность просто таки фатальной => очень большой класс корректных HTML документов не может быть подгружен как DOMDocument объекты.
Работающий workaround:
- Вставить дополнительную секцию с соответствующим Content-type HTTP-EQUIV мета тегом непосредственно за открытым тегом .
Опционально, с помощьюiconv()
и постфикса '//IGNORE', добавляемого к названию target кодировки, порвести насильственную конвертацию текста в UTF-8 или в любую другую подходящую кодировку (именно эта кодировка должна быть указана в дополнительной секции).
После парсинга документа удалить служебную секцию:
$dom = DOMDocument::loadHTML($htmlString);
$xpath = new DOMXPath($dom);
$dummyHead = $xpath->query('/html/head')->item(0);
$dummyHead->parentNode->removeChild($dummyHead);
Ограничения:
- Код страницы не должен содержать бинарные данные.
- Страница не должна содержать non-ASCII символов до открывающего тега (там могут встретиться комментарии к странице).
Необходимо самостоятельно распознавать, была ли подана на вход полноценная HTML страница или HTML фрагмент (и оборачивать его ...... тегами).
Как узнать исходную кодировку страницы?
Во-первых, в некоторых случаях исходная кодировка нам заранее известна.
Второй путь — производить парсинг документа дважды. Предварительный проход выдаст нам распознанную кодировку в DOMDocument::$encoding property. Способ хорош при небольшом размере документов (что даст не слишком большие накладные расходы на дополнительный проход).
Универсальный и наиболее правильный путь — производить самостоятельный парсинг начала документа с помощью XMLReader или XML Parser PHP extension'ов (выбор — по собственным предпочтениям). Это также даст возможность параллельно определить, является ли входная строка полноценным HTML или только HTML фрагментом, а также определить наличие комментариев до стартового тега (и, соответственно, вырезать эти комментарии, а затем добавить в уже загруженный документ).
Отмечу некоторые особенности поведенияDOMDocument::loadHTML()
метода:
- Вне зависимости от того, был ли подан на вход полноценный HTML или HTML фрагмент, создаются соответствующаяся обертка: ... (если в соответствии с вышеприведенным алгоритмом мы сделали это сами, то для нас эта особенность становится несущественной).
Все названия тегов автоматически приводятся к lower case.
Алгоритм автозакрытия тегов отличается от режима recovery дляDOMDocument::loadXML()
(а именно, он менее «жаден»).
Проверяется применимость тегов в том или ином контексте. Например, тег не должен встречаться внутри .
DOMDocument::loadHTML()
«знает» о стандартных HTML entities.
5. При операциях дампа —DOMDocument::saveXML()
/DOMDocument::saveHTML()
определение кодировки для формируемого документа происходит следующим образом:
- Дамп всего документа посредством
DOMDocument::saveXML()
всегда производится в кодировке, указанной в DOMDocument::$encoding property. Непопадающие в указанную кодировку символы выводятся как character references (&#XXXX;). Если кодировка не распознана при парсинге или не указана при создании документа (самостоятельно выставить значениеencoding
атрибута в null или какое-либо нераспознаваемое DOM extension'ом значение мы не можем), то весь вывод будет произведен в ASCII, а все non-ASCII символы будут выводиться как character references. - Дамп отдельного узла или ветки документа посредством
DOMDocument::saveXML($node)
всегда производится в 'UTF-8'. - Дамп HTML докумета производится в кодировке, указанной в первом встретившимся
/html/head/meta
http-equiv теге (опять таки, вне зависимости от содержимогоDOMDocument::$encoding
). Важно: поиск тега производится в lower case режиме, т.е. как сами названия тегов html, head, meta, так и название атрибута content должны быть в нижнем регистре. Символы, не попадающие в выбранную кодировку (при отсутствии тега, указывающего кодировку, или же при невалидном с точки зрения libxml названии кодировки — все non-ASCII символы), выводятся как character references (&#XXXX;).
Таким образом мы при необходимости можем заменить кодировку для вывода документа (например, жестко прописать UTF-8):
$dom = DOMDocument::loadHTML($htmlString);
...
$xpath = new DOMXPath($dom);
$metaTags = $xpath->query('/html/head/meta');
// Unfortunately DOMXPath supports only XPath 1.0 and we have to iterate through meta tags
// instead of selecting node using '/html/head/meta[lower-case(@http-equiv)="content-type"]'
for ($count = 0; $count < $metaTags->length; $count++) {
$httpEquivAttribute = $metaTags->item($count)->attributes->getNamedItem('http-equiv');
if ($httpEquivAttribute !== null && strtolower($httpEquivAttribute->value) == 'content-type') {
$fragment = $doc->createDocumentFragment();
$fragment->appendXML('/>');
$metaTags->item($count)->parentNode->replaceChild($fragment, $metaTags->item($count));
break;
}
}
// Do nothing if meta tag is not found
Изменение кодировки вывода документа на самом деле никак не влияет на фактическое его содержание — броузер, интерпретируя полученный документ, осуществляет обратное преобразование (в соответствии с выставленной кодировкой).
- Дамп всего документа посредством
- Вне зависимости от того, был ли подан на вход полноценный HTML или HTML фрагмент, создаются соответствующаяся обертка: ... (если в соответствии с вышеприведенным алгоритмом мы сделали это сами, то для нас эта особенность становится несущественной).