author: 专注Python实战,分享爬虫与数据分析干货
title: Python爬虫实战⑦|XPath精准定位,复杂网页数据提取利器
update: 2026-04-26
tags: Python,爬虫,XPath,lxml,数据提取,精准定位

作者:专注Python实战,分享爬虫与数据分析干货
更新时间:2026年4月
适合人群:已掌握BS4、想用XPath提升提取效率的爬虫开发者


前言:CSS选择器不够用?试试XPath

前面我们用BeautifulSoup的find/find_all/select提取数据,很好用。但遇到这些情况就吃力了——

  • 提取"某个div下面第二个p标签的文本"
  • 选择"包含特定文字的标签"
  • 获取"父标签的属性"
  • 按位置、按条件组合筛选

XPath = XML Path Language,比CSS选择器强大10倍的数据定位语言。 在lxml库的加持下,速度还比BS4快5-10倍。


一、lxml安装与基础

1.1 安装lxml

pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple

1.2 创建XPath解析对象

from lxml import etree

# 方式1:从字符串创建
html = """
<div class="container">
    <h1 id="title">文章标题</h1>
    <ul class="list">
        <li class="item">项目1</li>
        <li class="item active">项目2</li>
        <li class="item">项目3</li>
    </ul>
    <p class="desc">描述文本 <a href="/link">链接</a></p>
</div>
"""
# etree.HTML() 自动修复不规范的HTML
tree = etree.HTML(html)

# 方式2:从文件创建
# tree = etree.parse("page.html")

# 方式3:从requests响应创建
import requests
response = requests.get("https://example.com")
tree = etree.HTML(response.text)

二、XPath语法详解

2.1 基础路径表达式

表达式 说明 示例
/ 从根节点选取 /html/body/div
// 从任意位置选取 //div
. 当前节点 ./p
父节点 …/div
@ 选取属性 //a/@href
* 任意元素 //div/*
@* 任意属性 //a/@*

2.2 路径选择实战

from lxml import etree

html = """
<html>
<body>
    <div id="main">
        <h1>标题</h1>
        <ul>
            <li>项目1</li>
            <li>项目2</li>
            <li>项目3</li>
        </ul>
        <a href="/page1" class="link">链接1</a>
        <a href="/page2">链接2</a>
    </div>
</body>
</html>
"""
tree = etree.HTML(html)

# 绝对路径
h1 = tree.xpath("/html/body/div/h1/text()")
print("绝对路径:", h1)  # ['标题']

# 相对路径(从任意位置找)
lis = tree.xpath("//li/text()")
print("所有li:", lis)  # ['项目1', '项目2', '项目3']

# 获取属性
hrefs = tree.xpath("//a/@href")
print("所有链接:", hrefs)  # ['/page1', '/page2']

# 获取class属性
classes = tree.xpath("//a/@class")
print("a的class:", classes)  # ['link']

2.3 谓语(条件筛选)

谓语用方括号 [] 表示,用于添加筛选条件:

from lxml import etree

html = """
<div>
    <ul id="nav">
        <li class="item">首页</li>
        <li class="item active">产品</li>
        <li class="item">关于</li>
        <li class="item last">联系</li>
    </ul>
    <div class="content">
        <p>段落1</p>
        <p>段落2</p>
        <p>段落3</p>
    </div>
    <a href="/link1">链接1</a>
    <a href="/link2">链接2</a>
    <a href="/link3" target="_blank">链接3</a>
</div>
"""
tree = etree.HTML(html)

# 按索引选择(XPath索引从1开始!)
first_li = tree.xpath("//li[1]/text()")
print("第1个li:", first_li)  # ['首页']

last_li = tree.xpath("//li[last()]/text()")
print("最后1个li:", last_li)  # ['联系']

second_li = tree.xpath("//li[2]/text()")
print("第2个li:", second_li)  # ['产品']

# 倒数第2个
second_last = tree.xpath("//li[last()-1]/text()")
print("倒数第2个li:", second_last)  # ['关于']

# 前2个
first_two = tree.xpath("//li[position()<=2]/text()")
print("前2个li:", first_two)  # ['首页', '产品']

# 按属性选择
active = tree.xpath("//li[@class='item active']/text()")
print("active项:", active)  # ['产品']

# 有target属性的a标签
blank_links = tree.xpath("//a[@target]/@href")
print("新窗口链接:", blank_links)  # ['/link3']

# 属性包含某个值(contains)
items_with_item = tree.xpath("//li[contains(@class, 'item')]/text()")
print("含item的li:", items_with_item)  # ['首页', '产品', '关于', '联系']

2.4 文本匹配

# 文本包含特定内容
results = tree.xpath("//li[contains(text(), '产')]/text()")
print("包含'产'的li:", results)  # ['产品']

# 文本以特定内容开头
results = tree.xpath("//li[starts-with(text(), '首')]/text()")
print("以'首'开头的li:", results)  # ['首页']

# 完全匹配文本
results = tree.xpath("//li[text()='关于']/text()")
print("文本是'关于'的li:", results)  # ['关于']

三、XPath高级技巧

3.1 逻辑运算

# and — 同时满足多个条件
result = tree.xpath("//li[contains(@class, 'item') and contains(@class, 'active')]/text()")
print("item且active:", result)

# or — 满足任一条件
result = tree.xpath("//li[contains(@class, 'active') or contains(@class, 'last')]/text()")
print("active或last:", result)

# not — 排除
result = tree.xpath("//li[not(contains(@class, 'active'))]/text()")
print("非active的li:", result)

3.2 轴(Axes)——沿关系导航

from lxml import etree

html = """
<div class="container">
    <div class="header">
        <h1>标题</h1>
        <p class="subtitle">副标题</p>
    </div>
    <div class="body">
        <p>段落1</p>
        <p>段落2</p>
        <p>段落3</p>
    </div>
    <div class="footer">
        <a href="/about">关于</a>
        <a href="/contact">联系</a>
    </div>
</div>
"""
tree = etree.HTML(html)

# child — 子节点(默认)
children = tree.xpath("//div[@class='header']/child::*")
print(f"header的子标签: {len(children)}个")

# parent — 父节点
parent = tree.xpath("//h1/parent::div/@class")
print(f"h1的父div的class: {parent}")

# following-sibling — 后面的兄弟节点
siblings = tree.xpath("//div[@class='header']/following-sibling::div/@class")
print(f"header后面的兄弟div: {siblings}")

# preceding-sibling — 前面的兄弟节点
siblings = tree.xpath("//div[@class='footer']/preceding-sibling::div/@class")
print(f"footer前面的兄弟div: {siblings}")

# ancestor — 所有祖先
ancestors = tree.xpath("//h1/ancestor::div/@class")
print(f"h1的所有祖先div: {ancestors}")

# descendant — 所有后代
desc = tree.xpath("//div[@class='container']/descendant::p/text()")
print(f"container下所有p: {desc}")

3.3 获取标签和属性的完整信息

# 获取标签名
tag_name = tree.xpath("//div[@class='header']/child::*[1]/name()")
# 注意:lxml不支持name()函数,用替代方案

# 获取元素对象(不加/text()或/@attr就返回元素)
elements = tree.xpath("//li[contains(@class, 'item')]")
for elem in elements:
    print(f"标签: {elem.tag}")
    print(f"文本: {elem.text}")
    print(f"属性: {elem.attrib}")
    print(f"内部HTML: {etree.tostring(elem, encoding='unicode')}")
    print()

四、XPath vs CSS选择器对比

功能 CSS选择器 XPath
按标签 div //div
按class .item //*[@class=‘item’]
按id #main //*[@id=‘main’]
后代 div p //div//p
直接子 div > p //div/p
按属性 a[href] //a[@href]
文本包含 ❌ 不支持 //*[contains(text(),‘关键词’)]
按位置 :nth-child(2) [2]
父节点 ❌ 不支持 /…
逻辑组合 有限 and/or/not
模糊匹配 ^= $= *= contains/starts-with

结论:XPath更强大,CSS选择器更简洁。日常用XPath,简单场景用CSS。


五、实战案例:用XPath抓取豆瓣电影

import requests
from lxml import etree
import time
import random
import csv

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Referer": "https://movie.douban.com/",
}

all_movies = []

for page in range(2):  # 演示抓2页
    start = page * 25
    url = f"https://movie.douban.com/top250?start={start}"
    print(f"抓取第{page+1}页...")

    response = requests.get(url, headers=headers, timeout=15)
    response.encoding = "utf-8"
    tree = etree.HTML(response.text)

    # 用XPath提取数据
    items = tree.xpath("//ol[@class='grid_view']/li")

    for item in items:
        # 排名
        rank = item.xpath(".//em/text()")[0] if item.xpath(".//em/text()") else ""

        # 电影名(可能有多个title span,取第一个)
        title_list = item.xpath(".//span[@class='title']/text()")
        title = title_list[0] if title_list else ""

        # 其他名称
        other_names = title_list[1:] if len(title_list) > 1 else []

        # 评分
        rating = item.xpath(".//span[@class='rating_num']/text()")[0].strip() if item.xpath(".//span[@class='rating_num']/text()") else ""

        # 评价人数
        people = item.xpath(".//div[@class='star']/span[4]/text()")[0] if item.xpath(".//div[@class='star']/span[4]/text()") else ""

        # 短评
        quote = item.xpath(".//span[@class='inq']/text()")[0] if item.xpath(".//span[@class='inq']/text()") else ""

        # 详情链接
        link = item.xpath(".//div[@class='hd']/a/@href")[0] if item.xpath(".//div[@class='hd']/a/@href")") else ""

        all_movies.append([rank, title, rating, people, quote, link])

    print(f"  获取 {len(items)} 部")
    time.sleep(random.uniform(1, 3))

# 保存CSV
with open("豆瓣Top250_XPath版.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["排名", "电影", "评分", "评价人数", "短评", "链接"])
    writer.writerows(all_movies)

print(f"\n共 {len(all_movies)} 部电影,已保存到CSV")
for m in all_movies[:5]:
    print(f"  {m[0]:>3}. {m[1]:<15} {m[2]}  {m[4]}")

六、XPath调试技巧

6.1 在浏览器中测试XPath

Chrome开发者工具 → Console → 输入:

// 测试XPath
$x("//div[@class='item']")

// 测试并获取文本
$x("//div[@class='item']/text()")

// 测试并获取属性
$x("//a/@href")

6.2 XPath Helper插件

安装Chrome插件 “XPath Helper”:

  • Ctrl+Shift+X 打开
  • 输入XPath表达式
  • 实时高亮匹配结果

6.3 常见XPath错误排查

# 问题1:返回空列表
result = tree.xpath("//div[@class='item']/text()")
# 排查:class可能有多个值,用contains
result = tree.xpath("//div[contains(@class, 'item')]/text()")

# 问题2:编码问题
result = tree.xpath("//p[contains(text(), '中文')]/text()")
# 排查:确保HTML字符串是正确的编码

# 问题3:命名空间问题
# 有些XML有命名空间,需要处理
html = '<root xmlns:ns="http://example.com"><ns:item>数据</ns:item></root>'
tree = etree.HTML(html)
# 方法1:忽略命名空间
result = tree.xpath("//*[local-name()='item']/text()")
# 方法2:注册命名空间
# 较复杂,实际爬虫中HTML很少有命名空间

七、知识卡

XPath表达式 说明
//div 所有div标签
//div/p div下直接子p
//div//p div下所有后代p
//div[@class=‘item’] class等于item的div
//div[contains(@class,‘item’)] class包含item的div
//div[@id=‘main’]/p[2] id=main的div下第2个p
//div/p[last()] 最后一个p
//div/p[position()❤️] 前两个p
//a/@href a标签的href属性
//p/text() p标签的文本
//*[contains(text(),‘关键词’)] 文本包含关键词的任意标签
//li/… li的父节点
and/or/not 逻辑运算

八、课后作业

必做题:

  1. 用lxml+XPath重写第1篇的豆瓣电影爬虫
  2. 练习5种不同的XPath谓语条件
  3. 在浏览器Console中用$x()测试XPath表达式

选做题:

  1. 对比同一页面用BS4和XPath提取数据的速度差异
  2. 用XPath轴(axes)提取指定元素的兄弟节点数据

完成作业的同学,把运行截图发到评论区!


XPath = 爬虫数据提取的终极武器。 比CSS选择器更强大,比BS4更快速,是专业爬虫工程师的标配技能。

本篇要点:

  • XPath语法(路径、谓语、函数、轴)
  • lxml库的安装和使用
  • XPath vs CSS选择器对比
  • 实战:XPath版豆瓣爬虫
  • 浏览器调试技巧

下一篇学习Selenium——模拟真实浏览器,JS渲染页面一把抓。

收藏 + 关注,专栏更新不迷路!

有问题欢迎评论区留言,大家一起讨论!


标签:Python | XPath | lxml | 数据提取 | 精准定位 | 爬虫进阶

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐