【问题标题】:Convert spaces between PRE tags, via DOM parser通过 DOM 解析器转换 PRE 标签之间的空格
【发布时间】:2011-10-06 16:28:06
【问题描述】:

正则表达式是我作为解决方案的最初想法,尽管很快就发现 DOM 解析器更合适......我想将一串 HTML 文本中的 PRE 标记之间的空格转换为  。例如:

<table atrr="zxzx"><tr>
<td>adfa a   adfadfaf></td><td><br /> dfa  dfa</td>
</tr></table>
<pre class="abc" id="abc">
abc 123
<span class="abc">abc 123</span>
</pre>
<pre>123 123</pre>

into(注意span标签属性中的空格是保留的):

<table atrr="zxzx"><tr>
<td>adfa a   adfadfaf></td><td><br /> dfa  dfa</td>
</tr></table>
<pre class="abc" id="abc">
abc&nbsp;123
<span class="abc">abc&nbsp;123</span>
</pre>
<pre>123 123</pre>

结果需要序列化回字符串格式,以供其他地方使用。

【问题讨论】:

    标签: php html dom html-parsing


    【解决方案1】:

    当您想要插入 &amp;nbsp; 实体而不将 DOM 转换为 &amp;amp; 实体时,这有点棘手,因为实体是节点,而空间只是字符数据。操作方法如下:

    $dom = new DOMDocument;
    $dom->loadHtml($html);
    $xp = new DOMXPath($dom);
    foreach ($xp->query('//text()[ancestor::pre]') as $textNode)
    {
        $remaining = $textNode;
        while (($nextSpace = strpos($remaining->wholeText, ' ')) !== FALSE) {
            $remaining = $remaining->splitText($nextSpace);
            $remaining->nodeValue = substr($remaining->nodeValue, 1);
            $remaining->parentNode->insertBefore(
                $dom->createEntityReference('nbsp'),
                $remaining
            );
        }
    }
    

    获取所有 pre 元素并使用它们的 nodeValues 在这里不起作用,因为 nodeValue 属性将包含所有子元素的 combined DOMText 值,例如它将包括跨度子节点的 nodeValue。在 pre 元素上设置 nodeValue 将删除那些。

    因此,我们不是获取 pre 节点,而是获取在其轴上某处具有 pre 元素父级的所有 DOMText 节点:

    DOMElement pre
        DOMText "abc 123"         <-- picking this
        DOMElement span
           DOMText "abc 123"      <-- and this one
    DOMElement
        DOMText "123 123"         <-- and this one
    

    然后我们遍历每个 DOMText 节点,并将它们拆分为每个空间的单独 DOMText 节点。我们删除空格并在拆分节点之前插入一个 nbsp Entity 节点,所以最后你会得到一个像

    的树
    DOMElement pre
        DOMText "abc"
        DOMEntity nbsp
        DOMText "123"
        DOMElement span
           DOMText "abc"
           DOMEntity nbsp
           DOMText "123"
    DOMElement
        DOMText "123"
        DOMEntity nbsp
        DOMText "123"
    

    因为我们只使用 DOMText 节点,所以任何 DOMElement 都不会被触及,因此它将保留 pre 元素内的 span 元素。

    警告:

    您的 sn-p 无效,因为它没有根元素。使用 loadHTML 时,libxml 会将任何缺失的结构添加到 DOM,这意味着您将获得包含 DOCTYPE、html 和 body 标记的 sn-p。

    如果你想找回原来的 sn-p,你必须getElementsByTagName body 节点并获取所有子节点以获取 innerHTML。不幸的是,there is no innerHTML function or property in PHP's DOM implementation,所以我们必须手动完成:

    $innerHtml = '';
    foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $child) {
        $tmp_doc = new DOMDocument();
        $tmp_doc->appendChild($tmp_doc->importNode($child,true));
        $innerHtml .= $tmp_doc->saveHTML();
    }
    echo $innerHtml;
    

    另见

    【讨论】:

    • 这可能会去掉&lt;pre&gt;标签中的标签,我在之前的回答中遇到了同样的问题。
    【解决方案2】:

    我看到了我之前的答案的不足之处。这是在&lt;pre&gt; 标签内保留标签的解决方法:

    <?php
    $test = file_get_contents('input.html');
    $dom = new DOMDocument('1.0');
    $dom->loadHTML($test);
    $xpath = new DOMXpath($dom);
    $pre = $xpath->query('//pre//text()');
    // manipulate nodes of type XML_TEXT_NODE
    foreach($pre as $e) {
        $e->nodeValue = str_replace(' ', '__REPLACEMELATER__', $e->nodeValue);
        // when you attempt to write &nbsp; in a dom node
        // the & will be converted to &amp; :(
    }
    $temp = $dom->saveHTML();
    $temp = str_replace('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">', '', $temp);
    $temp = str_replace('<html>', '', $temp);
    $temp = str_replace('<body>', '', $temp);
    $temp = str_replace('</body>', '', $temp);
    $temp = str_replace('</html>', '', $temp);
    $temp = str_replace('__REPLACEMELATER__', '&nbsp;', $temp);
    echo $temp;
    ?>
    

    输入

    <p>paragraph 1 remains untouched</p>
    <pre>preformatted 1</pre>
    <div>
        <pre>preformatted 2</pre>
    </div>
    <div>
        <pre>preformatted 3 <span class="foo">span text</span> preformatted 3</pre>
    </div>
    <div>
        <pre>preformatted 4 <span class="foo">span <b class="bla">bold test</b> text</span> preformatted 3</pre>
    </div>
    

    输出

    <p>paragraph 1 remains untouched</p>
    <pre>preformatted&nbsp;1</pre>
    <div>
        <pre>preformatted&nbsp;2</pre>
    </div>
    <div>
        <pre>preformatted&nbsp;3&nbsp;<span class="foo">span&nbsp;text</span>&nbsp;preformatted&nbsp;3</pre>
    </div>
    <div>
        <pre>preformatted&nbsp;4&nbsp;<span class="foo">span&nbsp;<b class="bla">bold&nbsp;test</b>&nbsp;text</span>&nbsp;preformatted&nbsp;3</pre>
    </div>
    

    注意 #1

    DOMDocument::saveHTML() PHP >= 5.3.6 中的方法允许您指定要输出的节点。否则,您可以使用str_replace()preg_replace() 来分隔doctype、html 和body 标签。

    注意 #2

    这个技巧似乎行得通,并减少了一行代码,但我不确定它是否能保证工作:

    $e->nodeValue = utf8_encode(str_replace(' ', "\xA0", $e->nodeValue));
    // dom library will attempt to convert 0xA0 to &nbsp;
    // nodeValue expects utf-8 encoded data but 0xA0 is not valid in this encoding
    // hence replaced string must be utf-8 encoded
    

    【讨论】: