反爬对抗策略
反爬对抗是爬虫工程师的核心战场。本章聚焦三大支柱:请求头伪装、IP代理体系、验证码识别,逐一拆解网站常见反爬手段的应对方法,帮助你从被动填坑转为主动防御。
1. 反爬机制全景
在进入具体方案之前,先用一张 mindmap 俯瞰主流反爬手段的完整格局。
网站通常不会只用一种手段,而是把上述几种叠加成"连环套"——例如某网站先用 IP 频率限制拦住大批量请求,扛不住时才弹出验证码,而验证码本身又嵌在 JS 动态渲染的表单里。因此,反爬对抗必须组合出招,单点突破很难走到最后。
2. 请求头伪装
最简单的反爬手段就是读取 HTTP 请求头,如果 User-Agent 是 python-requests/2.x,或者 Referer 为空,服务器直接扔 403。请求头伪装是最低成本的"隐身术"。
2.1 User-Agent 轮换
维护一个 UA 池,每次请求随机选取一个。现代浏览器 UA 数以百计,维护 50+ 条即可覆盖绝大多数场景。
import random
UA_POOL = [
# Chrome 125 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
# Chrome 125 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
# Firefox 127 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0",
# Edge 125 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
# Safari 17 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
]
# 配合 requests Session 使用
import requests
session = requests.Session()
session.headers.update({
"User-Agent": random.choice(UA_POOL),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1",
"Upgrade-Insecure-Requests": "1",
})不要只换 UA。一个真实的浏览器请求会携带 10+ 个 Header,如果只换了 UA 却忘了 Accept、Accept-Language 等字段,行为指纹还是暴露了"你不是浏览器"。
2.2 Referer 策略
Referer 告诉服务器"你从哪个页面跳过来的"。很多反爬系统校验 Referer 是否来自本站合法的上一页。
# 场景:从列表页翻到第2页
session.headers["Referer"] = "https://example.com/list?page=1"
# 场景:API请求需要来自合法来源
session.headers["Referer"] = "https://example.com/"某些浏览器隐私策略(如 Referrer-Policy)会将 Referer 截断为仅有域名部分(origin)。如果目标站点要求完整路径,需要调整策略。
2.3 Cookie 策略
Cookie 是重要的会话凭证。未登录、Cookie 过期都会触发反爬。
# 直接设置 Cookie 字符串
session.headers["Cookie"] = "session_id=abc123; user_token=xyz789; ..."
# 更推荐的方式:使用 Session + 登录流程自动管理
session.post("https://example.com/login", data={"user": "xxx", "pass": "xxx"})
# 之后 Session 会自动维护 Cookie,无需手动管理| Cookie 属性 | 作用 | 反爬要点 |
|---|---|---|
session_id / JSESSIONID | 会话标识 | 必须保持一致性,不同会话不能共用 |
token / csrf_token | 防跨站请求伪造 | 通常嵌入在页面 HTML 中,需先请求页面提取 |
_uuid / _device_id | 设备指纹 | 固定值模拟同一设备,频繁切换可能触发风控 |
Hm_lvt_* / Hm_lpvt_* | 百度统计 Cookie | 部分网站不校验,但保持规范更有益 |
3. IP 代理体系
如果说请求头伪装是换张脸,那 IP 代理就是换个身份。当单 IP 请求频率超标(通常表现为 429 Too Many Requests 或 403 Forbidden),代理是继续采集的唯一出路。
3.1 代理类型速览
┌──────────────────┐
│ 代理获取方式 │
└────────┬─────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
免费代理 付费API型 隧道/ADSL型
(可用率1-10%) (可用率60-90%) (可用率95%+)| 代理类型 | 成本 | 可用率 | 速度 | IP独占 | 维护成本 | 适用场景 |
|---|---|---|---|---|---|---|
| 免费公共代理 | 免费 | 1%~10% | 慢(1~10s) | 共享 | 极高 | 学习测试 |
| 付费 API 提取型 | 几十元/天 | 60%~90% | 中(0.5~3s) | 部分共享 | 中 | 中等规模爬取 |
| 付费隧道型 | 几百元/月 | 95%+ | 快(0.1~1s) | 共享 | 低 | 大规模爬取 |
| ADSL 拨号代理 | 几十元/月+服务器 | 99%+ | 快(0.1~0.5s) | 完全独占 | 中 | 高要求长期项目 |
3.2 代理设置速查
各请求库的代理设置方式:
# === requests ===
import requests
proxies = {"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"}
resp = requests.get("http://httpbin.org/get", proxies=proxies, timeout=10)
# === urllib ===
from urllib.request import ProxyHandler, build_opener
handler = ProxyHandler({"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"})
opener = build_opener(handler)
resp = opener.open("http://httpbin.org/get")
# === aiohttp (异步) ===
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get("http://httpbin.org/get", proxy="http://127.0.0.1:8888") as resp:
text = await resp.text()
# === Selenium / Chrome ===
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://127.0.0.1:8888")
browser = webdriver.Chrome(options=options)忘记给代理加协议前缀(http:// 或 socks5://)是新手最高频的错误。requests 的 proxies 参数值必须是 http://ip:port 格式,不能直接传 ip:port。否则会报 ProxyError。
3.3 代理池架构
代理池是一个全自动的代理 IP 管理系统:持续从多个来源获取代理、定时检测可用性、通过 API 接口对外提供可用代理。
分数机制设计
| 事件 | 分数操作 | 原因 |
|---|---|---|
| 新代理入库 | 设为 10 | 初始分数低,需验证 |
| 检测可用 | 设为 100(直接满分) | 确保可用代理始终能被取到 |
| 检测不可用 | 减 1 | 给予 100 次失败机会,避免偶发失败误删 |
| 分数降至 0 | 从池中移除 | 连续 100 次失败才淘汰 |
因为免费代理极不稳定,可能 5 次请求 2 次成功 3 次失败。如果每次成功只加 1 分,好代理几乎不可能积到高分,即便可用也无法被取到。"可用即满分"策略确保只要代理过检测就立即可用。
爬虫侧获取代理
import requests
PROXY_POOL_URL = "http://127.0.0.1:5555/random"
def get_proxy() -> str | None:
"""从代理池获取随机可用代理"""
try:
resp = requests.get(PROXY_POOL_URL, timeout=5)
if resp.status_code == 200:
return resp.text.strip()
except requests.exceptions.RequestException:
pass
return None
# 使用示例
proxy = get_proxy()
if proxy:
proxies = {"http": f"http://{proxy}", "https": f"https://{proxy}"}
resp = requests.get("https://example.com", proxies=proxies, timeout=15)3.4 免费代理 vs 付费代理 vs ADSL
| 维度 | 免费代理池 | 付费 API 型 | 付费隧道型 | ADSL 拨号 |
|---|---|---|---|---|
| 可用率 | 1%~10% | 60%~90% | 95%+ | 99%+ |
| IP 独占性 | 共享 | 部分共享 | 共享 | 完全独占 |
| 单 IP 被封概率 | 极高 | 中 | 低 | 极低 |
| 技术门槛 | 低 | 低 | 极低(固定地址) | 高(需搭建运维) |
| 推荐场景 | 学习/测试 | 中等规模 | 大规模 | 高要求长期项目 |
付费隧道型代理(如阿布云动态版)是最省心的方案:整个爬虫只需要配置一个固定的代理入口地址,后端自动为每个请求分配不同的出口 IP。无需代理池、无需手动轮换。
# 阿布云代理隧道示例 —— 固定地址,自动换IP
proxies = {
"http": "http://USER:PASS@proxy.abuyun.com:9020",
"https": "https://USER:PASS@proxy.abuyun.com:9020",
}
# 每次请求自动使用不同的出口IP
resp = requests.get("https://example.com", proxies=proxies)ADSL 拨号代理是终极方案:购买一台 ADSL 拨号 VPS,每次 adsl-stop; adsl-start 就换一个公网 IP。装上 TinyProxy 作为 HTTP 代理,再用脚本定时拨号并把新 IP 更新到 Redis。
- 拨号前必须先从 Redis 移除旧代理,否则拨号间隙旧 IP 被取用会导致请求失败。
- 至少配置 2 台主机互为备份,一台拨号时另一台继续提供代理。
- 代理验证 URL 应设置为目标站点的地址,而非通用测试站点(如百度),以筛掉被目标站点封禁的 IP。
3.5 代理使用检查清单
- 代理验证通过了吗?(请求
httpbin.org/get,看origin字段是否等于代理 IP) - 设置超时了吗?(
timeout=10~30,代理慢是常态) - HTTP 和 HTTPS 代理都配置了吗?
- 认证代理格式正确吗?(
http://user:pass@ip:port) - Selenium 的代理认证是否用了 Chrome 扩展插件?(Chrome 不支持命令行传认证信息)
- 代理失败时是否自动切换下一个代理?
4. 验证码识别
验证码(CAPTCHA)是反爬的最后一道防线。当 IP 轮换和请求头伪装都没有瞒过风控系统时,验证码就会出现。以下是完整处理流程:
4.1 图形验证码:OCR 为主,预处理为辅
图形验证码是最经典的类型,由 4~6 位字母或数字组成,叠加干扰线、噪点和扭曲。
直接 OCR(低干扰场景):
import tesserocr
from PIL import Image
image = Image.open("captcha.jpg")
result = tesserocr.image_to_text(image) # 或 .file_to_text("captcha.jpg")
print(result) # 输出: JR42预处理 + OCR(有干扰线时):
import tesserocr
from PIL import Image
def recognize_captcha(image_path: str, threshold: int = 127) -> str:
"""图形验证码识别(灰度化 + 二值化 + OCR)"""
image = Image.open(image_path)
# 步骤1: 灰度化
image = image.convert("L")
# 步骤2: 自定义阈值二值化
table = [0 if i < threshold else 1 for i in range(256)]
image = image.point(table, "1")
# 步骤3: OCR 识别
result = tesserocr.image_to_text(image)
return result.strip()
result = recognize_captcha("code2.jpg", threshold=80)
print(result) # 输出: PFRT预处理三步曲:
| 步骤 | 操作 | 目的 |
|---|---|---|
| 灰度化 | image.convert("L") | 去除颜色干扰,RGB 三通道压缩为单通道 |
| 二值化 | image.point(table, "1") | 将像素转为只有黑(0)白(255)两色 |
| 降噪 | 中值滤波 / 形态学操作 | 去除孤立噪点 |
设置白名单(提升识别精度):
result = tesserocr.image_to_text(
image,
config="-c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)4.2 滑动验证码:像素对比 + 轨迹模拟
以极验(GeeTest)为典型代表。核心挑战有两个:找到缺口位置和生成拟人化拖动轨迹。
缺口识别:像素对比法
def get_gap(image1: Image.Image, image2: Image.Image) -> int:
"""
对比两张图(无缺口 vs 有缺口),找到缺口左边界
:param image1: 不带缺口的原图
:param image2: 带缺口的图片
:return: 缺口左边界 x 坐标
"""
threshold = 60 # RGB 差异阈值
left = 60 # 从滑块右侧开始检测
for i in range(left, image1.size[0]):
for j in range(image1.size[1]):
pixel1 = image1.load()[i, j]
pixel2 = image2.load()[i, j]
if (abs(pixel1[0] - pixel2[0]) > threshold or
abs(pixel1[1] - pixel2[1]) > threshold or
abs(pixel1[2] - pixel2[2]) > threshold):
return i # 找到第一个差异像素
return left轨迹生成:物理运动模型
极验使用机器学习检测轨迹特征。匀速或简单分段变速的通过率不到 10%。必须使用先加速后减速的物理模型,并加入随机抖动。
import random
def get_track(distance: int) -> list[int]:
"""
生成拟人化移动轨迹(加速-减速模型 + 随机抖动)
:param distance: 需要移动的总距离(px)
:return: 每步移动距离的列表
"""
track = []
current = 0
mid = distance * 4 / 5 # 前 80% 加速,后 20% 减速
t = 0.2 # 时间间隔
v = 0 # 初速度
while current < distance:
if current < mid:
a = 2 + random.uniform(-0.5, 0.5) # 加速阶段
else:
a = -3 + random.uniform(-0.5, 0.5) # 减速阶段
v0 = v
v = v0 + a * t
move = v0 * t + 0.5 * a * t * t
move += random.uniform(-1, 1) # 微小随机抖动
current += move
track.append(round(move))
return track轨迹策略通过率对比:
| 策略 | 通过率 | 说明 |
|---|---|---|
| 匀速移动 | <10% | 最容易被机器学习识别 |
| 简单分段变速 | ~30% | 仍会被识别 |
| 物理模型(加速-减速) | ~70% | 推荐使用 |
| 物理模型 + 随机抖动 | ~85% | 最佳效果 |
| 录制真实轨迹 | ~95% | 最可靠但需收集真人数据 |
4.3 点触验证码:打码平台 + 坐标点击
点触验证码(如 12306 图片点选、TouClick 文字点选)要求用户点击图片中的特定目标。这类验证码 OCR 无法直接处理,打码平台是最优解。
打码平台对比:
| 平台 | 支持类型 | 价格 | 响应速度 | 准确率 |
|---|---|---|---|---|
| 超级鹰 | 图形/点触/坐标 | 中等 | 5~15s | 90%+ |
| 云打码 | 图形/点触 | 较低 | 3~10s | 85%+ |
| 若快 | 图形/点触 | 较低 | 3~8s | 85%+ |
| 2Captcha | reCAPTCHA/点触 | 较高 | 10~30s | 90%+ |
打码平台 API 封装:
import requests
from hashlib import md5
class Chaojiying:
"""超级鹰打码平台 API"""
def __init__(self, username: str, password: str, soft_id: int):
self.username = username
self.password = md5(password.encode()).hexdigest()
self.soft_id = soft_id
self.base_params = {
"user": self.username,
"pass2": self.password,
"softid": self.soft_id,
}
self.headers = {
"User-Agent": "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)",
}
def post_pic(self, im: bytes, codetype: int) -> dict:
"""
:param im: 图片字节流
:param codetype: 验证码类型(9102=坐标多选, 1902=英文数字)
:return: {"err_no": 0, "pic_str": "132,127|56,77"}
"""
params = {"codetype": codetype, **self.base_params}
files = {"userfile": ("ccc.jpg", im)}
resp = requests.post(
"http://upload.chaojiying.net/Upload/Processing.php",
data=params, files=files, headers=self.headers
)
return resp.json()
def report_error(self, im_id: str) -> dict:
"""识别错误时报告,返还题分"""
return requests.post(
"http://upload.chaojiying.net/Upload/ReportError.php",
data={"id": im_id, **self.base_params},
headers=self.headers,
).json()坐标解析与模拟点击:
def get_points(captcha_result: dict) -> list[list[int]]:
"""解析 pic_str: '132,127|56,77' -> [[132,127],[56,77]]"""
return [[int(n) for n in g.split(",")] for g in captcha_result["pic_str"].split("|")]
# 用 Selenium ActionChains 模拟点击
from selenium.webdriver.common.action_chains import ActionChains
for x, y in locations:
ActionChains(browser) \
.move_to_element_with_offset(captcha_element, x, y) \
.click().perform()4.4 宫格验证码:模板匹配
微博宫格验证码的样式是有限的(最多 24 种排列组合),可以用模板匹配法高效识别。
from os import listdir
from PIL import Image
def detect_image(image: Image.Image, templates_folder: str = "templates/") -> list[int] | None:
"""
模板匹配:遍历所有模板,找到匹配的验证码
模板文件名如 "4132.png" 表示滑动顺序 4->1->3->2
"""
for template_name in listdir(templates_folder):
template = Image.open(templates_folder + template_name)
# 计算像素相似度
count = sum(
1 for x in range(image.width) for y in range(image.height)
if is_pixel_equal(image, template, x, y, threshold=20)
)
similarity = count / (image.width * image.height)
if similarity > 0.99:
# 从文件名解析滑动顺序
return [int(n) for n in template_name.split(".")[0]]
return None| 方法 | 准确率 | 速度 | 适用场景 |
|---|---|---|---|
| 模板匹配 | 90%~99% | 1~2s | 样式有限(≤100种) |
| 深度学习(CNN) | 85%~95% | <1s | 样式无限或大量变体 |
4.5 验证码方案选择原则
- 先简后繁:OCR 能解决的不上打码平台(省钱)
- 模板优先:如果验证码样式有限,模板匹配是最快最准的方案
- 打码平台兜底:OCR 和模板都搞不定时,打码平台是最后的保障
- 识别错误要报错:使用打码平台时,识别错误一定要调用报错接口返还题分
5. 请求频率控制
即使 IP 和请求头都完美,如果发送请求的频率像机关枪一样均匀密集,也会触发风控。请求频率控制的本质是让机器的请求模式看起来像人类。
5.1 随机延迟
import time
import random
# 每次请求之间随机等待 1~3 秒
time.sleep(random.uniform(1.0, 3.0))
# 翻页时的延迟应该更长(模拟人类翻页阅读)
time.sleep(random.uniform(3.0, 8.0))5.2 指数退避
当请求返回 429(Too Many Requests)或 503(Service Unavailable)时,不要立即重试。使用指数退避算法逐步拉长重试间隔:
import time
import requests
def request_with_backoff(
url: str, max_retries: int = 5, base_delay: float = 1.0
) -> requests.Response:
"""带指数退避的请求重试"""
for attempt in range(max_retries):
resp = requests.get(url)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 503):
delay = base_delay * (2 ** attempt) # 1, 2, 4, 8, 16 秒
print(f"收到 {resp.status_code},{delay:.0f}秒后重试...")
time.sleep(delay)
else:
resp.raise_for_status()
raise RuntimeError(f"重试 {max_retries} 次后仍未成功")5.3 白天/夜间差异化策略
import time
import random
from datetime import datetime
def get_sleep_interval() -> float:
"""根据当前时间返回不同的睡眠间隔"""
hour = datetime.now().hour
if 1 <= hour < 7: # 凌晨:低风控,可以快一些
return random.uniform(0.5, 1.5)
elif 9 <= hour < 12: # 上午:正常
return random.uniform(1.0, 3.0)
elif 14 <= hour < 18: # 下午:高峰期,放缓
return random.uniform(3.0, 8.0)
else: # 默认
return random.uniform(1.5, 4.0)6. JS 逆向实操
当数据不是直接在 HTML 中,而是通过 JS 动态加载、加密后再提交到 API 时,就需要 JS 逆向。
6.1 JS 逆向的完整流程
6.2 技术栈与工具准备
| 工具 | 安装 | 用途 |
|---|---|---|
| Chrome DevTools | 浏览器内置 | JS 断点调试、搜索加密入口 |
| PyExecJS | pip install PyExecJS | Python 调用 JS 引擎执行加密函数 |
| Node.js | 官网下载安装 | PyExecJS 的推荐 JS 运行环境 |
| js2py | pip install js2py | 纯 Python 实现的 JS 解释器(性能较差) |
| Playwright | pip install playwright | 浏览器自动化 + CDP 协议提取加密结果 |
| Babel / AST Explorer | 在线工具 astexplorer.net | JS AST 语法树分析 |
6.3 实战一:定位并复写简单加密函数
场景:目标网站的 API 请求携带 sign 参数,由 JS 函数生成。
步骤一:抓包定位
在 Chrome DevTools 的 Network 面板,找到目标 XHR 请求,查看其参数:
// 请求参数中有一个 sign 字段
{
"keyword": "Python",
"page": 1,
"sign": "a3f5b8c2d1e4..."
}步骤二:搜索加密入口
在 DevTools 的 Sources 面板(Ctrl+Shift+F 全局搜索),搜索关键词 sign、token、encrypt、md5、sha、hmac。找到生成 sign 的函数:
// 网站源码中找到的加密函数
function generateSign(keyword, page, timestamp) {
var str = keyword + '&' + page + '&' + timestamp;
return md5(str); // 使用了 md5 库
}步骤三:Python 复写
import hashlib
import time
def generate_sign(keyword: str, page: int, timestamp: int) -> str:
"""Python 复写 JS 加密函数"""
str_ = f"{keyword}&{page}&{timestamp}"
return hashlib.md5(str_.encode('utf-8')).hexdigest()
# 发送请求
timestamp = int(time.time())
sign = generate_sign('Python', 1, timestamp)
response = requests.get(
'https://api.example.com/search',
params={
'keyword': 'Python',
'page': 1,
'sign': sign,
'timestamp': timestamp,
},
)6.4 实战二:PyExecJS 调用复杂 JS 加密
场景:加密函数使用了复杂的 JS 库(如 CryptoJS AES 加密),用 Python 复写成本太高。
import execjs
import requests
# 步骤1:提取 JS 加密函数源码
# 从网站源码中复制整个加密函数和相关依赖库
js_code = """
// 从网站源码中提取的加密函数(示例)
var CryptoJS = require('crypto-js'); // 或直接内联 CryptoJS 源码
function encryptData(data, key) {
return CryptoJS.AES.encrypt(
JSON.stringify(data),
CryptoJS.enc.Utf8.parse(key),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
).toString();
}
function generateSign(params, key) {
var sortedKeys = Object.keys(params).sort();
var str = sortedKeys.map(k => k + '=' + params[k]).join('&');
return CryptoJS.HmacSHA256(str, key).toString();
}
"""
# 步骤2:创建 JS 执行上下文
ctx = execjs.compile(js_code)
# 步骤3:调用 JS 函数获取加密结果
params = {'keyword': 'Python', 'page': 1}
sign = ctx.call('generateSign', params, 'secret_key_123')
print(f"sign: {sign}")
# 步骤4:发送请求
response = requests.get(
'https://api.example.com/search',
params={'keyword': 'Python', 'page': 1, 'sign': sign},
)PyExecJS 需要一个 JS 运行环境。推荐安装 Node.js(性能最好),PyExecJS 会自动检测。如果没有 Node.js,会回退到 PyV8 或 js2py,性能大幅下降。
# 检查 PyExecJS 的运行环境
import execjs
print(execjs.get().name) # 应输出 'Node.js (V8)'6.5 实战三:WebDriver 检测与绕过
许多网站通过 navigator.webdriver 属性检测自动化浏览器:
// 网站的反爬检测代码
if (navigator.webdriver) {
// 检测到 Selenium/Playwright,拒绝服务
document.body.innerHTML = '';
}方案一:undetected-chromedriver(推荐)
import undetected_chromedriver as uc
# 自动绕过 webdriver 检测
driver = uc.Chrome()
driver.get('https://example.com')
# navigator.webdriver 会返回 undefined(而非 true)
print(driver.execute_script('return navigator.webdriver')) # None/undefined方案二:Playwright + stealth 配置
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
],
)
context = browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
)
page = context.new_page()
# 注入 stealth JS:隐藏 webdriver 标记
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// 覆盖 chrome 对象
window.chrome = {
runtime: {},
loadTimes: function() {},
csi: function() {},
app: {}
};
// 覆盖 permissions 查询
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) =>
parameters.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: originalQuery(parameters);
""")
page.goto('https://example.com')
content = page.content()
browser.close()6.6 实战四:字体反爬破解
部分网站(如猫眼电影、58同城)使用自定义字体映射来隐藏关键数据(价格、评分等):
from fontTools.ttLib import TTFont
from io import BytesIO
import requests
# 下载并解析自定义字体
font_url = 'https://example.com/fonts/custom.woff'
font_data = requests.get(font_url).content
font = TTFont(BytesIO(font_data))
cmap = font.getBestCmap() # 获取字符映射表
# cmap 示例:Unicode 编码 → glyph 名称
# {0xE001: 'glyph00000', 0xE002: 'glyph00001', ...}
# 建立映射:glyph名称 → 实际字符
# 需要通过渲染字体图片 + OCR 来确定每个 glyph 对应的数字
glyph_map = {
'glyph00000': '3', # OCR 识别出 glyph00000 渲染为数字 3
'glyph00001': '5',
'glyph00002': '9',
# ...
}
# 替换源码中的编码数字
def decode_font_text(html: str, cmap: dict, glyph_map: dict) -> str:
"""将 HTML 中的字体编码替换为真实数字"""
for unicode_code, glyph_name in cmap.items():
real_char = glyph_map.get(glyph_name, '?')
html = html.replace(chr(unicode_code), real_char)
return html6.7 JS 逆向方案选择决策
| 方案 | 难度 | 性能 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| Python 复写加密函数 | 低 | 最高 | 高 | 加密逻辑简单(MD5、SHA256、Base64) |
| PyExecJS 执行 JS | 中 | 中 | 中 | 加密逻辑复杂但可提取为独立函数 |
| Playwright + CDP | 中 | 低 | 高 | 加密需要浏览器环境(DOM操作、Canvas等) |
| 字体反爬解析 | 高 | 高 | 中 | 数据被自定义字体映射隐藏 |
优先找 API > Python 复写 > PyExecJS > 浏览器自动化。越往后方案越重、越慢,但覆盖面越广。90% 的 JS 渲染页面背后都有现成的 XHR API,直接请求 API 比执行 JS 简单得多。
7. 常见陷阱速查表
| 序号 | 陷阱 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | 忘记协议前缀 | ProxyError: Cannot connect to proxy | 代理地址必须加 http:// |
| 2 | 仅换 UA 不换其他 Header | 仍然被识别为爬虫 | 模拟完整浏览器 Header 集合 |
| 3 | 匀速拖动滑块 | 极验验证失败 | 使用加速-减速物理模型 + 随机抖动 |
| 4 | OCR 二值化阈值不当 | 字符断裂或粘连 | 多试几个阈值(50/80/100/127) |
| 5 | 代理检测 URL 不对 | 代理显示可用但目标站拒绝 | 将检测 URL 设为目标站地址 |
| 6 | 未设置请求超时 | 程序长时间卡死不报错 | 所有请求加 timeout=10~30 |
| 7 | 302 重定向自动跟随 | 无法判断 IP 是否被封 | 设置 allow_redirects=False |
| 8 | 代理拨号前未移除旧代理 | 拨号间隙爬虫取到过期 IP | 拨号前先 remove_proxy() |
| 9 | 打码平台 codetype 选错 | 识别结果完全不对 | 确认验证码类型与 codetype 匹配 |
| 10 | 高 DPI 屏幕坐标偏移 | 模拟点击位置不准 | 用 window.devicePixelRatio 缩放坐标 |
| 11 | 免费代理直接使用不验证 | 大量请求失败 | 必须先验证后使用 |
| 12 | Cookie 过期未更新 | 只能看首页,翻页就 302 | 重新登录获取新 Cookie |
8. 法律与伦理
反爬对抗技术是把双刃剑。以下原则是所有爬虫工程师的基本职业底线:
8.1 robots.txt
任何网站部署了反爬机制,说明它不希望被爬取。在动手之前,先访问该网站的 /robots.txt:
User-agent: *
Disallow: /admin/
Disallow: /api/private/
Crawl-delay: 10Disallow列出的路径是明确禁止爬取的,无论技术是否可行,都应遵守。Crawl-delay: 10表示每次请求之间至少间隔 10 秒。- 虽然
robots.txt协议本身没有法律强制力,但它体现了网站的意愿。
8.2 爬取频率
- 不要打垮对方服务器:控制并发数(建议 ≤5),单个域名不要超过正常用户能产生的请求频率。
- 尽量在低峰期爬取:避开网站的业务高峰时段。
- 设置合理的 User-Agent 和联系方式:让网站管理员能找到你。
8.3 数据合规
- 《网络安全法》规定,未经授权获取计算机信息系统数据可能构成违法。
- 《个人信息保护法》严格限制个人信息的收集和处理,未经用户同意的爬取涉嫌违法。
- 用于学习、研究的爬虫一般问题不大,但商业化使用需极其谨慎。
- 爬取公开数据也需要评估:数据量级、是否涉及个人隐私、是否侵犯商业利益。
以下行为在任何情况下都应避免:
- 非法获取公民个人信息(电话、地址、身份证号等)
- 破解付费内容的访问控制
- 绕过安全措施进入未授权的系统
- 爬取数据后用于诈骗、骚扰等违法用途
- 爬虫代码中附上联系邮箱,方便网站管理员反馈
- 数据仅用于学术研究或个人学习,不商业转售
- 如果收到网站运营方的通知要求停止爬取,应立即停止
9. 小结
反爬对抗的核心方法论可以用四句话概括:
- 请求头伪装是入场券 -- UA、Referer、Cookie 三位一体,缺一不可。
- IP 代理是发动机 -- 免费代理入不了场,付费代理是及格线,ADSL 拨号是天花板。
- 验证码识别是守门员 -- OCR 对图形验证码,像素对比对滑动验证码,打码平台对点触验证码,模板匹配对宫格验证码。
- 频率控制是人设 -- 随机延迟 + 指数退避 + 差异化节奏,让请求模式像人类。
这些技术必须组合使用,单靠某一种方式很难在现代化反爬体系下生存。同时,所有技术应建立在遵守法律、尊重网站意愿的基础之上。
版本差异(爬虫技术栈 → 当前版本)
| 库 | 本文编写时 | 当前稳定版 |
|---|---|---|
requests | 2.28/2.31 | 2.32.x |
Scrapy | 1.x/2.0 | 2.11.x(API 稳定) |
httpx | 0.24 | 0.28.x |
Playwright | 1.3x | 1.6x(Python 版) |
lxml/BeautifulSoup | 旧版 | 保持稳定 |
| Python | 3.8-3.12 | 3.14(推荐) |
本文讲解的爬虫原理(HTTP、解析、反爬、存储)与核心 API 在最新版本中成立;注意 Python 3.9 及以下已 EOL,新项目使用 3.13/3.14。