3、网络爬虫实战:使用Requests和BeautifulSoup抓取静态网页,处理分页、请求头、反爬策略(限速、代理、验证码)
静态网页抓取是数据采集的基石。尽管现代网站大量采用JavaScript动态渲染,但仍有大量数据源(如政府公开数据、学术文献库、传统新闻门户、部分企业官网)以静态HTML形式提供服务。本章聚焦于Python生态中最经典的两个库——Requests(HTTP客户端)与BeautifulSoup(HTML解析器),系统讲解从构造请求到解析数据的完整链路,并深入探讨分页遍历、请求头伪装、限速、代理池及验证码应对等实战反爬策略。你将掌握一套可复用的静态网页采集方法论,为后续动态渲染页面(Selenium/Playwright)和API接口采集打下坚实基础。
上图展示了静态网页采集的完整工程链路:从目标分析、请求构造、解析提取到存储,四个阶段环环相扣。反爬策略并非独立模块,而是贯穿于请求构造与解析提取的每一个环节。分页处理模式则决定了遍历策略的复杂度。下面我们将逐一拆解每个环节的核心原理与实战技巧。
3.1 核心原理:Requests与BeautifulSoup的工作机制
Requests库基于urllib3封装,提供了简洁的API来发送HTTP/1.1请求。其核心优势在于:自动处理Keep-Alive连接复用、支持Session会话保持(自动携带Cookie)、内置SSL证书验证、支持代理与自定义请求头。当调用requests.get(url, headers=headers)时,库内部完成DNS解析、TCP握手、TLS协商、发送请求行与请求头、接收响应体等步骤。响应对象response包含status_code、headers、text(根据响应头编码解码后的字符串)、content(原始字节)等关键属性。
BeautifulSoup则是一个HTML/XML解析库,它构建了解析树,支持通过标签名、属性、CSS选择器、文本内容等多种方式定位元素。其底层解析器可选择html.parser(Python内置)、lxml(性能更优)或html5lib(容错性最强)。核心操作流程为:soup = BeautifulSoup(html, 'lxml'),然后使用soup.find()、soup.find_all()、soup.select()等方法提取数据。值得注意的是,BeautifulSoup本身不发起网络请求,它只负责解析已获取的HTML字符串。
response.text中包含目标数据(而非登录跳转页或验证码页)。建议在解析前打印response.url和response.status_code进行调试。
3.2 操作流程:从单页抓取到分页遍历
一个健壮的静态网页爬虫应遵循以下标准化流程:
- 初始化Session:创建
requests.Session()对象,设置默认请求头(User-Agent、Accept-Language等),并配置重试策略(使用urllib3.util.retry或requests.adapters.HTTPAdapter)。 - 发送请求并校验:调用
session.get(url, timeout=10),检查response.status_code,若为403或429则触发反爬应对逻辑。 - 解析HTML:使用BeautifulSoup定位目标数据节点,提取文本、属性或链接。优先使用CSS选择器(
soup.select('div.item h2 a')),因其表达力强且简洁。 - 数据清洗:去除空白字符、处理缺失值、转换数据类型(如将字符串日期转为
datetime对象)。 - 分页控制:分析URL参数模式(如
?page=2)或翻页按钮的href属性,循环遍历直到满足终止条件(如页面为空、达到最大页数或捕获到重复数据)。 - 异常处理与日志:使用
try-except捕获网络超时、连接错误、解析异常,记录日志并决定是否重试或跳过。
以下是一个典型的分页抓取代码骨架:
import requests
from bs4 import BeautifulSoup
import time
import random
def fetch_page(session, url, headers):
try:
resp = session.get(url, headers=headers, timeout=10)
if resp.status_code == 200:
return resp.text
elif resp.status_code == 403:
print(f"403 Forbidden: {url}")
return None
else:
print(f"Unexpected status {resp.status_code}: {url}")
return None
except requests.RequestException as e:
print(f"Request failed: {e}")
return None
def parse_listing(html):
soup = BeautifulSoup(html, 'lxml')
items = []
for card in soup.select('div.product-card'):
title = card.select_one('h2.title').text.strip()
price = card.select_one('span.price').text.strip()
items.append({'title': title, 'price': price})
return items
# 主循环
session = requests.Session()
base_url = 'https://example.com/products?page={}'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
all_data = []
for page in range(1, 6): # 抓取前5页
url = base_url.format(page)
html = fetch_page(session, url, headers)
if html:
page_items = parse_listing(html)
if not page_items:
print(f"Page {page} is empty, stopping.")
break
all_data.extend(page_items)
print(f"Page {page}: got {len(page_items)} items")
# 随机延时,避免请求过快
time.sleep(random.uniform(1, 3))
print(f"Total items collected: {len(all_data)}")
3.3 反爬策略实战:请求头、限速、代理与验证码
反爬的本质是模拟真实用户行为,同时保持采集效率。以下策略按优先级排序:
| 策略 | 触发场景 | 实现方法 | 注意事项 |
|---|---|---|---|
| 请求头伪装 | 服务器校验User-Agent、Referer、Accept-Language | 使用fake-useragent库随机生成UA;补充Referer为站点首页;保持Accept头与浏览器一致 | 不要伪造过于冷门的UA,避免被识别为爬虫特征 |
| 限速控制 | 请求频率过高导致IP被临时封禁 | 在请求间加入time.sleep(random.uniform(1, 3));使用令牌桶算法控制QPS | 限速需平衡效率与安全,建议初始QPS≤1,观察日志后逐步调整 |
| 代理池 | IP被封禁或目标站点有地域限制 | 维护代理列表,每次请求随机选取;使用proxies={'http': 'http://ip:port'};定期检测代理可用性 | 免费代理不稳定,付费代理需关注带宽与并发限制;代理失效时需自动切换 |
| 验证码应对 | 触发滑块、图形验证码或短信验证 | 优先降低请求频率;使用OCR(如tesseract)识别简单图形验证码;接入打码平台(如超级鹰)处理复杂验证码;保持Session以维持Cookie | 验证码是强反爬信号,频繁触发会导致IP永久封禁;建议遇到验证码时暂停采集并人工介入 |
robots.txt协议及当地法律法规,不得用于商业竞争或侵犯个人隐私。
3.4 案例拆解:抓取某电商平台商品列表(含分页与反爬)
背景与约束:假设我们需要采集某电商平台“手机”类目下前10页商品信息(每页20条),包括商品名称、价格、评论数。该平台有基础反爬:检测User-Agent、请求频率超过5次/秒则封IP 10分钟,且部分页面会弹出滑块验证码。
初始现象:直接使用默认UA(如Python-requests/2.31.0)请求,返回403;使用单IP高频请求,第3页后出现验证码页面。
关键输入参数:目标URL模式为https://www.example-shop.com/list?cat=phone&page={};响应HTML中商品卡片位于<div class="item">内,名称在<a class="title">,价格在<span class="price">。
分析判断过程:首先,通过浏览器开发者工具查看正常请求的请求头,发现UA为Chrome/120.0,且带有Accept-Language: zh-CN,zh;q=0.9。其次,测试发现每次请求间隔2秒时不会触发验证码,但间隔1秒时第5次请求即出现验证码。因此,将限速设为2-4秒随机值。最后,准备了一个包含5个代理IP的池,每10次请求轮换一次IP。
实际操作步骤:
- 使用
fake_useragent库生成随机UA,并固定Referer为首页。 - 构建Session,设置重试3次、超时15秒。
- 循环遍历1-10页,每页请求前从代理池随机选取一个IP,并随机延时2-4秒。
- 解析HTML,提取数据,若发现验证码元素(如
soup.select('div.captcha')),则暂停60秒并切换代理后重试。 - 将数据写入CSV文件,每完成一页打印进度。
结果对比:未使用反爬策略时,仅成功抓取2页(40条数据)即被封IP;使用完整策略后,成功抓取全部10页(200条数据),耗时约35秒,无验证码触发,IP未被封禁。
风险与代价:代理池中2个代理响应速度较慢(平均延迟3秒),导致整体采集时间延长约20%;打码平台未使用,因为通过限速和代理已规避验证码。若遇到强验证码,需额外支付打码费用。
复盘经验:反爬策略应分层部署——先限速,再伪装UA,最后才考虑代理池。代理池的维护成本较高,建议优先优化请求频率。同时,日志记录至关重要,通过分析日志可快速定位被封原因(如某IP连续失败3次即自动剔除)。
3.5 常见误区与总结复盘
在实际项目中,开发者常陷入以下误区:
- 忽略robots.txt:即使技术上可行,也应尊重网站的爬取许可声明,避免法律风险。
- 过度依赖单一反爬手段:仅更换UA而不限速,或仅限速而不处理Cookie,均无法应对综合反爬策略。
- 解析时使用正则而非BeautifulSoup:正则表达式在HTML结构变化时极易失效,BeautifulSoup的容错性更佳。
- 未处理编码问题:某些网站返回GBK编码,直接使用
response.text会乱码,应通过response.encoding = 'gbk'或从响应头获取charset。 - 分页终止条件不明确:仅依赖“最大页数”可能导致漏抓,应结合“当前页无数据”或“URL重复”作为终止信号。
通过本章的学习,你已掌握静态网页采集的核心技能。下一阶段,我们将探讨如何应对JavaScript动态渲染的页面,届时Requests将不再适用,需要引入浏览器自动化工具。但请记住,静态采集的思维框架——分析、请求、解析、存储、反爬——依然适用于更复杂的场景。