XPath选择前导元素及可选的中间仅包含空格的文本节点。

3
在给定元素作为上下文的情况下,我想选择前一个兄弟元素并检查它是否具有特定名称。但是需要注意的是,如果存在具有非空白内容的插入文本节点,则不希望选择该节点。
例如,给定此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>)。

我可以使用 preceding-sibling::*[1][name()="a"] 来检查之前的兄弟元素是否为 <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(.)="" - 确保此后续节点不是文本节点,或者其规范化空格为空

我知道可以使用Ruby + XPath的组合来实现这一点,但我正在尝试寻找仅XPath的解决方案。 - Phrogz
你确实问了些难题。 :-) - the Tin Man
一个问题在这里:not(text()) or normalize-space(.)="" 必须是:not(self::text()) or normalize-space(.)="" - Dimitre Novatchev
1个回答

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(.)='']

这个测试是用来检查上下文节点是否有一个文本节点子节点

但是,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>

完全可以(或者说,对于我的需求来说,跟在“/ * / a /”后面的部分是可以的,因为我已经选择了元素)。你能解释一下我的XPath实际上做了什么,而不是我想要的吗? - Phrogz
@Phrogz,我不理解你的XPath表达式,因此我无法解释它的作用。如果你在问题中提供XPath表达式的每个部分的解释,那么我可能能够指出逻辑上的错误所在。 - Dimitre Novatchev
谢谢,我已经编辑了问题的结尾,以澄清我写的XPath的意图。 - Phrogz
@Phrogz,我更新了答案,并分析了您的XPath表达式——问题已经被发现并提出了更正建议。 - Dimitre Novatchev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接