Python 爬虫实战:8 大反爬与健壮性技巧详解

前言

在爬虫开发中,如何做到不被封 IP、断点续爬、应对页面结构变化是每个爬虫工程师的必修课。本文基于一个真实的高校新闻爬虫项目,系统梳理其中用到的 8 大爬虫技巧,涵盖请求伪装、频率控制、异常退避、断点续爬、多路径回退、数据清洗与持久化等内容。

技术栈:requests + lxml + re + pandas + csv


1. User-Agent 随机化

UA 池是反反爬的第一道防线。每次请求从 UA 列表中随机选取一个,避免固定 UA 被服务端识别为爬虫。

import random

user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15"
]

header = {
    'User-Agent': random.choice(user_agents),
}

技巧要点:UA 列表应常更新,覆盖 Chrome / Edge / Firefox / Safari 主流浏览器,且版本号不宜过旧。


2. 请求头完整伪装

除了 UA,还需要模拟完整浏览器请求头,尤其是 Referer,让请求看起来像站内跳转。

header = {
    'User-Agent': random.choice(user_agents),
    'Referer': 'https://www.xyu.edu.cn/',          # 伪装来源
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Connection': 'keep-alive'
}

技巧要点AcceptAccept-LanguageConnection 三个字段建议一并设置,让你的请求看起来更像正常浏览器行为。


3. 随机延时 + 分段长休息

固定频率请求是触发反爬的常见原因。通过随机延时 + 分段长休息,模拟人类浏览节奏。

# 每次请求后随机休眠 2-6 秒
wait = random.uniform(2, 6)
time.sleep(wait)

# 每爬取 50 条,额外休息 30-60 秒
if id % 50 == 0:
    rest = random.uniform(30, 60)
    time.sleep(rest)
策略间隔目的
单次延时2~6s 随机避免固定间隔被检测
分段休息每 50 条休息 30~60s模拟人类阅读间歇,降低服务器压力

技巧要点:使用 random.uniform() 而非固定整数,让间隔无法被简单统计规律识别。


4. 反爬状态码自动退避

当服务端返回限流/封禁状态码时,立即进入长时间休眠,避免持续冲击触发更严厉封锁。

if resp.status_code in (429, 403, 503):
    time.sleep(600)   # 休眠 10 分钟
状态码含义应对策略
429Too Many Requests(请求频率过高)等待 10 分钟再继续
403Forbidden(拒绝访问)等待 10 分钟,IP 可能被暂时封禁
503Service Unavailable(服务不可用)等待 10 分钟,服务恢复后再试

技巧要点:根据实际反爬强度可调整休眠时间(600s / 1200s),激进场景可配合 IP 代理池使用。


5. 断点续爬(访问记录持久化)

大规模爬取不可避免会遇到中断(网络波动、程序异常、IP 被封)。断点续爬机制保证不重复爬取已访问页面,程序重新运行时从断点继续。

visited_file = "visited_ids.txt"

# 1. 启动时加载已访问 ID 集合
try:
    with open(visited_file, 'r', encoding='utf-8') as f:
        visited_ids = set(line.strip() for line in f if line.strip())
except FileNotFoundError:
    visited_ids = set()

# 2. 遍历时跳过已访问 ID
for id in range(23900, 40000):
    if str(id) in visited_ids:
        continue
    # ... 爬取逻辑 ...

    # 3. 每条爬取后立即写入记录文件
    with open(visited_file, 'a', encoding='utf-8') as f_visited:
        f_visited.write(str(id) + "\n")
    visited_ids.add(str(id))

设计要点

  • 使用 set 做 O(1) 查找,避免大文件线性扫描
  • 每条爬完后立即追写文件('a' 模式),而非等全部结束再写
  • 启动时从文件重建 visited_ids,实现真正的断点续爬

技巧要点visited_ids 同时存在于内存(set)和磁盘(txt),内存用于快速判重,磁盘用于持久化恢复。


6. 多路径 XPath 回退

目标网站的 HTML 结构可能存在多种模板,单一 XPath 容易漏数据。通过多路径回退策略,依次尝试不同模板,提高数据覆盖率。

# 第一优先级:id="vsb_content_4" 的模板
content = html.xpath(
    '//div[@id="vsb_content_4"]//div[@class="v_news_content"]//p//text()'
)

# 回退路径 1:id="vsb_content" 的模板
if not content:
    content = html.xpath(
        '//div[@id="vsb_content"]//div[@class="v_news_content"]//p//text()'
    )

# 回退路径 2:不分段落,直接取所有文本
if not content:
    content = html.xpath(
        '//div[@id="vsb_content"]//div[@class="v_news_content"]//text()'
    )

技巧要点:回退路径按优先级排列——先精确再宽松,最后兜底。确保不同版本的页面模板都能正确提取正文。


7. 正则数据清洗

爬取的原始文本常包含 \xa0(不间断空格)、多余换行、HTML 实体等脏数据,需要用正则统一清洗。

import re

# 删除浏览量后缀(如 "浏览:123")
conttime = re.sub(r' 浏览:.*$', '', conttime)

# 去除 \xa0、\n、\r
content = content.replace('\xa0', '').replace('\n', '').replace('\r', '')

# 合并连续空白为单个空格
content = re.sub(r'\s+', ' ', content)

# 审核信息的多字段拼接与清洗
sssx = ' '.join([
    part.strip()
        .replace('\r', '')
        .replace('\n', '')
        .replace('\xa0', '')
    for part in sssx
    if part.strip()
])
清洗目标方法说明
浏览量信息re.sub(r' 浏览:.*$', '', conttime)删除时间后的浏览量尾巴
不间断空格.replace('\xa0', '')网页中常见的特殊空白字符
多余换行.replace('\n', '').replace('\r', '')去除 HTML 排版带来的换行
连续空白re.sub(r'\s+', ' ', content)多个空白合并为一个

技巧要点:优先用 str.replace() 处理已知字符,再用 re.sub() 处理通用模式,性能更好。


8. 实时刷盘 + 去重

数据安全是爬虫的底线——爬了 2 万条然后崩溃没存盘等于白干。每条写入后立即 flush() 到磁盘,爬取完成后用 Pandas 去重。

with open("../data/output.csv", 'a+', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)

    # 空文件先写入表头
    if f.tell() == 0:
        writer.writerow(["标题", "发行信息", "正文", "审核信息"])

    for id in range(start, end):
        # ... 爬取逻辑 ...
        writer.writerow([title, conttime, content, sssx])
        f.flush()   # ← 每条写入立即刷盘!

# 爬取完毕后基于多列去重
df = pd.read_csv("../data/output.csv", encoding="utf-8")
df_dedup = df.drop_duplicates(["标题", "发行信息", "正文", "审核信息"])
df_dedup.to_csv("../data/final.csv", index=False, encoding="utf-8")

设计要点

  • f.tell() == 0 判断文件是否为空,仅在空文件时写入表头
  • f.flush() 保证每一条数据都实时落盘,程序崩溃也不丢数据
  • drop_duplicates() 基于多列组合去重,比单列更安全

整体架构总结

启动 ──→ 加载 visited_ids(断点恢复)
  │
  ▼
遍历 ID 范围 ──→ 已在 visited_ids?──→ 跳过
  │                    否
  ▼                     │
发送请求 ◄──────────────┘
  │  ├─ UA 随机化
  │  ├─ 完整请求头伪装
  │  └─ 429/403/503 退避休眠
  ▼
XPath 解析(多路径回退)──→ 正则清洗
  │
  ▼
写入 CSV + flush() + 记录 visited_id
  │
  ├─ 每次请求后 sleep(2~6s)
  └─ 每 50 条 sleep(30~60s)
  ▼
全部完成 ──→ Pandas 去重 ──→ 最终输出

进阶建议

本文介绍的策略适合轻量级礼貌爬虫场景。如果你面对更强的反爬(如 Cloudflare 5 秒盾、滑块验证码、动态渲染页面),还需要补充以下能力:

场景进阶方案
IP 被封频繁引入代理池(付费/免费 IP 代理)
动态渲染页面使用 Selenium / Playwright 模拟浏览器
验证码拦截OCR 识别(ddddocr / tesseract)或打码平台
Cookie/登录态requests.Session 维持会话,保持登录态
大规模采集改用 Scrapy 框架,支持并发、中间件、分布式

本文代码均来自真实项目,核心思想:用最小的代价,换取最大的数据采集成功率。希望这些技巧对你的爬虫开发有所帮助!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐