XPath是处理HTML和XML文档时最常用的查询语言,而在提取文本内容时,初学者最容易混淆的就是text()和.这两种写法。表面上看它们都能拿到文字,实际上返回的类型、覆盖的范围、适用的函数都完全不同。理解它们的差异,是写对爬虫提取规则、避免拿到空结果的关键一步。
text()的本质:只匹配直接子文本节点
首先必须明确一点:text()是XPath中的一个节点测试,它返回的是节点,而不是字符串。更关键的是,它只会选中当前节点直接的文本子节点,不会递归深入到后代标签内部去取文字。这意味着如果一段文字被内嵌的标签包裹,text()就会漏掉这部分内容。
来看一个典型的HTML片段:
<p id="demo">Hello <b>World</b>!</p>
当我们执行//p[@id='demo']/text()时,返回的结果是一个包含两个文本节点的列表:['Hello ', '!']。注意,World这个被<b>标签包裹的文字并不在其中,因为它是<b>元素的子节点,而不是<p>元素的直接文本子节点。
如果改用//p[@id='demo']/b/text(),就能拿到['World']。所以在使用text()时,思路是“精确到具体层级的文本节点”,适合目标结构非常明确、不存在嵌套变化的情况。它的优点是粒度细,缺点是一旦页面结构发生轻微变化,就可能取到残缺内容甚至空列表。
点号的本质:以当前节点为上下文取字符串
.在XPath中代表当前节点本身,它通常不会单独用来提取文本,而是作为参数传递给字符串函数使用,最常见的就是string(.)。与text()最大的不同在于:string()函数会返回当前节点及其所有后代节点拼接后的完整文本内容,返回值是一个字符串,而不是节点列表。
同样针对上面的HTML,执行string(//p[@id='demo'])得到的结果是Hello World!,三段文字被完整地串联起来了。在Python的lxml库中,写法通常是这样的:
from lxml import etree
html = etree.HTML('<p id="demo">Hello <b>World</b>!</p>')
# text() 返回列表,只有直接子文本节点
result1 = html.xpath('//p[@id="demo"]/text()')
print(result1) # ['Hello ', '!']
# string() 返回单个字符串,包含所有后代文本
result2 = html.xpath('string(//p[@id="demo"])')
print(result2) # Hello World!除了string(),还有几个相关函数值得了解。normalize-space(.)在拼接所有后代文本的基础上,还会压缩连续空白并去除首尾空格,处理格式混乱的HTML时非常实用。text-join类似的逻辑在XPath 3.0才有,但在Python生态中一般通过string-join(//text(), '')的老版本写法或在代码层面手动拼接来模拟。
在Scrapy等框架中extract与extract_first的坑
使用Scrapy的时候,很多人会写出response.xpath('//p/text()').extract_first()这样的代码,结果发现取到的内容不完整。原因正是前面说的:text()漏掉了嵌套标签里的文字。正确且稳妥的做法是先定位到目标节点,再用string()或框架提供的方法提取整体文本。
对比如下两种写法:
# 容易漏内容的写法
text = response.xpath('//div[@class="content"]/text()').get()
# 推荐写法:先选中节点,再转成字符串
text = response.xpath('string(//div[@class="content"])').get()
# 等价写法:用 . 选中节点后调用 getall 再拼接
node = response.xpath('//div[@class="content"]')[0]
text = ''.join(node.xpath('.//text()').getall())第三种写法中的.//text()也值得注意:它表示当前节点下所有层级的文本节点,是介于text()和string(.)之间的折中方案。它返回的是列表,方便你在代码中进一步处理每段文字,比如过滤空白、插入分隔符等,灵活性比string()更高。
如何选择:三种写法的对比总结
综合来看,提取文本主要有三种思路,各自的适用场景如下表所示:
| 写法 | 返回类型 | 覆盖范围 | 适用场景 |
|---|---|---|---|
/text() | 节点列表 | 仅直接子文本节点 | 结构固定、文本不在子标签内 |
.//text() | 节点列表 | 所有后代文本节点 | 需要逐段处理文字内容 |
string(.)或normalize-space(.) | 字符串 | 所有后代文本拼接 | 直接要完整句子或整段文字 |