XPath选择前面的元素与可选的插入空白文本节点

时间:2012-12-26 19:54:09

标签: ruby xpath nokogiri

给定一个元素作为上下文我想选择前面的兄弟元素并检查它是否有特定的名称。需要注意的是,如果存在具有非空白内容的插入文本节点,我不想选择它。

例如,给定这个XML文档......

<r>
  <a>a1</a><a>a2</a>
   b
  <a>a3</a>
    <a>a4</a>
  <b/>
  <a>a5</a>
</r>

...然后:

  • 对于“a1”,应该没有匹配(紧跟在它之前没有<a>兄弟元素)
  • 对于“a2”,则应匹配“a1”(没有插入文本节点)
  • 对于“a3”,应该没有匹配(有一个插入的文本节点包含非空白内容)
  • 对于“a4”,则应匹配“a3”(插入的文本节点仅为空格)
  • 对于“a5”,不应该匹配(前一个兄弟元素不是<a>)。

我可以查看前面的兄弟是<a> preceding-sibling::*[1][name()="a"]

然而,我无法弄清楚如何说“选择以下兄弟节点,无论元素或文本,看看是不是文本还是normalize-space(.)=""。我最好的猜测是:

preceding-sibling::*[1][name()="a"][following-sibling::node()[1][not(text()) or normalize-space(.)=""]]

......但似乎没有效果。


这是我的测试Ruby文件:

require 'nokogiri'

xpath = 'preceding-sibling::*[1][name()="a"][following-sibling::node()[1][not(text()) or normalize-space(.)=""]]'
fragment = Nokogiri::XML.fragment '<a>a1</a><a>a2</a> b <a>a3</a> <a>a4</a> <b/> <a>a5</a>'    

fragment.css('a').each{ |a| p [a.text,a.xpath(xpath).to_s] }
#=> ["a1", ""]
#=> ["a2", ""]
#=> ["a3", "<a>a2</a>"]
#=> ["a4", "<a>a3</a>"]
#=> ["a5", ""]

“a2”和“a3”的结果是错误的,让我感到困惑。它正确地找到了前面的<a>,但是没有正确地验证第一个后续兄弟是不是文本(应该允许“a2”找到“a1”)或者它只是空白(哪个)应该防止“a3”找到“a2”。


编辑:这是我写的XPath,以及我打算做的事情:

  • preceding-sibling::*[1][name()="a"]… - 找到前面的第一个元素,并确保它是<a>这似乎符合要求。

    • [following-sibling::node()[1][…]] - 确保第一个后续节点(找到的前一个<a>)符合某些条件

      • not(text()) or normalize-space(.)="" - 确保此后续节点不是文本节点,或者其规范化空间为空

1 个答案:

答案 0 :(得分:5)

使用

/*/a/preceding-sibling::node()
       [not(self::text()[not(normalize-space())])]
            [1]
              [self::a]

基于XSLT的验证:

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

 <xsl:template match="/">
     <xsl:copy-of select=
       "/*/a
          /preceding-sibling::node()
                      [not(self::text()[not(normalize-space())])]
                                        [1]
                                         [self::a]
    "/>
 </xsl:template>
</xsl:stylesheet>

在提供的XML文档上应用此转换时:

<r>
  <a>a1</a><a>a2</a>
   b
  <a>a3</a>
    <a>a4</a>
  <b/>
  <a>a5</a>
</r>

评估XPath表达式,并将此评估选择的节点复制到输出

<a>a1</a>
<a>a3</a>

<强>更新

问题中的XPath表达式有什么问题?

问题在于

[not(text()) or normalize-space(.)='']

这测试上下文节点是否没有文本节点 child

但OP希望测试上下文节点文本节点。

<强>解决方案

将上述内容替换为

[not(self::text()) or normalize-space(.)='']

基于XSLT的验证

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

 <xsl:template match="/*/a">
     <xsl:copy-of select=
     "preceding-sibling::*[1]
                      [name()='a']
                         [following-sibling::node()[1]
                                    [not(self::text()) or normalize-space(.)='']
                       ]"/>
 </xsl:template>
 <xsl:template match="text()"/>
</xsl:stylesheet>

现在,此转换产生了完全想要的结果:

<a>a1</a>
<a>a3</a>