3、网络爬虫实战:使用Requests和BeautifulSoup抓取静态网页,处理分页、请求头、反爬策略(限速、代理、验证码)

静态网页抓取是数据采集的基石。尽管现代网站大量采用JavaScript动态渲染,但仍有大量数据源(如政府公开数据、学术文献库、传统新闻门户、部分企业官网)以静态HTML形式提供服务。本章聚焦于Python生态中最经典的两个库——Requests(HTTP客户端)与BeautifulSoup(HTML解析器),系统讲解从构造请求到解析数据的完整链路,并深入探讨分页遍历、请求头伪装、限速、代理池及验证码应对等实战反爬策略。你将掌握一套可复用的静态网页采集方法论,为后续动态渲染页面(Selenium/Playwright)和API接口采集打下坚实基础。

静态网页采集工程链路与反爬策略 1. 目标分析 URL结构/编码 分页参数/数据量 robots.txt/法律边界 2. 请求构造 Requests Session Headers/UA/Cookie 超时/重试机制 3. 解析提取 BeautifulSoup选择器 CSS/XPath/正则 数据清洗与结构化 4. 存储 CSV/JSON MySQL/MongoDB 增量/全量 反爬策略矩阵(贯穿请求与解析阶段) 请求头伪装 UA/Referer/Accept 随机切换 限速控制 time.sleep() 随机延时1-3s 代理池 IP轮换/失效检测 免费/付费代理 验证码应对 OCR/打码平台 会话保持/降频 异常处理 重试/退避 日志监控 分页处理模式 URL参数分页 ?page=1&size=20 翻页按钮/JS 需逆向分析 无限滚动 需抓包找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字符串。

关键认知:Requests负责“拿”,BeautifulSoup负责“拆”。两者结合时,务必先确认响应状态码为200,且response.text中包含目标数据(而非登录跳转页或验证码页)。建议在解析前打印response.url和response.status_code进行调试。

3.2 操作流程:从单页抓取到分页遍历

一个健壮的静态网页爬虫应遵循以下标准化流程:

  1. 初始化Session:创建requests.Session()对象,设置默认请求头(User-Agent、Accept-Language等),并配置重试策略(使用urllib3.util.retry或requests.adapters.HTTPAdapter)。
  2. 发送请求并校验:调用session.get(url, timeout=10),检查response.status_code,若为403或429则触发反爬应对逻辑。
  3. 解析HTML:使用BeautifulSoup定位目标数据节点,提取文本、属性或链接。优先使用CSS选择器(soup.select('div.item h2 a')),因其表达力强且简洁。
  4. 数据清洗:去除空白字符、处理缺失值、转换数据类型(如将字符串日期转为datetime对象)。
  5. 分页控制:分析URL参数模式(如?page=2)或翻页按钮的href属性,循环遍历直到满足终止条件(如页面为空、达到最大页数或捕获到重复数据)。
  6. 异常处理与日志:使用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永久封禁;建议遇到验证码时暂停采集并人工介入
风险警示:使用代理池时,务必确保代理来源合法,避免使用被黑客控制的僵尸网络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。

实际操作步骤:

  1. 使用fake_useragent库生成随机UA,并固定Referer为首页。
  2. 构建Session,设置重试3次、超时15秒。
  3. 循环遍历1-10页,每页请求前从代理池随机选取一个IP,并随机延时2-4秒。
  4. 解析HTML,提取数据,若发现验证码元素(如soup.select('div.captcha')),则暂停60秒并切换代理后重试。
  5. 将数据写入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重复”作为终止信号。
经验总结:一个生产级爬虫应具备“三明治”结构——底层是稳定的请求重试与日志机制,中间层是灵活的解析与数据清洗逻辑,顶层是动态可配置的反爬策略(通过配置文件调整限速、代理、UA池)。建议将采集任务封装为类,支持断点续爬(记录已抓取的URL或页码),以便应对中断后恢复。

通过本章的学习,你已掌握静态网页采集的核心技能。下一阶段,我们将探讨如何应对JavaScript动态渲染的页面,届时Requests将不再适用,需要引入浏览器自动化工具。但请记住,静态采集的思维框架——分析、请求、解析、存储、反爬——依然适用于更复杂的场景。