XPath以递归方式删除空DOM节点?

Xeo*_*oss 4 php xpath dom

我试图找到一种方法从HTML源清理一堆空DOM元素,如下所示:

<div class="empty">
    <div>&nbsp;</div>
    <div></div>
</div>
<a href="http://example.com">good</a>
<div>
    <p></p>
</div>
<br>
<img src="http://example.com/logo.png" />
<div></div>
Run Code Online (Sandbox Code Playgroud)

但是,我不想损害有效元素或换行符.所以结果应该是这样的:

<a href="http://example.com">good</a>
<br>
<img src="http://example.com/logo.png" />
Run Code Online (Sandbox Code Playgroud)

到目前为止,我尝试了一些像这样的XPath:

$xpath = new DOMXPath($dom);

//$x = '//*[not(*) and not(normalize-space(.))]';
//$x = '//*[not(text() or node() or self::br)]';
//$x = 'not(normalize-space(.) or self::br)';
$x = '//*[not(text() or node() or self::br)]';

while(($nodeList = $xpath->query($x)) && $nodeList->length > 0) {
    foreach ($nodeList as $node) {
        $node->parentNode->removeChild($node);
    }
}
Run Code Online (Sandbox Code Playgroud)

有人能告诉我正确的XPath来删除那些空的DOM节点吗?(即使是空的,img,br和输入也有用)

当前输出:

<div>
    <div>&nbsp;</div>

</div>
<a href="http://example.com">good</a>
<div>

</div>
<br>
Run Code Online (Sandbox Code Playgroud)

更新

为了澄清,我正在寻找一个XPath查询,它是:

  • 在匹配空节点时递归,直到找到所有节点(包括空节点的父节点)
  • 每次清理后可以多次成功运行(如我的例子所示)

Dim*_*hev 6

I.初步解决方案:

XPath是XML文档的查询语言.因此,对XPath表达式的评估仅选择节点或从XML文档中提取非节点信息,但从不改变XML文档.因此,评估XPath表达式永远不会删除或插入节点 - XML文档保持不变.

你想要的是"从HTML源清理一堆空DOM元素",而不能单独使用XPath.

这是由XPath上最可靠和唯一的官方(我们说的规范)来源证实的--W3C XPath 1.0建议:

" XPath的主要目的是解决一个XML [XML]文档的部分.为了支持这一主要目的,还提供了基本设施为字符串,数字和布尔值的操纵.XPath使用小型的非XML语法,以促进URI和XML中使用XPath属性值.XPath的对XML文档的摘要,逻辑结构进行操作,而不是其表面语法.XPath的其使用的路径的符号如URL的得名通过的层次结构进行导航一个XML文档. "

因此,必须使用一些其他语言与XPath结合才能实现require功能.

XSLT是一种专门用于XML转换的语言.

这是一个基于XSLT的示例 - 一个执行请求清理的简短XSLT转换:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match=
 "*[not(string(translate(., '&#xA0;', '')))
  and
    not(descendant-or-self::*
          [self::img or self::input or self::br])]"/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)

当应用于提供的XML(更正为成为良好的XML文档)时:

<html>
    <div class="empty">
        <div>&#xA0;</div>
        <div></div>
    </div>
    <a href="http://example.com">good</a>
    <div>
        <p></p>
    </div>
    <br />
    <img src="http://example.com/logo.png" />
    <div></div>
</html>
Run Code Online (Sandbox Code Playgroud)

产生了想要的正确结果:

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)

说明:

  1. 身份规则"按原样"复制为其执行选择的每个节点.

  2. 有一个单一的模板,覆盖对任何元素的身份模板(的除外img,inputbr),其字符串值从任何&nbsp;已被去除,是空字符串.此模板的主体为空,有效地"删除"匹配的元素 - 匹配的元素不会复制到输出.


II.更新:

OP澄清他想要一个或多个XPath表达式:

" 每次清理后都可以成功运行多次. "

有趣的是,存在一个XPath表达式,它准确地选择了所有需要删除的节点 - 因此完全避免了"多次清理":

//*[not(normalize-space((translate(., '&#xA0;', ''))))
  and
    not(descendant-or-self::*[self::img or self::input or self::br])
    ]
     [not(ancestor::*
             [count(.| //*[not(normalize-space((translate(., '&#xA0;', ''))))
                         and
                           not(descendant-or-self::*
                                  [self::img or self::input or self::br])
                          ]
                    )
             =
              count(//*[not(normalize-space((translate(., '&#xA0;', ''))))
                      and
                        not(descendant-or-self::*
                                 [self::img or self::input or self::br])
                        ]
                   )
              ]
          )
     ]
Run Code Online (Sandbox Code Playgroud)

基于XSLT的验证:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match=
   "//*[not(normalize-space((translate(., '&#xA0;', ''))))
      and
        not(descendant-or-self::*[self::img or self::input or self::br])
       ]
        [not(ancestor::*
               [count(.| //*[not(normalize-space((translate(., '&#xA0;', ''))))
                           and
                             not(descendant-or-self::*
                                    [self::img or self::input or self::br])
                             ]
                      )
               =
                count(//*[not(normalize-space((translate(., '&#xA0;', ''))))
                        and
                          not(descendant-or-self::*
                                 [self::img or self::input or self::br])
                          ]
                      )
               ]
            )
        ]
 "/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)

当这个转换应用于提供的(并且形成良好的)XML文档(上面)时,除了我们的XPath表达式选择的节点之外,所有节点都"按原样"复制:

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)

说明:

让我们$vAllEmpty根据问题中"空"的定义来表示所有"空"的节点.

$vAllEmpty 用以下XPath表达式表示:

   //*[not(normalize-space((translate(., '&#xA0;', ''))))
     and
       not(descendant-or-self::*
             [self::img or self::input or self::br])

      ]
Run Code Online (Sandbox Code Playgroud)

要删除所有这些内容,我们只需删除"顶级节点" $vAllEmpty

让我们将所有这些"顶级节点"的集合表示为:$vTopEmpty.

$vTopEmpty可以$vAllEmpty使用以下XPath 2.0表达式表示:

$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]
Run Code Online (Sandbox Code Playgroud)

这将选择那些$vAllEmpty没有任何祖先元素的节点$vAllEmpty.

最后一个XPath表达式具有等效的XPath 1.0表达式:

$vAllEmpty[not(ancestor::*[count(.|$vAllEmpty) = count($vAllEmpty)])]
Run Code Online (Sandbox Code Playgroud)

现在,我们用$vAllEmpty上面定义的扩展XPath表达式替换最后一个表达式,这就是我们获取最终表达式的方式,它只选择"要删除的顶级节点":

//*[not(normalize-space((translate(., '&#xA0;', ''))))
  and
    not(descendant-or-self::*[self::img or self::input or self::br])
    ]
     [not(ancestor::*
             [count(.| //*[not(normalize-space((translate(., '&#xA0;', ''))))
                         and
                           not(descendant-or-self::*
                                  [self::img or self::input or self::br])
                          ]
                    )
             =
              count(//*[not(normalize-space((translate(., '&#xA0;', ''))))
                      and
                        not(descendant-or-self::*
                                 [self::img or self::input or self::br])
                        ]
                   )
              ]
          )
     ]
Run Code Online (Sandbox Code Playgroud)

使用变量进行基于XSLT-2.0的简短验证:

<xsl:stylesheet version="2.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
     <xsl:strip-space elements="*"/>

     <xsl:variable name="vAllEmpty" select=
      "//*[not(normalize-space((translate(., '&#xA0;', ''))))
         and
           not(descendant-or-self::*
                 [self::img or self::input or self::br])

          ]"/>

     <xsl:variable name="vTopEmpty" select=
     "$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]"/>

     <xsl:template match="node()|@*">
      <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
     </xsl:template>

     <xsl:template match="*[. intersect $vTopEmpty]"/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)

此转换将"按原样"复制每个节点,但属于任何节点除外$vTopEmpty.结果是正确的和预期的:

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)

III.替代解决方案(可能需要"多次清理"):

另一种方法是不尝试指定要删除的节点,而是指定要保留的节点 - 然后要删除的节点是所有节点和要保留的节点之间的集合差异.

要保留的节点由此XPath表达式选择:

  //node()
    [self::input or self::img or self::br
    or
     self::text()[normalize-space(translate(.,'&#xA0;',''))]
    ]
     /ancestor-or-self::node()
Run Code Online (Sandbox Code Playgroud)

然后要删除的节点是:

  //node()
     [not(count(.
              |
                //node() 
                   [self::input or self::img or self::br
                  or
                    self::text()[normalize-space(translate(.,'&#xA0;',''))]
                   ]
                    /ancestor-or-self::node()
                )
        =
         count(//node()
                  [self::input or self::img or self::br
                 or
                   self::text()[normalize-space(translate(.,'&#xA0;',''))]
                  ]
                   /ancestor-or-self::node()
               )
         )
     ]
Run Code Online (Sandbox Code Playgroud)

但是,请注意,这些都是所有节点删除,而不是只有"顶级节点删除".可以仅表达"要删除的顶级节点",但结果表达式相当复杂.如果有人试图删除要删除的所有节点,则会出现错误,因为"要删除的顶级节点"的后代按文档顺序跟随它们.