查看: 121|回复: 3

XPath string()函数详解:与text()区别及批量提取文本避

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
在 Python + lxml 的网页解析场景中,XPath 的 string() 函数使用频率很高,但也是踩坑重灾区。不少开发者对它的认知停留在“能拿到标签内部所有文本”,却说不清它和 text() 的本质差异,更不知道它根本无法直接作用于节点集合。本文从一个真实表格抽取场景出发,重新梳理 string() 的语法、边界行为、常见错误和可直接落地的爬虫代码。

一、string() 基础定义

string(node) 接收一个节点作为参数,递归提取该节点下所有层级的文本内容,并将它们合并成一个连续的字符串。换句话说,当前节点的直接文本、子节点(如 <strong>、<span>、<br> 等)内部的文本,都会被拼接进最终结果。

基础语法形式:

string(节点路径)

如果在当前上下文节点内简写,则使用:

string(.)

string(.) 等价于 string(self),表示取当前节点自身的全部文本。

二、string() 与 text() 的核心区别

先看一段测试 HTML:
  1. <td>
  2.   <p><strong>Capital Market Day 2015</strong><br>测试文本</p>
  3. </td>
复制代码

1. 使用 ./p/text()
  1. res = td.xpath('./p/text()')
  2. # 结果:['测试文本']
  3. # <strong> 内部文字丢失
复制代码

text() 只获取当前节点的直接文本子节点,不会深入子标签;如果存在多个文本片段,会返回字符串列表。以上例来说,<p> 下有两个文本节点:<strong> 内部文本属于 <strong> 节点,text() 无法跨层读取,所以只能拿到 <br> 之后的“测试文本”。

2. 使用 string(./p)
  1. res = td.xpath('string(./p)')
  2. # 结果:'Capital Market Day 2015测试文本'
复制代码

string() 会递归遍历节点下所有层级,把 <strong> 内文字、<br> 后文字全部合并成一个字符串。无论多少处文本,最终都只有一个字符串。

直观对比如下:

./p/text():返回类型为 list[str],不能获取嵌套标签文字
string(./p):返回类型为 str,可以获取嵌套标签文字

三、实战分层案例

沿用实际业务中的表格结构:
  1. <td>
  2.   <p>Capital Market Day 2015:</p>
  3.   <p>ANDRITZ GROUP, Wolfgang Leitner</p>
  4.   <p>Schuler Group, Stefan Klebert</p>
  5. </td>
复制代码

上下文节点是 td_node。

案例1:获取第一个 p 标签完整文本
  1. # XPath下标从1开始
  2. top_title = td_node.xpath('string(./p[1])').strip()
复制代码

这种写法可以拿到第一个 p 内包括 <br> 在内的所有文本。注意,XPath 中不存在 0 号索引,如果写成 ./p[0] 会匹配不到节点,string() 返回空字符串。

案例2:string() 不能批量处理一组节点

很多新手会尝试这样写:
  1. # ❌ 错误写法
  2. text_list = td_node.xpath('string(./p)')
  3. print(text_list)
复制代码

这样只会返回第一个 p 的文本,而不是列表。原因在于 string() 只接受单个节点,当 XPath 路径匹配到多个节点时,它只作用于集合中的第一个节点,返回值是单个字符串。

正确做法是:先选出节点列表,再循环调用 string():
  1. p_nodes = td_node.xpath('./p')
  2. text_list = [p.xpath('string()').strip() for p in p_nodes]
复制代码

这里的 p.xpath('string()') 等价于 p.xpath('string(.)'),表示读取当前 p 节点的全部文本。

案例3:提取第2个及以后所有 p 标签文本

这种场景常见于“标题 + 内容列表”的表格结构。使用 position() 过滤掉第一个 p:
  1. p_nodes = td_node.xpath('./p[position() > 1]')
  2. content_list = [p.xpath('string()').strip() for p in p_nodes]
复制代码

这样 content_list 就是第二个及之后所有 p 的完整文本列表。

案例4:string(.) 在循环节点中的使用

当遍历 tr 节点时,常常需要提取每个 tr 下第一个 td 的完整文本:
  1. tr_nodes = tree.xpath('//table/tr')
  2. for tr in tr_nodes:
  3.     title = tr.xpath('string(./td[1])').strip()
  4.     # 处理 title
复制代码

string(.) 配合相对路径,在循环中能够准确拿到当前节点范围内的内容。

四、string() 边界特性

1. 路径匹配不到节点时

如果传入的路径匹配不到任何节点,string() 不会抛异常,而是返回空字符串:
  1. res = td_node.xpath('string(./p[99])')
  2. print(res == "")  # True
复制代码

2. 不间断空格的处理

页面中常见的  会被 string() 原样读入。为了后续清洗,建议统一替换:
  1. import re
  2. def clean_text(raw):
  3.     if not raw:
  4.         return ""
  5.     # 将所有空白(换行、多个空格、不间断空格)统一替换为单个空格
  6.     return re.sub(r"\s+", " ", raw).strip()
复制代码

3. 节点内部无文字时

如果节点内部只有标签、没有文字,string() 返回空字符串。

五、高频错误汇总

错误1:企图用 string() 批量获取多个节点文本
  1. # ❌ 错误
  2. list_data = td.xpath('string(./p[position()>1])')
  3. # ✅ 正确
  4. p_list = td.xpath('./p[position()>1]')
  5. list_data = [clean_text(p.xpath('string()')) for p in p_list]
复制代码

错误2:混淆 text() 与 string(),丢失嵌套文字

当页面中存在 <b>、<strong>、<a> 等嵌套标签时,只用 text() 会丢失内容,应优先使用 string()。

错误3:括号位置产生混淆
  1. # ✅ 取全局第二个 div 的内部文本
  2. string((//div[@class="box"])[2])
  3. # ❌ 语义错误,含义完全不同
  4. string(//div[@class="box"][2])
复制代码

错误4:Python 列表下标与 XPath 下标混用
  1. # XPath 内部下标从 1 开始
  2. td.xpath('string(./p[1])')
  3. # Python 列表取值从 0 开始
  4. p_nodes = td.xpath('./p')
  5. text = p_nodes[0].xpath('string()')
复制代码

不要把 Python 的 [0] 习惯直接写进 XPath 字符串。

六、常用表达式速查表

string(.):获取当前节点所有文本

string(./p[1]):当前节点下第一个 p 的全部文本

string(./p[1]/strong):当前 p 内部 strong 标签的文本

(//div[@class="demo"])[1]/string(.):全局第一个匹配 div 的全部文本

七、可直接复制的完整爬虫模板

下面这个模板适用于抓取表格 <td> 内多段 <p> 文本的场景:
  1. import re
  2. from lxml import etree
  3. def clean_text(raw_text):
  4.     if not raw_text:
  5.         return ""
  6.     return re.sub(r"\s+", " ", raw_text).strip()
  7. html = """
  8. <td>
  9.   <p>Capital Market Day 2015:</p>
  10.   <p>ANDRITZ GROUP, Wolfgang Leitner</p>
  11.   <p>Schuler Group, Stefan Klebert</p>
  12. </td>
  13. """
  14. tree = etree.HTML(html)
  15. td_list = tree.xpath("//td")
  16. for td in td_list:
  17.     # 标题:第一个p文本
  18.     title = clean_text(td.xpath('string(./p[1])'))
  19.     # 内容列表:第2个及之后所有p
  20.     p_nodes = td.xpath('./p[position() > 1]')
  21.     content_list = [clean_text(p.xpath('string()')) for p in p_nodes]
  22.     print("标题:", title)
  23.     print("名单:", content_list)
复制代码

八、总结

string(node) 接收单个节点,递归提取所有层级文本并合并为一段字符串;它无法直接批量作用于节点集合,想要列表必须先选出节点,再循环执行 string();与 text() 的最大区别在于 text() 只取直接文本节点,无法获取嵌套标签内容;XPath 索引从 1 开始,找不到节点时 string() 返回空字符串,不会报错;当页面存在 <strong>、<a>、<br> 等嵌套时,优先选择 string() 而非 text()。
回复

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: XPath string()函数详解:与text()区别及批量提取文本避

楼主的总结非常到位,尤其是“string() 只作用于节点集合中第一个节点”这个坑,我当初也踩过,还以为是 lxml 的 bug。后来用 ` 加 `''.join()` 才彻底绕开。不过楼主的循环写法更直观,而且配合 `position()` 过滤首标题的逻辑也很实用。另外那个 `string(./p[99])` 返回空字符串的边界测试值得记一下,以后写代码能少不少判空。感谢分享!
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: XPath string()函数详解:与text()区别及批量提取文本避

楼主的总结很到位,尤其是“string() 只作用于节点集合中的第一个节点”这个坑,新手几乎必踩。之前我也习惯用 `td.xpath('string()')` 来取当前节点文本,后来发现复杂嵌套时用 ` 配合 `''.join()` 更灵活,但确实不如 `string()` 直接。那楼主处理 `&lt;br&gt;` 时拼接出来的文本有没有额外加分隔符?有时候多个子标签间没空格,清洗起来还挺麻烦。
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: XPath string()函数详解:与text()区别及批量提取文本避

写得很清楚,尤其是 string() 只取第一个节点的坑,新手确实容易踩。我之前愣是用 string(./p) 以为能返回列表,结果调试半天才发现是字符串。后来统一用你这种先取节点列表再循环调 string() 的方式,稳多了。 补充一个小点:如果节点里有 ,string() 拼接出来的文本会连在一起,没有空格。我一般会先替换 为空格再提取,或者清洗时用正则把边界处理一下,不然像“测试文本”和下一句容易黏成一句。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-20 18:55 , Processed in 0.023233 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部