XPath contains(text(),'some string') 无法与具有多个文本子节点的节点一起使用。

393

我在使用dom4j的XPath contains时遇到了一个小问题...

假设我的XML如下:

<Home>
    <Addr>
        <Street>ABC</Street>
        <Number>5</Number>
        <Comment>BLAH BLAH BLAH <br/><br/>ABC</Comment>
    </Addr>
</Home>

假设我想要查找根元素下所有文本中包含ABC的节点...

那么我需要编写的XPath是:

//*[contains(text(),'ABC')]

然而,这不是dom4j返回的结果......这是dom4j的问题还是我的XPath理解有误?因为该查询仅返回Street元素而非Comment元素?

DOM将Comment元素作为一个复合元素处理,具有四个标签中的两个。

[Text = 'XYZ'][BR][BR][Text = 'ABC'] 
我会认为查询仍应返回元素,因为它应该找到元素并在其上运行contains,但它没有...以下查询返回元素,但它返回的不仅是元素,还包括父元素,这对问题来说是不可取的。
//*[contains(text(),'ABC')]

请问有人知道XPath查询语句,能够仅返回元素 <Street/><Comment/> 吗?


据我所知,//*[contains(text(),'ABC')] 只返回 <Street> 元素。它不返回任何 <Street><Comment> 的祖先元素。 - Ken Bloom
1
没有一个答案涉及到XPath新版本(2.0及以上版本,从2007年开始)中发现的不同行为,因此我添加了一个更新的答案来解释这种差异。 - kjhughes
7个回答

982
"<Comment>标签包含两个文本节点和两个<br>节点作为其子节点。

您的xpath表达式是

"
//*[contains(text(),'ABC')]

为了解释这段内容,
  1. *是一个选择器,匹配任何元素(即标签)--它返回一个节点集。
  2. []是一个条件语句,作用于该节点集中的每个单独节点。如果它作用的每个单独节点都满足括号内的条件,则匹配成功。
  3. text()是一个选择器,匹配上下文节点的所有文本子节点 -- 它返回一个节点集。
  4. contains是一个操作字符串的函数。如果它传递的是一个节点集,那么节点集会 通过返回节点集中按文档顺序排列的第一个节点的字符串值将其转换为字符串。因此,它只能匹配你的<Comment>元素中的第一个文本节点,即BLAH BLAH BLAH。由于它不匹配,所以你的结果中没有<Comment>

您需要将其更改为

//*[text()[contains(.,'ABC')]]
  1. *是一种选择器,可以匹配任何元素(例如标签),返回一个节点集合。
  2. 外部的[]是一个条件语句,作用于该节点集合中的每个节点,在这里它作用于文档中的每个元素。
  3. text()是一个选择器,可以匹配上下文节点的所有文本子节点,返回一个节点集合。
  4. 内部的[]是一个条件语句,作用于该节点集合中的每个节点,在这里它作用于每个单独的文本节点。每个单独的文本节点都是括号内路径的起点,并且也可以在括号内明确地表示为“。”。如果操作的任何单独节点匹配括号内的条件,则会进行匹配。
  5. contains是一个操作字符串的函数。这里它被传递一个单独的文本节点(.)。因为它被传递到<Comment>标记中的第二个文本节点,所以它会看到'ABC'字符串并能够匹配它。

1
太棒了,我在XPath方面还是有些新手,所以让我理解一下,text()是一个函数,需要使用表达式contains(.,'ABC')。有没有可能请您解释一下,这样我就不会再犯这种愚蠢的错误了;) - Mike Milkin
41
我已经编辑了我的回答,提供了详细的解释。其实我对XPath并不是非常了解——只是通过一些试验偶然发现了这种组合。一旦我找到了有效的组合,我猜测了一下发生了什么,并查阅了 XPath 标准 来确认我的想法和编写解释。 - Ken Bloom
1
我知道这是一个旧的帖子,但有人能否评论一下Ken Bloom给出的答案和//*[contains(., 'ABC')]之间是否存在根本区别,最好附带一些简单的测试用例。我一直使用Mike Milkin给出的模式,认为它更合适,但在当前上下文中只是执行contains似乎更符合我的需求。 - knickum
2
...//*[text()[contains(.,'ABC')]] 的意思是任何一个元素,其中 text()[contains(.,'ABC')]truetext()[contains(.,'ABC')] 是上下文节点的所有文本节点子节点的节点集,其中 contains(.,'ABC')true。由于 text()[contains(.,'ABC')] 是一个节点集,因此它通过 boolean() 函数转换为布尔值。对于一个节点集,boolean() 如果不为空,则返回 true - x-yuri
1
由于此答案得分较高,可能会被广泛查看,请允许我澄清一下,它与XPath 1.0有关,而不是语言的后续版本。从XPath 2.0(于2007年发布)开始,如果存在具有多个文本节点的元素,//*[contains(text(),'ABC')]将抛出一个错误。这个更改是为了防止在此答案中描述的问题。 - Michael Kay
显示剩余10条评论

24

XML文档:

<Home>
    <Addr>
        <Street>ABC</Street>
        <Number>5</Number>
        <Comment>BLAH BLAH BLAH <br/><br/>ABC</Comment>
    </Addr>
</Home>

XPath表达式:
//*[contains(text(), 'ABC')]

//*匹配根节点之外的任何后代元素

[...]是一个谓词, 它过滤节点集合。它返回true的节点:

谓词将节点集合过滤[...]以产生新的节点集合。对于要过滤的节点集中的每个节点,都会评估PredicateExpr[...];如果PredicateExpr对该节点求值为true,则该节点包含在新的节点集合中;否则,它不包含。

contains('haystack', 'needle') 如果haystack包含needle,则返回true

函数:boolean contains(string, string)

如果第一个参数字符串包含第二个参数字符串,则contains函数返回true;否则返回false。

但是contains()函数的第一个参数需要传入一个字符串。而且它接受节点作为参数。为了处理这种情况,每个作为第一个参数传递的节点或节点集都会通过string()函数转换为一个字符串:

如果一个参数被调用字符串函数,则该参数将被转换为字符串类型。

string()函数返回第一个节点string-value

通过返回节点集中按文档顺序排列的第一个节点的字符串值来将节点集转换为字符串。如果节点集为空,则返回空字符串。

元素节点string-value

元素节点的字符串值是元素节点所有后代文本节点的字符串值按文档顺序连接而成的。

文本节点string-value

文本节点的字符串值是字符数据。

因此,基本上string-value是包含在节点中的所有文本(所有后代文本节点的连接)。

text()是一个节点测试,用于匹配任何文本节点:

节点测试text()适用于任何文本节点。例如,child::text()将选择上下文节点的文本节点子项。

说到这一点,//*[contains(text(),'ABC')]匹配包含ABC的第一个文本节点的任何元素(但不包括根节点)。由于text()返回包含上下文节点的所有子文本节点的节点集(相对于表达式评估的位置),但contains()只使用第一个节点。因此,对于上面的文档,该路径匹配Street元素。

以下表达式//*[text()[contains(.,'ABC')]]匹配至少有一个包含ABC的子文本节点的任何元素(但不包括根节点)。.表示上下文节点。在这种情况下,它是除根节点之外任何元素的子文本节点。因此,对于上面的文档,该路径匹配StreetComment元素。

现在,//*[contains(., 'ABC')] 匹配任何包含 ABC (在后代文本节点的连接中)的元素(但不包括根节点)。对于上面的文档,它匹配HomeAddrStreetComment元素。因此,//*[contains(., 'BLAH ABC')] 匹配HomeAddrComment元素。


3
与被接受的答案类似,这个答案仅涉及XPath 1.0。XPath 2.0(发布于2007年)及更高版本的情况是不同的。 - Michael Kay
正确,这个答案适用于XPath 1.0。请查看我现代(2022)答案,该答案解释了XPath 2.0及以后的不同行为。 - kjhughes

19

覆盖XPath 1.0与XPath 2.0+行为的现代答案...

这个XPath,

//*[contains(text(),'ABC')]

使用XPath 1.0和更高版本的XPath(2.0+)有不同的行为。

常见行为

  • //*选择文档中的所有元素。
  • []根据其中所表达的谓词过滤这些元素。
  • contains(string, substring)在谓词中将筛选那些string中包含子字符串substring的元素。

XPath 1.0行为

  • contains(arg1, substring):如果第一个参数求值为节点集contains()将通过获取节点集中的第一个节点的字符串值将节点集转换为字符串。(如果arg1text(),则contains()只考虑所有匹配的text节点中的第一个。)如果您觉得这很奇怪,您并不孤单。
  • 对于//*[contains(text(),'ABC')],该节点集将是文档中每个元素的所有子文本节点。
  • 由于仅使用第一个文本节点子代,违反了测试所有子文本节点是否包含'ABC'子字符串的期望。
  • 这导致对上述转换规则不熟悉的人产生直观上的结果。

XPath 1.0在线示例显示只选择了一个'ABC'

XPath 2.0+行为

  • 将序列中超过一个项目的序列作为第一个参数调用contains(arg1, substring)是错误的。
  • 这纠正了XPath 1.0中所描述的反直觉行为。

XPath 2.0在线示例显示了由于特定于XPath 2.0+的转换错误而引起的典型错误消息。

常见解决方案

  1. 如果您希望包括后代元素(超出子元素),请针对元素的字符串值作为单个字符串进行测试,而不是子文本节点的各个字符串值。使用以下XPath:

    //*[contains(.,'ABC')]
    

    选取目标元素StreetComment,以及它们的祖先元素AddrHome,因为它们的字符串值中也有子串'ABC'

    在线示例显示了选择祖先元素的情况。

  2. 如果您希望排除后代元素(超出子代级别),可以使用此XPath:

    //*[text()[contains(.,'ABC')]]
    

    选择只包含文本节点子元素的字符串值包含'ABC'子串的目标StreetComment,因为只有这些元素符合条件。 对于所有版本的XPath都是如此。

    在线示例仅显示选择了StreetComment


8

[contains(text(),'')]只会返回true或false,它不会返回任何元素结果。


如果我有 ' ' 或 ' ',这个方法就不起作用了,我们该如何去除空格? - shareef
"contains(text(),'JB-')" 不起作用! "contains" 需要 两个字符串 作为参数 - contains(**string**, **string**)! text() 不是字符串,而是一个函数! - AtachiShadow

5
接受的答案将返回全部的父节点,即使字符串在
之后,为了仅获取实际带有ABC的节点,请使用以下代码:
//*[text()[contains(.,'ABC')]]/text()[contains(.,"ABC")]

如果有人想要获取文本节点的父元素,可以在查询后缀中加上 /..,像这样://*[text()[contains(.,'ABC')]]/text()[contains(.,"ABC")]/..。谢谢!@roger - Radical Edward

5
//*[text()='ABC'] 

返回值

<street>ABC</street>
<comment>BLAH BLAH BLAH <br><br>ABC</comment>

6
在对一个已有五个回答的九年老问题进行回答时,非常重要的是指出你的回答针对该问题的哪一方面提供了独特的新观点。 - Jason Aller
2
我发布的答案非常简单。所以想分享一下,希望能帮助像我这样的初学者。 - learningIsFun

2

以下是另一种匹配包含给定文本字符串的节点的方法。首先查询文本节点本身,然后获取其父节点:

//text()[contains(., "ABC")]/..

对我而言,这篇文章易读易懂。

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