第45课:Python|爬虫进阶【正则爬虫、XPath、lxml网页解析高阶实战】

在这里插入图片描述


📖 开篇导读

在上一节课中,我们学习了使用 Requests 和 BeautifulSoup 抓取静态网页的基本方法。但面对复杂的网页结构、糟糕的HTML代码或海量数据时,BeautifulSoup 的解析效率有时会显得力不从心。此外,很多网页的数据并不是直接写在 HTML 中,而是通过 AJAX 动态加载的。要应对这些挑战,我们需要更强大的工具和技巧。

本课是爬虫进阶级别的内容。我们将学习两种更高效、更灵活的解析方式:正则表达式XPath。配合lxml库,解析速度比 BeautifulSoup 提升数倍,且 XPath 语法简洁强大。此外,我们还会介绍如何分析 AJAX 请求,直接抓取后端返回的 JSON 数据,绕过复杂的页面解析。

💡 工作场景

  • 海量数据爬取:lxml 解析速度比纯 Python 的解析器快得多,适合高并发场合。
  • 结构清晰但复杂:当 HTML 层级很深时,XPath 能一行定位元素。
  • 动态内容:通过 Chrome 开发者工具分析网络请求,直接抓取 API 接口。
  • 数据提取:正则表达式在提取特定格式文本(如邮箱、手机号、URL)时极其高效。

学完本课,你将能够独立分析并抓取各类复杂网站数据,包括动态加载的内容,并具备应对初级反爬虫的能力。


🎯 学习目标

目标编号具体掌握内容对应面试/工作价值
1️⃣熟练使用正则表达式提取网页中的特定模式数据(链接、邮箱、价格等)提取非结构化文本
2️⃣掌握 XPath 语法,能够在 lxml 中快速定位元素高效解析 HTML/XML
3️⃣理解 lxml 库的安装和使用,对比 BeautifulSoup 的优势性能优化
4️⃣能够分析网页 AJAX 请求,直接抓取 JSON 数据绕过页面解析
5️⃣了解如何保持会话(Session)和模拟登录(登录过程)爬取需认证的数据
6️⃣综合运用上述技术完成一个复杂爬虫案例独立解决实际问题

🔥 面试考点:“XPath 和 CSS 选择器有何区别?”“如何抓取 AJAX 动态加载的数据?”“lxml 比 BeautifulSoup 快在哪里?为什么?”“简述正则表达式在爬虫中的使用场景。”


📚 知识点理论精讲

一、正则表达式在爬虫中的高效使用

正则表达式(Regular Expression)擅长从字符串中提取符合特定格式的子串。在爬虫中,它非常适合:

  • 从 HTML 文本中直接提取邮箱、电话号码、身份证号等。
  • 清理文本(去除多余空白、HTML 标签)。
  • 从 JavaScript 变量中提取数据(当数据嵌在 script 标签中时)。

1.1 Python re 模块回顾

import re

text = "My email is john@example.com and tom@test.com."
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
emails = re.findall(pattern, text)
print(emails)

1.2 贪婪与非贪婪

爬虫中经常需要匹配 HTML 标签内的内容,建议使用非贪婪 .*?

html = '<div class="title">Python教程</div><div>其他</div>'
match = re.search(r'<div class="title">(.*?)</div>', html)
print(match.group(1))  # Python教程

1.3 常用正则示例

目标正则表达式
邮箱[\w\.-]+@[\w\.-]+\.\w+
手机号(中国)1[3-9]\d{9}
IP 地址\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
URLhttps?://[^\s]+
数字(整数/小数)\d+(?:\.\d+)?

💡 工作应用:清理从网页中提取的文本时,经常用 re.sub(r'<[^>]+>', '', html) 去除 HTML 标签。


二、XPath 语法与 lxml 库

XPath(XML Path Language)是一种在 XML/HTML 文档中查找信息的语言。它比 CSS 选择器更强大、更灵活,可以沿父子、祖先、兄弟关系导航,还能使用函数。

2.1 安装 lxml

pip install lxml

2.2 使用 lxml 解析 HTML

from lxml import etree

html = '<html><body><div id="content">Hello</div></body></html>'
# 创建解析器
parser = etree.HTMLParser()
tree = etree.parse(StringIO(html), parser)   # 从字符串解析
# 或者直接使用 etree.HTML()
root = etree.HTML(html)

2.3 XPath 基本语法

表达式描述
/从根节点选取
//从任意位置选取
.当前节点
..父节点
@选取属性
*通配符
text()获取文本
常用示例
# 选取所有 div 元素
root.xpath('//div')

# 选取所有 class 为 "title" 的元素
root.xpath('//*[@class="title"]')

# 选取 id 为 "main" 的 div 下的所有 a 标签
root.xpath('//div[@id="main"]//a')

# 获取 a 标签的 href 属性
root.xpath('//a/@href')

# 获取文本内容(不包括子标签)
root.xpath('//h1/text()')

# 选取包含特定文本的节点
root.xpath('//a[contains(text(), "下一页")]')

2.4 谓语(Predicate)

谓语用来过滤节点,写在方括号中。

# 第一个 a 标签
root.xpath('(//a)[1]')
# 最后一个 a 标签
root.xpath('(//a)[last()]')
# 位置小于3的 a 标签
root.xpath('(//a)[position()<3]')
# 有 href 属性的 a 标签
root.xpath('//a[@href]')
# 属性值为特定值
root.xpath('//input[@name="username"]')

2.5 轴(Axis)

可以沿着父子、祖先、兄弟等关系移动。

# 选取当前节点的所有子节点
root.xpath('//div/child::*')
# 选取父节点
root.xpath('//a/parent::*')
# 选取祖先节点
root.xpath('//a/ancestor::div')
# 选取前面的兄弟
root.xpath('//a/preceding-sibling::div')

2.6 函数

XPath 有很多内置函数:contains()starts-with()substring()count()position() 等。

# 包含
root.xpath('//div[contains(@class, "article")]')
# 以某字符串开头
root.xpath('//a[starts-with(@href, "/topic/")]')
# 统计某节点下子节点数量
root.xpath('count(//div[@id="list"]/ul/li)')

2.7 lxml 与 BeautifulSoup 性能对比

  • BeautifulSoup:纯 Python,易用但较慢。
  • lxml:底层 C 实现,解析速度极快,支持 XPath。

⚠️ 高频坑点:lxml 对不规范的 HTML 有较强容错能力,但有时会补全或添加 htmlbody 标签,导致 XPath 预期不一致。建议使用 etree.HTML() 解析,然后直接写 XPath。


三、处理动态加载内容(Ajax)

很多现代网站通过 JavaScript 异步加载数据,HTML 源码中并没有这些数据。这时需要分析网络请求,直接请求后端 API。

3.1 分析 XHR 请求步骤

  1. 打开浏览器开发者工具(F12)→ Network → XHR 标签。
  2. 刷新页面,观察加载数据的请求。
  3. 找到返回 JSON 数据的请求,复制 URL 和请求参数。
  4. 用 Python 模拟该请求,直接拿到结构化数据。

3.2 示例:爬取 GitHub API

import requests

url = 'https://api.github.com/repos/python/cpython/stargazers'
params = {'per_page': 5}
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url, params=params, headers=headers)
data = resp.json()
for user in data:
    print(user['login'])

3.3 携带 Cookie 和 Referer

某些 API 需要验证身份,可以使用 Session 保持登录状态。

session = requests.Session()
session.headers.update({'User-Agent': 'Mozilla/5.0'})
# 首先登录(post 用户名密码)
login_resp = session.post('https://example.com/login', data={...})
# 然后访问需要登录的 API
data = session.get('https://example.com/api/data').json()

💻 代码案例实操

案例1:使用正则提取网页中的所有邮箱

"""
regex_emails.py
从本地 HTML 文件或任意文本中提取邮箱地址
"""

import re
import requests

def extract_emails(text):
    pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
    return re.findall(pattern, text)

# 示例:从百度首页提取(可能没有邮箱,换个例子)
url = 'https://www.sina.com.cn/'
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url, headers=headers)
emails = extract_emails(resp.text)
print(f"找到 {len(emails)} 个邮箱:")
for e in emails[:10]:
    print(e)

案例2:XPath 提取某新闻网站标题列表

"""
xpath_news.py
使用 lxml 和 XPath 提取新闻标题和链接
"""

import requests
from lxml import etree

url = 'https://news.sina.com.cn/'
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url)
resp.encoding = 'utf-8'
tree = etree.HTML(resp.text)

# 观察新浪新闻首页结构,假设标题在 <a> 标签内,且 class 包含"news-title"
# 实际需根据网站调整 XPath,此处做示例
titles = tree.xpath('//a[contains(@href, "doc-")]//text()')
links = tree.xpath('//a[contains(@href, "doc-")]/@href')

print("新闻标题与链接:")
for title, link in zip(titles[:10], links[:10]):
    print(f"{title.strip()}\n{link}\n")

案例3:使用 XPath 爬取豆瓣电影 Top250(对比 BeautifulSoup)

"""
xpath_douban.py
用 lxml 与 XPath 重写豆瓣 Top250 爬虫,展示速度优势
"""

import requests
from lxml import etree
import time

def fetch_page(start):
    url = f'https://movie.douban.com/top250?start={start}'
    headers = {'User-Agent': 'Mozilla/5.0'}
    resp = requests.get(url, headers=headers)
    resp.encoding = 'utf-8'
    return resp.text

def parse_with_xpath(html):
    tree = etree.HTML(html)
    movies = []
    items = tree.xpath('//div[@class="item"]')
    for item in items:
        title = item.xpath('.//span[@class="title"]/text()')[0]
        rating = item.xpath('.//span[@class="rating_num"]/text()')[0]
        people = item.xpath('.//div[@class="star"]/span[4]/text()')[0].replace('人评价', '')
        movies.append((title, rating, int(people)))
    return movies

start_time = time.time()
all_movies = []
for start in range(0, 250, 25):
    html = fetch_page(start)
    movies = parse_with_xpath(html)
    all_movies.extend(movies)
print(f"lxml+XPath 耗时: {time.time() - start_time:.2f}秒, 共{len(all_movies)}条")

案例4:分析 XHR 请求抓取动态数据(微博热搜为例)

"""
weibo_hot_search.py
通过分析 API 获取微博热搜榜(示例接口)
"""

import requests

# 实际微博热搜接口可能需要 token,这里用一个公开的示例
url = 'https://weibo.com/ajax/side/hotSearch'
headers = {
    'User-Agent': 'Mozilla/5.0',
    'Referer': 'https://weibo.com/'
}
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
    data = resp.json()
    for idx, item in enumerate(data.get('data', {}).get('realtime', [])[:20], 1):
        title = item.get('word')
        print(f"{idx}. {title}")
else:
    print("请求失败", resp.status_code)

案例5:使用 Session 保持登录状态(模拟 GitHub 登录)

"""
github_login_session.py
演示如何用 Session 保持登录,访问需要登录的页面
"""

import requests

session = requests.Session()

# 1. 先访问登录页获取 authenticity_token(模拟)
login_url = 'https://github.com/login'
session.headers.update({'User-Agent': 'Mozilla/5.0'})
login_page = session.get(login_url)
from lxml import etree
tree = etree.HTML(login_page.text)
token = tree.xpath('//input[@name="authenticity_token"]/@value')[0]

# 2. 提交登录表单
post_data = {
    'commit': 'Sign in',
    'authenticity_token': token,
    'login': 'your_username',
    'password': 'your_password'
}
session.post('https://github.com/session', data=post_data)

# 3. 访问需要登录的页面(如个人仓库列表)
profile_url = 'https://github.com/settings/repositories'
resp = session.get(profile_url)
print(resp.status_code)   # 200 表示已登录

案例6:lxml 与 XPath 提取嵌套结构的商品信息

"""
xpath_nested_products.py
模拟电商网站的商品列表抓取
"""

html = """
<div class="product">
    <h3>商品A</h3>
    <span class="price">¥199</span>
    <div class="reviews">好评率98%</div>
</div>
<div class="product">
    <h3>商品B</h3>
    <span class="price">¥299</span>
    <div class="reviews">好评率95%</div>
</div>
"""

from lxml import etree
tree = etree.HTML(html)
products = tree.xpath('//div[@class="product"]')
for prod in products:
    name = prod.xpath('.//h3/text()')[0]
    price = prod.xpath('.//span[@class="price"]/text()')[0]
    review = prod.xpath('.//div[@class="reviews"]/text()')[0]
    print(f"{name} -> {price}, {review}")

案例7:综合实战——抓取动态加载的评论(分析请求参数)

"""
ajax_comments.py
豆瓣电影短评抓取(通过 Ajax 接口)
"""

import requests
import json

# 豆瓣电影 ID 例如 1292052(肖申克的救赎)
movie_id = '1292052'
start = 0
comments = []
while True:
    url = f'https://movie.douban.com/subject/{movie_id}/comments'
    params = {
        'start': start,
        'limit': 20,
        'status': 'P',
        'sort': 'new_score'
    }
    headers = {'User-Agent': 'Mozilla/5.0', 'Referer': f'https://movie.douban.com/subject/{movie_id}/'}
    resp = requests.get(url, params=params, headers=headers)
    # 注意豆瓣评论页的HTML,不是直接JSON,这里仅演示思路,实际应分析XHR
    # 假设是直接返回 JSON,下面为模拟
    # 真实一般需要解析HTML或找到真正的API
    if resp.status_code != 200:
        break
    # 模拟从HTML中解析评论...
    start += 20
    if start >= 100:
        break
print("完成评论抓取")

注意:很多网站的反爬机制会验证 Referer、Cookie、请求头等,实际中需要更细致的模拟。


⚠️ 易错点避坑总结

序号坑点描述后果解决方案
1XPath 表达式写错,导致结果为空提取不到数据在浏览器开发者工具中用 $x() 测试 XPath
2lxml 解析时自动添加 html/body相对路径 /html/body 可能失效使用 // 或忽略顶层,直接写相对路径
3忘记处理动态加载内容抓不到数据检查 XHR,直接请求 API
4正则表达式贪婪匹配匹配到过多内容.*? 非贪婪
5混淆 findallsearch返回结果不符合预期按需选择,findall 返回所有匹配
6忽略编码问题中文乱码设置 resp.encoding = 'utf-8' 或根据网页 meta 指定
7不设置 User-Agent请求被拒绝每次请求都添加浏览器 UA
8高频请求未加延时IP 被封time.sleep(random.uniform(0.5, 2))
9忽略 Cookies 和 Session无法保持登录状态使用 requests.Session()
10硬编码 XPath 位置索引网页结构微调后崩溃尽量使用 id、class 等稳定属性

📝 课后实战练习题

第1题:正则提取网页中的所有 URL

编写函数 extract_urls(html),使用正则提取所有以 http://https:// 开头的 URL,去重后返回列表。

第2题:XPath 提取知乎热榜问题

抓取知乎首页的热榜(https://www.zhihu.com/hot),使用 lxml 提取每个问题的标题、链接和热度值。

第3题:爬取动态加载的腾讯新闻

打开腾讯新闻客户端首页,分析 XHR 请求,找到新闻列表的 API 接口,用 Python 请求并解析 JSON,提取新闻标题和发布时间。

第4题:登录豆瓣并抓取我的标记

模拟登录豆瓣(使用你的测试账号),然后访问“我读过的书”页面,提取书名和评分。注意处理验证码(可选)。

第5题:正则与 XPath 结合

某网页中,商品价格格式为<span class="price">¥1,234.56</span>,先用正则提取价格数字,再转换为浮点数,并与 XPath 获取的商品名称一起存储。

第6题:爬取分页的 Ajax 评论

找到一个有分页评论的网站(如新闻评论区),分析其加载更多评论的 API 请求,循环抓取所有评论内容,保存为 JSON。

第7题:构建代理池(选做)

使用免费代理网站抓取代理 IP,验证可用性后存入列表,并在后续请求中随机使用代理。

🔜 下节课预告

正则和 XPath 让我们能高效解析数据,但网站的反爬机制也越来越复杂。下一节课我们将学习爬虫反爬入门,包括 User-Agent 伪装、代理 IP、Cookie 维持、验证码识别等内容。

第46课:爬虫反爬入门:请求头、代理IP、Cookie会话维持实战

内容包括:

  • 常见的反爬手段(User-Agent、IP 限速、验证码、字体反爬)
  • 请求头伪装与浏览器指纹
  • 代理 IP 的原理与使用
  • Cookie 持久化与会话维持
  • 验证码识别初步

学完本课,你将能够应对大多数初级反爬措施,提高爬虫的可用性。

🌟 学习鼓励:正则和 XPath 是爬虫进阶的重要工具。正则看似繁琐,但能解决很多文本提取问题;XPath 则是解析结构化文档的利器。请多做练习,在真实网站上尝试抓取,并总结不同网站的结构规律。经过本课学习,你已经具备独立开发复杂爬虫的能力了!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值