4

我正在尝试找到一种方法来从 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>

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

<a href="http://example.com">good</a>
<br>
<img src="http://example.com/logo.png" />

到目前为止,我已经尝试了一些这样的 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);
    }
}

有人可以告诉我正确的 XPath 来删除空的 DOM 节点,如果为空则无用?(img、br 和 input 即使为空也有用)

电流输出:

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

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

</div>
<br>

更新

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

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

4 回答 4

7

一、初步解决方案:

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

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>

当应用于提供的 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>

产生了想要的正确结果

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>

说明

  1. 身份规则“按原样”复制选择执行它的每个节点。

  2. 有一个模板,覆盖任何元素的标识模板(除了img,inputbr),其中任何&nbsp;已删除的字符串值是空字符串。这个模板的主体是空的,它有效地“删除”了匹配的元素——匹配的元素不会被复制到输出中。


二、更新

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])
                        ]
                   )
              ]
          )
     ]

基于 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>

当此转换应用于提供的(并且格式正确的)XML 文档(上图)时,所有节点都“按原样”复制,但我们的 XPath 表达式选择的节点除外

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>

说明

$vAllEmpty根据问题中“空”的定义,让我们用所有“空”的节点来表示。

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

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

      ]

要删除所有这些,我们只需要从$vAllEmpty

让我们将所有此类“顶级节点”的集合表示为:$vTopEmpty

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

$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]

这会从中选择那些$vAllEmpty没有任何祖先元素的节点$vAllEmpty

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

$vAllEmpty[not(ancestor::*[count(.|$vAllEmpty) = count($vAllEmpty)])]

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

//*[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])
                        ]
                   )
              ]
          )
     ]

使用变量的基于 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>

此转换“按原样”复制每个节点,但属于 的任何节点除外$vTopEmpty。结果是正确且预期的结果:

<html>
   <a href="http://example.com">good</a>
   <br/>
   <img src="http://example.com/logo.png"/>
</html>

三、替代解决方案(可能需要“多次清理”)

另一种方法不是尝试指定要删除的节点,而是指定要保留的节点——那么要删除的节点就是所有节点与要保留的节点之间的集合差。

通过这个 XPath 表达式选择要保留的节点

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

然后要删除的节点是

  //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()
               )
         )
     ]

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

于 2012-08-03T03:45:54.277 回答
2

所以你想要文本节点,<br><img>,以及它们的祖先?

//br您可以使用and获取所有 br 和 img //img

您可以使用 获取所有文本节点,使用 获取//text()所有非空文本节点//text()[normalize-space()]。(尽管如果您的 xml 解析器尚未这样做,您可能需要//text()[normalize-space(translate(., '&nbsp;', ''))]过滤文本节点之类的东西)&nbsp;

你可以让所有的父母都拥有ancestor-or-self::*.

所以得到的表达式是

//br/ancestor-or-self::* | //img/ancestor-or-self::* | //text()[normalize-space()]/ancestor-or-self::*

在 XPath 2 中更短:

(//br | //img | //text()[normalize-space()])/ancestor-or-self::*
于 2012-08-03T17:43:26.953 回答
1

您是否尝试过与此类似的 XPath?

*[not(*) and not(text()[normalize-space()])]

  • not(*)= 没有子元素
  • text()[normalize-space()]= 包含非空白文本的节点(不与此相反)
于 2012-08-02T17:26:31.190 回答
1

实现所需结果的最简单方法是在文本中使用正则表达式。备注:你必须多次使用这个表达式,因为它不是贪婪的,它只删除最低的空子节点,所以要删除所有空节点,我们必须多次调用正则表达式。

这是解决方案:

<?
$text = '<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>';

// recursive function
function recreplace($text)
{
    $restext = preg_replace("/<div(.*)?>((\s|&nbsp;)*|(\s|&nbsp;)*<p>(\s|&nbsp;)*<\/p>(\s|&nbsp;)*)*<\/div>/U", '', $text);
    if ($text != $restext) 
    {
        recreplace($restext);
    }
    else
    {
        return $restext;
    }
}

print recreplace($text);
?>

此代码打印您想要的结果。如果您需要可以编辑正则表达式,则可以在其中添加任何其他应计为空的标签(as <p> </p>)。

对于给定的示例,此函数将结果调用自身两次,第三次调用没有任何替换 - 这将是结果。

于 2012-08-03T06:36:55.717 回答