Как вы форматируете структуры DOM в PHP?

#php #html #dom

#php #HTML #dom

Вопрос:

Моим первым предположением были классы PHP DOMпараметром formatOutput). Однако я не могу правильно отформатировать и вывести этот блок HTML. Как вы можете видеть, отступ и выравнивание неверны.

 $html = '
<html>
<body>
<div>

<div>

        <div>

                <p>My Last paragraph</p>
            <div>
                            This is another text block and some other stuff.<br><br>
                Again we will start a new paragraph
                            and some other stuff
                            <br>
        </div>
</div>
        <div>
                        <div>
                            <h1>Another Title</h1>
                                                    </div>
                        <p>Some text again <b>for sure</b></p>
                </div>
</div>
<div>
    <pre><code>
    <span>amp;<htmlamp;></span>
        <span>amp;<headamp;></span>
            <span>amp;<titleamp;></span>
                Page Title
            <span>amp;</titleamp;></span>
            <span>amp;</headamp;></span>
    <span>amp;</htmlamp;></span>
    </code></pre>
</div>
</div>
</body>
</html>';

header('Content-Type: text/plain');
libxml_use_internal_errors(TRUE);

$dom = new DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadHTML($html);
print $dom->saveHTML();
  

Обновление: я добавил в пример предварительно отформатированный блок кода.

Комментарии:

1. Для таких крупных блоков текста, как этот, вы должны использовать HEREDOC вместо многострочной строки.

2. bugs.php.net/bug.php?id=48509

3. @MarcB, если вы посмотрите на редакции post, вы увидите, что сначала это был HEREDOC. Однако markdown не может форматировать строки HEREDOC. Итак, для вашего удобства теперь это многострочная строка. Как будто у кого-то все равно будет длинная строка HTML в их PHP-файле … : P

4. Удалите все отступы и ненужные закрывающие теги. Наличие красивого html с отступами абсолютно бесполезно для конечного пользователя (на самом деле это плохо, потому что вы тратите впустую пропускную способность). Просто проверьте источник в Google или Facebook.

5. При вызове loadHTML вы также должны использовать LIBXML_NOERROR | LIBXML_NOWARNING флаги, чтобы избежать заполнения стека ошибок и потребления вашей оперативной памяти. Либо это, либо вызовите libxml_clear_errors() после.

Ответ №1:

Вот некоторые улучшения по сравнению с ответом @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 ошибок и установке флагов подавления), если вы передаете пустую (или пустую) строку load*() методам.

Регулярное выражение

Регулярное выражение />s*</im не имеет большого смысла, его лучше использовать ~>[[:space:]] <~m также для перехвата v (вертикальные вкладки) и замены только в том случае, если пробелы действительно существуют ( вместо * ), не возвращая ( ) — что быстрее — и отбрасывать накладные расходы без учета регистра (поскольку пробелы не имеют регистра).

Вы также можете захотеть нормализовать новые строки n и другие управляющие символы (особенно, если происхождение HTML неизвестно), поскольку a r вернется amp;#23; saveXML() , например, как after .

DOMDocument::$preserveWhitespace бесполезно и не нужно после выполнения приведенного выше регулярного выражения.

О, и я не вижу необходимости защищать здесь пустые теги pre-like. Фрагменты, содержащие только пробелы, бесполезны.

Дополнительные флаги для loadHTML()

  • LIBXML_COMPACT — «это может ускорить работу вашего приложения без необходимости изменения кода»
  • LIBXML_NOBLANKS нужно запустить больше тестов на этом
  • LIBXML_NOCDATA нужно запустить больше тестов на этом
  • LIBXML_NOXMLDECL — документировано, но не реализовано = (

ОБНОВЛЕНИЕ: установка любого из этих параметров приведет к тому, что выходные данные не будут форматироваться.

На 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 теперь имеет две основные проблемы:

  1. нет DOCTYPE (он был удален при использовании $dom->documentElement )
  2. пустые теги теперь являются встроенными тегами, что означает, что один <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 всегда в нижнем регистре. ОБНОВЛЕНИЕ: теги всегда в нижнем регистре.

On <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) amp;amp; ($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;
}
  

Комментарии:

1. Просто интересно, касается ли ваш ответ XML или HTML или обоих? Вы пишете об обработке XML-файлов и последующем преобразовании их в HTML? Так ли это должно быть сделано?

Ответ №2:

Вот комментарий на php.net : http://ru2.php.net/manual/en/domdocument.save.php#88630

Похоже, что когда вы загружаете HTML из строки (как вы это делали), DOMDocument становится ленивым и ничего в нем не форматирует.

Вот рабочее решение вашей проблемы:

 // Clean your HTML by hand first
$html = preg_replace('/>s*</im', '><', $html);
$dom = new DOMDocument;
$dom->loadHTML($html);
$dom->formatOutput = true;
$dom->preserveWhitespace = false;
// Use saveXML(), not saveHTML()
print $dom->saveXML();
  

По сути, вы удаляете пробелы между тегами и используете saveXML() вместо saveHTML() .
saveHTML() просто не работает в этой ситуации. Однако вы получаете XML-объявление в первой строке текста.

Комментарии:

1. Это хорошее начало, использование вывода XML действительно помогает. Однако это уничтожает отступ, используемый в предварительно отформатированном блоке кода. Тем не менее, возможно, я могу просто изменить ваше регулярное выражение, чтобы не включать <span> теги, которые обычно размещаются вокруг кода в <pre> блоках. Тем не менее, я хотел бы знать, почему библиотека PHP DOM не работает. Редактировать: Да, '/>s*<(?!span)/i' кажется, работает нормально.

2. @Xeoncross: Дэвид, >s*< регулярное выражение должно обрезаться только в том случае, если блок кода pre-like полностью состоит из символов-интервалов. Если это так, я не понимаю, зачем вам нужно сохранять избыточное пустое пространство. Чего мне здесь не хватает?

3. Почему formatOutput больше не saveHTML() работает? Работает ли это с файлами?