前言
很多使用 lxml 做网页爬虫的开发者,都会踩中 position() 相关的经典坑:
- 混淆 XPath 下标从 1开始,和 Python 列表下标从0开始;
- 不清楚
[]作用范围,不加括号直接使用//div[position()=2]导致匹配失败; - 分不清
position()匹配规则:相对于当前同级节点,不是全局序号; - 尝试用
position()配合string()批量提取文本,出现预期外结果。
本文结合真实爬虫场景,完整讲解 position() 语法、案例、常见错误、实用业务代码。
运行环境:Python + lxml.etree
一、基础语法规则
1. position() 核心定义
position() 返回当前节点在同级兄弟节点中的位置序号 ,序号从 1 开始,不存在 position()=0。
语法格式:
节点选择器[position() = N]
简写等价写法(日常最常用):
节点选择器[N]
//p[2] 等价于 //p[position()=2]
2. 关键红线(90%的人踩坑)
//div[@class="box"][2] ❌ 错误理解
(//div[@class="box"])[2] ✅ 全局匹配集合取第二个
原理拆解
//div[@class="box"][2]
含义:在每一个匹配到的div内部,寻找第二个子元素,不是「全局所有匹配元素里的第二个」。
(//div[@class="box"])[2]
先把所有匹配的元素打包成节点集合,再从集合中取第二个元素。
如果你的需求是全局筛选结果按序号取值,必须加括号。
对应 position() 写法:
(//div[@class="box"])[position()=2]
二、分层实战案例
测试HTML样本(沿用你爬虫真实结构)
<td>
<p>Capital Market Day 2015:<br><br> </p>
<p>ANDRITZ GROUP, Wolfgang Leitner</p>
<p>Schuler Group, Stefan Klebert</p>
<p>Achievements and potentials, Martin Habert</p>
</td>
假设当前上下文节点:td_node(对应你代码里的 td_list[0])
案例1:选取第一个p标签
# 两种写法等价
p1 = td_node.xpath('./p[position()=1]')
p1 = td_node.xpath('./p[1]')
# 提取内部全部文本
title = td_node.xpath('string(./p[position()=1])').strip()
案例2:选取第2个及以后所有p标签(业务高频需求)
爬虫经常需要:标题取第一个p,数据列表取剩余全部p
./p[position() > 1]
Python完整代码:
from lxml import etree
html = """
<td>
<p>Capital Market Day 2015:<br><br> </p>
<p>ANDRITZ GROUP, Wolfgang Leitner</p>
<p>Schuler Group, Stefan Klebert</p>
<p>Achievements and potentials, Martin Habert</p>
</td>
"""
tree = etree.HTML(html)
td_node = tree.xpath('//td')[0]
# 标题:第一个p
top_title = td_node.xpath('string(./p[position()=1])').strip()
# 数据列表:position>1 所有p
p_nodes = td_node.xpath('./p[position() > 1]')
data_list = [p.xpath('string()').strip() for p in p_nodes]
print("标题:", top_title)
print("名单列表:", data_list)
输出:
标题: Capital Market Day 2015:
名单列表: ['ANDRITZ GROUP, Wolfgang Leitner', 'Schuler Group, Stefan Klebert', 'Achievements and potentials, Martin Habert']
案例3:全局匹配,取第二个class匹配元素
场景:页面存在多个 <div class="content-placement-wrapper">,取第二个
# 正确写法
(//div[@class="content-placement-wrapper"])[position()=2]
# 简写
(//div[@class="content-placement-wrapper"])[2]
接着在内部查找子元素:
(//div[@class="content-placement-wrapper"])[2]//div[@class="ci-col-1"]
案例4:position() 与 last() 搭配
last() 获取同级节点总数
选取最后一个p:
./p[position() = last()]
选取倒数第二个p:
./p[position() = last()-1]
三、高频错误汇总
错误1:使用 position()=0
./p[0]
./p[position()=0]
❌ XPath序号从1开始,永远匹配不到任何节点,返回空列表。
很多新手把 Python 列表 [0] 的习惯直接带入 XPath,这是最常见bug。
错误2:不加括号,想用序号筛选全局结果
//div[@class="content-placement-wrapper"][2] ❌
解读:找到每个content-placement-wrapper内部第二个子节点,不是全局第二个div。
错误3:试图使用 string() 批量配合position直接返回列表
# 错误写法,无法得到列表
text_list = td_node.xpath('string(./p[position()>1])')
string(节点) 只能接收单个节点,返回字符串;不能批量处理一组节点。
✅ 正确思路:先获取节点列表,再循环调用 string() 提取文本。
错误4:混淆「相对上下文position」和「全局序号」
<div class="wrap">
<p>文本1</p>
</div>
<div class="wrap">
<p>文本2</p>
</div>
//div/p[1]
这里的 position()=1 是每个div内部p的位置,不是页面上所有p全局排序。
四、position() 实用拓展表达式速查表
| XPath表达式 | 含义 |
|---|---|
./p[position()=1] |
当前节点下第一个p |
./p[position()>1] |
当前节点下第2个及以后所有p |
./p[position()<3] |
当前节点下第1、2个p |
./p[position()=2 or position()=3] |
当前节点下第2、3个p |
(//div[@class="box"])[3] |
全局所有匹配div集合中第三个 |
./p[position()=last()] |
同级最后一个p |
五、爬虫通用模板(直接复制使用)
适用于你当前抓取表格 <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 = """你的网页html"""
tree = etree.HTML(html)
td_list = tree.xpath("//td")
for td in td_list:
# 标题:第一个p
title_raw = td.xpath('string(./p[position()=1])')
title = clean_text(title_raw)
# 内容列表:排除第一个p,剩下全部
p_node_list = td.xpath('./p[position() > 1]')
content_list = [clean_text(p.xpath('string()')) for p in p_node_list]
print("标题:", title)
print("内容:", content_list)
六、总结
- XPath
position()起始序号 1 ,杜绝[0]; []筛选作用域很关键:全局筛选集合取值必须外层包裹();position()是同级兄弟节点内排序,不是页面全局排序;- 批量提取文本:先选出节点列表,循环执行
string(),不要试图一条xpath直接返回字符串列表; - 业务中
position() > 1、position() = last()是表格、列表爬虫高频写法。