在 Python + lxml 的网页解析场景中,XPath 的 string() 函数使用频率很高,但也是踩坑重灾区。不少开发者对它的认知停留在“能拿到标签内部所有文本”,却说不清它和 text() 的本质差异,更不知道它根本无法直接作用于节点集合。本文从一个真实表格抽取场景出发,重新梳理 string() 的语法、边界行为、常见错误和可直接落地的爬虫代码。
一、string() 基础定义
string(node) 接收一个节点作为参数,递归提取该节点下所有层级的文本内容,并将它们合并成一个连续的字符串。换句话说,当前节点的直接文本、子节点(如 <strong>、<span>、<br> 等)内部的文本,都会被拼接进最终结果。
基础语法形式:
string(节点路径)
如果在当前上下文节点内简写,则使用:
string(.)
string(.) 等价于 string(self),表示取当前节点自身的全部文本。
二、string() 与 text() 的核心区别
先看一段测试 HTML:
- <td>
- <p><strong>Capital Market Day 2015</strong><br>测试文本</p>
- </td>
复制代码
1. 使用 ./p/text()
- res = td.xpath('./p/text()')
- # 结果:['测试文本']
- # <strong> 内部文字丢失
复制代码
text() 只获取当前节点的直接文本子节点,不会深入子标签;如果存在多个文本片段,会返回字符串列表。以上例来说,<p> 下有两个文本节点:<strong> 内部文本属于 <strong> 节点,text() 无法跨层读取,所以只能拿到 <br> 之后的“测试文本”。
2. 使用 string(./p)
- res = td.xpath('string(./p)')
- # 结果:'Capital Market Day 2015测试文本'
复制代码
string() 会递归遍历节点下所有层级,把 <strong> 内文字、<br> 后文字全部合并成一个字符串。无论多少处文本,最终都只有一个字符串。
直观对比如下:
./p/text():返回类型为 list[str],不能获取嵌套标签文字
string(./p):返回类型为 str,可以获取嵌套标签文字
三、实战分层案例
沿用实际业务中的表格结构:
- <td>
- <p>Capital Market Day 2015:</p>
- <p>ANDRITZ GROUP, Wolfgang Leitner</p>
- <p>Schuler Group, Stefan Klebert</p>
- </td>
复制代码
上下文节点是 td_node。
案例1:获取第一个 p 标签完整文本
- # XPath下标从1开始
- top_title = td_node.xpath('string(./p[1])').strip()
复制代码
这种写法可以拿到第一个 p 内包括 <br> 在内的所有文本。注意,XPath 中不存在 0 号索引,如果写成 ./p[0] 会匹配不到节点,string() 返回空字符串。
案例2:string() 不能批量处理一组节点
很多新手会尝试这样写:
- # ❌ 错误写法
- text_list = td_node.xpath('string(./p)')
- print(text_list)
复制代码
这样只会返回第一个 p 的文本,而不是列表。原因在于 string() 只接受单个节点,当 XPath 路径匹配到多个节点时,它只作用于集合中的第一个节点,返回值是单个字符串。
正确做法是:先选出节点列表,再循环调用 string():
- p_nodes = td_node.xpath('./p')
- text_list = [p.xpath('string()').strip() for p in p_nodes]
复制代码
这里的 p.xpath('string()') 等价于 p.xpath('string(.)'),表示读取当前 p 节点的全部文本。
案例3:提取第2个及以后所有 p 标签文本
这种场景常见于“标题 + 内容列表”的表格结构。使用 position() 过滤掉第一个 p:
- p_nodes = td_node.xpath('./p[position() > 1]')
- content_list = [p.xpath('string()').strip() for p in p_nodes]
复制代码
这样 content_list 就是第二个及之后所有 p 的完整文本列表。
案例4:string(.) 在循环节点中的使用
当遍历 tr 节点时,常常需要提取每个 tr 下第一个 td 的完整文本:
- tr_nodes = tree.xpath('//table/tr')
- for tr in tr_nodes:
- title = tr.xpath('string(./td[1])').strip()
- # 处理 title
复制代码
string(.) 配合相对路径,在循环中能够准确拿到当前节点范围内的内容。
四、string() 边界特性
1. 路径匹配不到节点时
如果传入的路径匹配不到任何节点,string() 不会抛异常,而是返回空字符串:
- res = td_node.xpath('string(./p[99])')
- print(res == "") # True
复制代码
2. 不间断空格的处理
页面中常见的 会被 string() 原样读入。为了后续清洗,建议统一替换:
- import re
- def clean_text(raw):
- if not raw:
- return ""
- # 将所有空白(换行、多个空格、不间断空格)统一替换为单个空格
- return re.sub(r"\s+", " ", raw).strip()
复制代码
3. 节点内部无文字时
如果节点内部只有标签、没有文字,string() 返回空字符串。
五、高频错误汇总
错误1:企图用 string() 批量获取多个节点文本
- # ❌ 错误
- list_data = td.xpath('string(./p[position()>1])')
- # ✅ 正确
- p_list = td.xpath('./p[position()>1]')
- list_data = [clean_text(p.xpath('string()')) for p in p_list]
复制代码
错误2:混淆 text() 与 string(),丢失嵌套文字
当页面中存在 <b>、<strong>、<a> 等嵌套标签时,只用 text() 会丢失内容,应优先使用 string()。
错误3:括号位置产生混淆
- # ✅ 取全局第二个 div 的内部文本
- string((//div[@class="box"])[2])
- # ❌ 语义错误,含义完全不同
- string(//div[@class="box"][2])
复制代码
错误4:Python 列表下标与 XPath 下标混用
- # XPath 内部下标从 1 开始
- td.xpath('string(./p[1])')
- # Python 列表取值从 0 开始
- p_nodes = td.xpath('./p')
- 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> 文本的场景:
- import re
- from lxml import etree
- def clean_text(raw_text):
- if not raw_text:
- return ""
- return re.sub(r"\s+", " ", raw_text).strip()
- html = """
- <td>
- <p>Capital Market Day 2015:</p>
- <p>ANDRITZ GROUP, Wolfgang Leitner</p>
- <p>Schuler Group, Stefan Klebert</p>
- </td>
- """
- tree = etree.HTML(html)
- td_list = tree.xpath("//td")
- for td in td_list:
- # 标题:第一个p文本
- title = clean_text(td.xpath('string(./p[1])'))
- # 内容列表:第2个及之后所有p
- p_nodes = td.xpath('./p[position() > 1]')
- content_list = [clean_text(p.xpath('string()')) for p in p_nodes]
- print("标题:", title)
- print("名单:", content_list)
复制代码
八、总结
string(node) 接收单个节点,递归提取所有层级文本并合并为一段字符串;它无法直接批量作用于节点集合,想要列表必须先选出节点,再循环执行 string();与 text() 的最大区别在于 text() 只取直接文本节点,无法获取嵌套标签内容;XPath 索引从 1 开始,找不到节点时 string() 返回空字符串,不会报错;当页面存在 <strong>、<a>、<br> 等嵌套时,优先选择 string() 而非 text()。 |