Вот некоторые улучшения по сравнению с ответом @hijarian:
Ошибки LibXML
Если вы не вызовете libxml_use_internal_errors(true)
, PHP выведет все найденные ошибки HTML.Однако, если вы вызовете эту функцию, ошибки не будут подавлены, вместо этого они попадут в кучу, которую вы можете проверить, вызвав libxml_get_errors()
.Проблема в том, что он ест память, а DOMDocument, как известно, очень требователен.Если вы обрабатываете много файлов в пакетном режиме, вам в конечном итоге не хватит памяти.Для этого есть два решения:
if (libxml_use_internal_errors(true) === true)
{
libxml_clear_errors();
}
Поскольку libxml_use_internal_errors(true)
возвращает предыдущее значение этого параметра (по умолчанию false
), это приводит к очистке только ошибок, если вы запускаете его более одного раза (как при пакетной обработке).
Другой вариант - передать флаги LIBXML_NOERROR | LIBXML_NOWARNING
методу loadHTML()
.К сожалению, по причинам, которые мне неизвестны, это все же оставляет пару ошибок позади.
Учтите, что DOMDocument всегда будет выводить ошибку (даже при использовании внутренних ошибок libxml
и установке флагов подавления)если вы передаете пустую (или blankish ) строку в методы load*()
.
Regex
Регулярное выражение />\s*</im
не имеет большого смысла, лучше использовать ~>[[:space:]]++<~m
, чтобы также поймать \v
(вертикальные табуляции) и заменять, только если пробелы действительно существуют (+
вместо *
) без возврата (++
) - что быстрее - иотбрасывать накладные расходы без учета регистра (поскольку пробел не имеет регистра).
Возможно, вы также захотите нормализовать символы новой строки до \n
и других управляющих символов (особенно, если происхождение HTML неизвестно), поскольку \r
вернется как 
после saveXML()
, например.
DOMDocument::$preserveWhitespace
бесполезно и не нужно после выполнения приведенного выше регулярного выражения.
О, и я не вижу необходимостидля защиты бланка предварительноАйк теги здесь.Фрагменты только для пробелов бесполезны.
Дополнительные Флаги для loadHTML()
LIBXML_COMPACT
- "это может ускорить работу вашего приложения без необходимости изменятькод " LIBXML_NOBLANKS
- необходимо выполнить больше тестов на этом LIBXML_NOCDATA
- необходимо запустить больше тестов на этом LIBXML_NOXMLDECL
- задокументировано, но не реализовано = (
ОБНОВЛЕНИЕ: Установка любого из этих параметров приведет к отсутствию форматированияoutput.
On saveXML()
Метод DOMDocument::saveXML()
выведет декларацию XML. Нам нужно вручную удалить ее (поскольку LIBXML_NOXMLDECL
не реализовано). Для этого, мы могли бы использовать комбинацию substr() + strpos()
для поиска первого разрыва строки или даже использовать регулярное выражение для его очистки.
Другой вариант, который, кажется, имеет дополнительное преимущество :просто сделав:
$dom->saveXML($dom->documentElement);
Другое дело, если у вас есть встроенные теги пустые, такие как b
, i
или li
в:
<b class="carret"></b>
<i class="icon-dashboard"></i> Dashboard
<li class="divider"></li>
Метод saveXML()
будет серьезно искажать их (помещая следующий элемент в пустой), портя весь ваш HTML.У Tidy также есть похожая проблема, за исключением того, что он просто отбрасывает узел.
Чтобы исправить это, вы можете использовать флаг LIBXML_NOEMPTYTAG
вместе с saveXML()
:
$dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG);
Эта опцияпреобразует пустые (то есть самозакрывающиеся) теги во встроенные теги, а также разрешает использовать пустые встроенные теги.
Исправление HTML [5]
Со всем, что мы делали до сих пор, наш вывод HTMLтеперь есть две основные проблемы:
- без DOCTYPE (он был удален, когда мы использовали
$dom->documentElement
) - пустые теги теперь являются встроенными тегами, то есть один
<br />
превращается в два (<br></br>
) и т. Д.
Исправить первый довольно легко, поскольку HTML5 довольно разрешительный:
"<!DOCTYPE html>\n" . $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG);
Чтобы вернуть наши пустые теги, а именно:
area
base
basefont
( устарело в HTML5 ) br
col
command
embed
frame
( устарело в HTML5 ) hr
img
input
keygen
link
meta
param
source
track
wbr
Мы можем использовать str_[i]replace
в цикле:
foreach (explode('|', 'area|base|basefont|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr') as $tag)
{
$html = str_ireplace('>/<' . $tag . '>', ' />', $html);
}
Или регулярное выражение:
$html = preg_replace('~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>\b~i', '/>', $html);
Это дорогостоящая операция,Я не тестировал их, поэтому не могу сказать, какая из них лучше, но я бы предположил preg_replace()
.Кроме того, я не уверен, нужна ли версия без учета регистра.У меня сложилось впечатление, что теги XML всегда в нижнем регистре. ОБНОВЛЕНИЕ: Теги всегда в нижнем регистре.
Вкл. <script>
и <style>
Теги
Эти теги всегда будут иметь свое содержимое (если оно существует), инкапсулированное в (без комментариев)Блоки CDATA, которые, вероятно, нарушат их значение.Вам придется заменить эти токены регулярным выражением.
Реализация
function DOM_Tidy($html)
{
$dom = new \DOMDocument();
if (libxml_use_internal_errors(true) === true)
{
libxml_clear_errors();
}
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
$html = preg_replace(array('~\R~u', '~>[[:space:]]++<~m'), array("\n", '><'), $html);
if ((empty($html) !== true) && ($dom->loadHTML($html) === true))
{
$dom->formatOutput = true;
if (($html = $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG)) !== false)
{
$regex = array
(
'~' . preg_quote('<![CDATA[', '~') . '~' => '',
'~' . preg_quote(']]>', '~') . '~' => '',
'~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>~' => ' />',
);
return '<!DOCTYPE html>' . "\n" . preg_replace(array_keys($regex), $regex, $html);
}
}
return false;
}