Python爬虫教程:XPath解析技术详解

1. XPath简介

XPath(XML Path Language)是一种用于在XML和HTML文档中查找信息的语言。它最初是为XML设计的,但同样适用于HTML文档解析。

核心特点

  • 强大的路径选择表达式
  • 提供100+内置函数用于字符串、数值、时间处理
  • W3C标准(1999年成为标准)
  • 广泛应用于XSLT、XPointer等XML处理技术

2. 环境准备

安装lxml库

pip install lxml

验证安装

from lxml import etree
print(etree.__version__)

3. 基本用法

解析HTML文档

from lxml import etree

html = """
<html>
    <body>
        <div>
            <ul>
                <li class="item-0"><a href="link1.html">first item</a></li>
                <li class="item-1"><a href="link2.html">second item</a></li>
                <li class="item-inactive"><a href="link3.html">third item</a></li>
                <li class="item-1"><a href="link4.html">fourth item</a></li>
                <li class="item-0"><a href="link5.html">fifth item</a></li>
            </ul>
        </div>
    </body>
</html>
"""

parser = etree.HTMLParser()
tree = etree.fromstring(html, parser)
result = etree.tostring(tree, pretty_print=True, method="html")
print(result.decode('utf-8'))

从文件加载

tree = etree.parse('test.html', etree.HTMLParser())
result = etree.tostring(tree)
print(result.decode('utf-8'))

4. 节点选择

选择所有节点

nodes = tree.xpath('//*')
for node in nodes:
    print(node.tag)

选择特定节点

# 选择所有li节点
li_nodes = tree.xpath('//li')
print(len(li_nodes))  # 输出: 5

# 选择所有a节点
a_nodes = tree.xpath('//a')
print(len(a_nodes))  # 输出: 5

5. 层级关系

子节点选择

# 直接子节点
a_nodes = tree.xpath('//li/a')
print(len(a_nodes))  # 输出: 5

# 子孙节点
all_nodes = tree.xpath('//ul//*')
print(len(all_nodes))  # 输出: 10 (5 li + 5 a)

父节点选择

# 使用..
parent = tree.xpath('//a[@href="link4.html"]/../@class')
print(parent)  # 输出: ['item-1']

# 使用parent::
parent = tree.xpath('//a[@href="link4.html"]/parent::*/@class')
print(parent)  # 输出: ['item-1']

6. 属性处理

属性匹配

# 匹配class为item-0的li节点
items = tree.xpath('//li[@class="item-0"]')
print(len(items))  # 输出: 2

属性获取

# 获取所有a节点的href属性
links = tree.xpath('//li/a/@href')
print(links)  # 输出: ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

多值属性匹配

html = """
<li class="li li-first"><a href="link.html">first item</a></li>
"""
tree = etree.fromstring(html)

# 错误方式
result = tree.xpath('//li[@class="li"]')
print(result)  # 输出: []

# 正确方式
result = tree.xpath('//li[contains(@class, "li")]')
print(result)  # 输出: [<Element li at 0x10a399288>]

多属性匹配

html = """
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
"""
tree = etree.fromstring(html)

result = tree.xpath('//li[contains(@class, "li") and @name="item"]')
print(result)  # 输出: [<Element li at 0x10a399288>]

7. 文本处理

获取节点文本

# 直接获取文本
text = tree.xpath('//li/a/text()')
print(text)  # 输出: ['first item', 'second item', 'third item', 'fourth item', 'fifth item']

# 获取所有子孙节点文本
all_text = tree.xpath('//li//text()')
print(all_text)  # 输出包含换行符在内的所有文本

8. 高级选择

按序选择

# 第一个li节点
first = tree.xpath('//li[1]/a/text()')
print(first)  # 输出: ['first item']

# 最后一个li节点
last = tree.xpath('//li[last()]/a/text()')
print(last)  # 输出: ['fifth item']

# 位置小于3的节点
position = tree.xpath('//li[position()<3]/a/text()')
print(position)  # 输出: ['first item', 'second item']

# 倒数第三个节点
last3 = tree.xpath('//li[last()-2]/a/text()')
print(last3)  # 输出: ['third item']

节点轴选择

# 祖先节点
ancestors = tree.xpath('//li[1]/ancestor::*')
print([a.tag for a in ancestors])  # 输出: ['html', 'body', 'div', 'ul']

# 属性值
attributes = tree.xpath('//li[1]/attribute::*')
print(attributes)  # 输出: ['item-0']

# 子节点
children = tree.xpath('//li[1]/child::a[@href="link1.html"]')
print(children)  # 输出: [<Element a at 0x10a399288>]

# 后代节点
descendants = tree.xpath('//ul/descendant::a')
print(len(descendants))  # 输出: 5

# 后续节点
following = tree.xpath('//li[1]/following::*[2]')
print(following[0].tag)  # 输出: li

# 同级后续节点
following_siblings = tree.xpath('//li[1]/following-sibling::*')
print(len(following_siblings))  # 输出: 4

9. 实际案例

案例1:获取豆瓣电影Top250

import requests
from lxml import etree

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

response = requests.get(url, headers=headers)
tree = etree.HTML(response.text)

# 获取所有电影标题
titles = tree.xpath('//div[@class="hd"]/a/span[1]/text()')
print(titles[:5])  # 输出前5个电影标题

案例2:电商网站商品信息抓取

# 假设我们有一个电商网站的商品列表页
html = """
<div class="product-list">
    <div class="product">
        <h3><a href="/product/1">商品1</a></h3>
        <span class="price">¥99.00</span>
        <span class="sales">已售1000件</span>
    </div>
    <div class="product">
        <h3><a href="/product/2">商品2</a></h3>
        <span class="price">¥199.00</span>
        <span class="sales">已售500件</span>
    </div>
</div>
"""

tree = etree.fromstring(html)

# 获取所有商品信息
products = []
for product in tree.xpath('//div[@class="product"]'):
    name = product.xpath('.//h3/a/text()')[0]
    price = product.xpath('.//span[@class="price"]/text()')[0]
    sales = product.xpath('.//span[@class="sales"]/text()')[0]
    products.append({
        'name': name,
        'price': price,
        'sales': sales
    })

print(products)

10. 性能优化技巧

  1. 尽量使用具体路径//div[@id="content"]//*[@id="content"]更快
  2. 减少使用//:从根节点开始的搜索效率较低
  3. 使用谓语提前过滤//div[@class="product"]比先获取所有div再过滤更快
  4. 考虑使用CSS选择器:在某些情况下,CSS选择器性能更好

11. 常见问题解决

问题1:编码问题

# 处理非UTF-8编码的页面
response = requests.get(url)
response.encoding = response.apparent_encoding  # 自动检测编码
tree = etree.HTML(response.text)

问题2:动态加载内容

对于JavaScript动态加载的内容,XPath无法直接获取,需要配合Selenium或分析API请求。

问题3:XPath表达式调试

使用浏览器开发者工具可以方便地测试XPath表达式:

  1. Chrome: 按F12 → Console → 输入$x('//your/xpath')
  2. Firefox: 按F12 → Console → 输入$x('//your/xpath')

12. 扩展资源

  1. W3C XPath标准
  2. XPath教程
  3. lxml官方文档
  4. XPath函数参考

通过本教程,您应该已经掌握了XPath在Python爬虫中的核心用法。XPath功能强大且灵活,是每个爬虫工程师必备的技能之一。