Статьи

Изменение XML как текста с помощью PHP DOM

Недавно мне пришлось провести массовое преобразование HTML- файлов в DITA XML — материал, который я написал для будущей справки по JavaScript Ultimate (третья и, пожалуй, самая сложная часть справочника SitePoint ).

Но проблема, с которой я сталкивался несколько раз, заключалась в явной сложности рекурсивного преобразования элементов — <code> становится <jsvalue> (или одним из дюжины похожих элементов), <a> становится <xref> … и все это достаточно просто; но каждый из этих элементов может содержать другие или дополнительные дочерние элементы, такие как <em> , и по мере того, как мы проходим через DOM, вероятность потенциальной рекурсии увеличивается, пока не дойдет до точки, где мой мозг взрывается.

Есть предел тому, сколько рекурсии я могу обдумать — или, скорее, — предел того, насколько я подготовлен, чтобы обдумать, прежде чем я просто пошлю на это, почему я не могу исказить это как текст с регулярные выражения!?

К сожалению, в PHP DOM нет способа получить текстовый эквивалент любого произвольного узла, но мы можем сделать это на уровне Document или DocumentFragment ; поэтому, немного поиграв, я нашел способ использовать эту возможность и заставить ее работать на уровне Node .

Например, давайте начнем с этого XML :

 <?xml version="1.0" encoding="utf-8"?> <root id="introduction"> <div class="section"> The fundamental data type is <code>Node</code> </div> </root> 

У нас есть ссылка на его DOM, сохраненную в переменной PHP с именем $xmldom . И мы хотим проанализировать его так, чтобы элемент <code> стал <jstype> , а <div class="section"> стал просто <section> , и все это не повлияло на остальную часть документа.

Вот полный код для этого, о котором я расскажу поэтапно:

 $node = $xmldom->documentElement->firstChild; $doc = new DOMDocument(); $doc->loadXML('<xmltext/>'); $node = $doc->importNode($node, true); $doc->documentElement->appendChild($node); $xmltext = ereg_replace('^.*<xmltext>(.*)</xmltext>.*$', '\1', $doc->saveXML()); $xmltext = ereg_replace('<([/]?)code>', '<\1jstype>', $xmltext); $xmltext = ereg_replace('<([/]?)div[^>]*>', '<\1section>', $xmltext); $node = $xmldom->createDocumentFragment(); $node->appendXML($xmltext); $xmldom->documentElement->replaceChild($node, $xmldom->documentElement->firstChild); 

На первом шаге мы получаем ссылку на элемент, с которым мы хотим работать, и сохраняем его в $node .

На втором шаге мы создаем новый документ и используем loadXML() для создания корневого узла-заполнителя ( метод loadXML преобразует ввод текста в XML и является одним из краеугольных камней нашего процесса). Затем мы импортируем исходный узел в этот документ, затем используем saveXML() для преобразования всего документа в текст ( метод saveXML преобразует документ XML в текст, и он так же важен, как loadXML() для того, что мы здесь делаем). Текстовый вывод анализируется с использованием ereg_replace для удаления внешнего содержимого документа (его пролога и корневого узла), так что у нас остается текстовый эквивалент исходного узла ввода.

На третьем этапе мы делаем все, что нужно для текстового искажения; в данном случае это простые преобразования имен элементов, но это может быть что угодно.

На четвертом шаге мы хотим преобразовать наш разобранный текст обратно в XML , и мы делаем это, создавая фрагмент документа, затем используя appendXML() для загрузки текста и его преобразования в XML ( метод appendXML делает то же самое, что и loadXML() , но не требует создания всего документа).

Наконец, на пятом шаге мы объединяем обработанный XML обратно в наш оригинальный документ. Фрагмент документа имеет в качестве владельца исходный документ, поэтому мы можем просто использовать метод replaceChild для замены исходного узла и его дочерних replaceChild обработанной версией. (Всякий раз, когда фрагмент документа добавляется в документ, фактически добавляются только его дочерние элементы, сам фрагмент документа отбрасывается; DocumentFragment является виртуальной конструкцией и фактически никогда не появляется в документе.)

И первый, и последний шаг являются произвольными — мы можем работать со всем документом или только с одним узлом и соответствующим образом редактировать наши ссылки и операторы слияния. Или мы могли бы создать метод из внутренних шагов, который принимает $node в качестве аргумента (и, возможно, массив выражений замены) и возвращает обработанный узел в конце:

 function mangleXML($node) { ... return $node; }