引言
“代码逻辑全对,请求一发出去,返回403。”这是很多爬虫新手最常见的翻车现场。问题往往不在代码,而在请求头——你的请求在服务器眼里,就像一个人穿着“我是机器人”的T恤去面试,被拒是必然的。
请求头(HTTP Headers)是客户端与服务器之间传递元信息的载体。服务器通过分析请求头来判断:你是谁、从哪来、用什么设备、是否携带有效凭证。爬虫要做的,就是把这些信息伪装成真实浏览器发出的样子。本文将从User-Agent开始,逐一拆解请求头伪装的完整体系,并介绍站大爷隧道代理IP在IP层面的配合策略。
免责声明:本文仅限技术学习与交流。请遵守目标网站的robots.txt协议及相关法律法规,合理控制请求频率,不得将技术用于非法用途。
一、请求头为什么是反爬的第一道关卡?
HTTP请求头是服务器识别客户端身份最直接的信息源。一个真实的浏览器请求,会携带十几到几十个头部字段,每个字段都包含特定的环境信息。而一个“裸奔”的爬虫请求,通常只有一个默认的User-Agent,甚至什么都没有。
服务器识别爬虫的逻辑大致如下:
- 缺失关键头部:没有
User-Agent、Accept、Accept-Language等浏览器必备字段 - 头部值异常:
User-Agent是python-requests/2.x或Scrapy/2.x - 头部组合矛盾:声称是Chrome浏览器,却没有
Sec-Fetch-*系列头部 - 顺序与频率异常:请求间隔完全固定,头部顺序与真实浏览器不一致
理解了这个逻辑,请求头伪装的目标就清晰了:让爬虫请求的头部特征与真实浏览器尽可能一致。
二、User-Agent伪装:最基础也最重要的一环
2.1 User-Agent是什么?
User-Agent(简称UA)是请求头中最重要的字段,它告诉服务器客户端的浏览器类型、操作系统、渲染引擎版本等信息。一个典型的Chrome UA如下:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
服务器通过UA可以判断:你用的是Windows系统、Chrome 120浏览器、WebKit渲染引擎。如果UA显示为python-requests/2.28.2,那等于直接告诉服务器“我是爬虫”。
2.2 UA池的构建
单一UA长期使用会被识别。正确做法是构建一个UA池,每次请求随机选择。一个覆盖主流浏览器和操作系统的UA池应包含:
USER_AGENTS = [
# Chrome - Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
# Chrome - macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
# Firefox - Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
# Safari - macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
# Edge - Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
]
使用fake_useragent库可以更方便地生成随机UA:
from fake_useragent import UserAgent
ua = UserAgent()
headers = {"User-Agent": ua.random}
2.3 移动端UA的注意事项
如果目标网站有移动端适配,使用移动端UA可能更容易绕过反爬。移动端UA的典型格式:
Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1
但需要注意:移动端UA需要与移动端页面结构、视口尺寸等特征匹配,否则会显得“不协调”。
三、Referer伪装:防盗链与来源校验
3.1 Referer的作用
Referer告诉服务器“你是从哪个页面跳转过来的”。很多网站的图片、API接口都设置了防盗链,只有从本站页面跳转过来的请求才能正常访问。直接请求资源URL会被拒绝或返回防盗链提示图。
3.2 Referer的构造策略
场景一:请求列表页。Referer可以设置为该网站的首页或上一级分类页。
场景二:请求详情页。Referer应设置为该详情页在列表页中的父页面URL。
场景三:请求图片资源。Referer应设置为该图片所在详情页的URL。
场景四:请求API接口。Referer应设置为调用该接口的前端页面URL。
headers = {
"User-Agent": ua.random,
"Referer": "https://www.example.com/list/page/2/",
}
3.3 常见错误
- Referer缺失:直接请求图片URL,服务器无法判断来源,触发防盗链
- Referer与目标域名不匹配:请求A网站的图片,Referer却是B网站,会被拒绝
- Referer格式错误:缺少
https://前缀或URL路径不完整
四、Cookie管理:登录态与身份凭证
4.1 Cookie的类型
Cookie分为两类:
- 会话Cookie:临时存储在内存中,浏览器关闭后失效
- 持久Cookie:存储在硬盘上,有明确的过期时间
对于爬虫来说,最核心的是登录态Cookie,它包含了用户身份凭证(如sessionid、token、MUSIC_U等)。
4.2 Cookie的获取方式
方式一:浏览器手动复制。登录目标网站后,在开发者工具的Application → Cookies中复制完整的Cookie字符串。
方式二:Selenium/Playwright自动获取。用浏览器自动化工具完成登录,然后通过driver.get_cookies()获取。
方式三:从请求响应中提取。部分网站的Cookie通过Set-Cookie响应头下发,可以用requests.Session()自动管理。
4.3 Cookie池的维护
对于需要多账号轮换的场景,需要维护一个Cookie池:
import random
COOKIE_POOL = [
"sessionid=abc123; token=xyz789",
"sessionid=def456; token=uvw012",
"sessionid=ghi789; token=rst345",
]
def get_random_cookie():
return random.choice(COOKIE_POOL)
4.4 Cookie过期的应对
Cookie有效期通常为数天到数周。建议:
- 监控请求状态,检测到401/403时自动触发重新登录
- 使用持久化存储(如Redis)保存Cookie,并记录过期时间
- 对于多账号场景,设置定时刷新任务
五、其他关键请求头
5.1 Accept系列
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
}
Accept:声明客户端能接受的内容类型Accept-Language:声明语言偏好Accept-Encoding:声明压缩算法支持
5.2 Connection与Upgrade
headers = {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
keep-alive表示保持长连接,Upgrade-Insecure-Requests表示支持升级到HTTPS。
5.3 Sec-Fetch系列(现代浏览器必备)
现代Chrome浏览器会发送Sec-Fetch-*系列头部,用于声明请求的上下文:
headers = {
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
}
如果声称是Chrome浏览器,却缺少这些头部,会显得异常。
5.4 X-Requested-With
对于Ajax请求,通常需要携带:
headers = {
"X-Requested-With": "XMLHttpRequest",
}
5.5 Content-Type
对于POST请求,必须声明请求体的格式:
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
}
# 或
headers = {
"Content-Type": "application/json; charset=UTF-8",
}
六、请求头顺序与一致性
6.1 头部顺序的重要性
真实浏览器的请求头顺序是固定的,由浏览器内核决定。Python的requests库默认的头部顺序与浏览器不同。部分高级反爬系统会检测头部顺序是否异常。
解决方案:使用OrderedDict或Session对象,确保头部顺序与浏览器一致:
from collections import OrderedDict
headers = OrderedDict([
("Host", "www.example.com"),
("Connection", "keep-alive"),
("Cache-Control", "max-age=0"),
("Upgrade-Insecure-Requests", "1"),
("User-Agent", ua.random),
("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
("Accept-Language", "zh-CN,zh;q=0.9"),
("Referer", "https://www.example.com/"),
])
6.2 头部一致性检查
头部之间需要保持逻辑一致:
- UA声称是Windows Chrome,就不应该出现Mac特有的头部
- 声称是移动端,
Accept应该包含移动端特有的MIME类型 - 有
Referer却声称是直接访问(Sec-Fetch-Site: none),存在矛盾
七、代理IP:请求头伪装的“搭档”
7.1 为什么请求头伪装需要代理配合?
请求头伪装解决的是“请求看起来像不像浏览器”的问题,而代理IP解决的是“请求从哪来”的问题。两者缺一不可。
即使请求头伪装得再完美,如果所有请求都来自同一个IP,服务器仍然可以通过IP频率分析识别出爬虫行为。站大爷隧道代理IP在请求头伪装的基础上,提供了IP层面的自动轮换,让每个请求都从不同的出口IP发出。
7.2 站大爷隧道代理的集成
在requests中集成站大爷隧道代理:
PROXIES = {
"http": "http://用户名:密码@tps.zdaye.com:8080",
"https": "http://用户名:密码@tps.zdaye.com:8080"
}
headers = {
"User-Agent": ua.random,
"Referer": "https://www.example.com/",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}
resp = requests.get(url, headers=headers, proxies=PROXIES, timeout=15)
在Scrapy中集成:
# settings.py
TUNNEL_PROXY_URL = "http://用户名:密码@tps.zdaye.com:8080"
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.TunnelProxyMiddleware': 543,
}
在Playwright中集成:
browser = p.chromium.launch(
headless=False,
proxy={"server": "http://用户名:密码@tps.zdaye.com:8080"}
)
站大爷隧道代理支持三种鉴权模式——白名单、用户名密码/白名单、用户名密码+白名单,可以根据需求灵活选择。对于需要精细控制IP更换周期的场景,可以通过period参数自定义切换频率。
7.3 请求头伪装 + 代理的组合效果
请求头伪装让请求在“内容层面”看起来像浏览器,代理IP让请求在“来源层面”看起来像不同用户。两者结合,才能最大限度地规避反爬系统的识别。
八、工具与框架的最佳实践
8.1 requests
import requests
from fake_useragent import UserAgent
ua = UserAgent()
session = requests.Session()
session.headers.update({
"User-Agent": ua.random,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
})
resp = session.get(url, proxies=PROXIES, timeout=15)
8.2 Scrapy
Scrapy通过中间件管理请求头:
class RandomUserAgentMiddleware:
def __init__(self, user_agents):
self.user_agents = user_agents
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.get("USER_AGENT_LIST"))
def process_request(self, request, spider):
request.headers["User-Agent"] = random.choice(self.user_agents)
8.3 Playwright
Playwright的浏览器实例会自动发送完整的请求头,但可以通过set_extra_http_headers补充自定义头部:
page.set_extra_http_headers({
"Referer": "https://www.example.com/",
"Accept-Language": "zh-CN,zh;q=0.9",
})
九、常见问题与避坑指南
9.1 请求头伪装后仍被识别怎么办?
- 检查头部是否完整(是否缺少
Sec-Fetch-*、Accept-Encoding等) - 检查头部顺序是否与浏览器一致
- 检查IP是否已被标记(使用站大爷隧道代理切换)
- 考虑使用Playwright等真实浏览器环境
9.2 Cookie频繁失效怎么办?
- 使用
requests.Session()自动管理Cookie - 建立Cookie池,多账号轮换
- 监控过期时间,定时刷新
9.3 如何判断请求头是否伪装成功?
- 对比浏览器和爬虫请求的头部差异
- 使用
httpbin.org/headers等回显服务测试 - 观察目标网站的响应状态和内容
9.4 法律与合规提醒
- 请求头伪装本身是中性的技术手段,但不得用于绕过合法的访问控制
- 遵守目标网站的robots.txt协议
- 控制请求频率,避免对目标服务器造成过大压力
- 仅将技术用于学习和研究目的
十、总结
请求头伪装是爬虫技术中最基础、也最核心的一环。从User-Agent到Cookie,从Referer到Sec-Fetch系列,每一个头部字段都承载着特定的环境信息。伪装的目标不是“伪造一个假身份”,而是“让请求看起来像一个真实用户发出的”。
| 伪装维度 | 关键头部 | 注意事项 |
| 身份标识 | User-Agent | 构建UA池,随机轮换 |
| 来源校验 | Referer | 与目标页面URL匹配 |
| 登录凭证 | Cookie | 持久化存储,定期刷新 |
| 内容协商 | Accept系列 | 与UA声明保持一致 |
| 上下文声明 | Sec-Fetch系列 | Chrome必备,不可缺失 |
| 请求类型 | X-Requested-With | Ajax请求必须携带 |
| 网络来源 | 代理IP | 站大爷隧道代理自动轮换 |
技术选型速览:推荐“完整请求头伪装 + 站大爷隧道代理 + 合理请求频率”的组合方案。请求头解决“内容层面”的识别,代理IP解决“来源层面”的识别,两者结合才能构建稳定的采集系统。
请求头伪装不是一劳永逸的,目标网站的反爬策略会持续升级。保持对新技术的学习,理解反爬背后的逻辑,才能在对抗中保持主动。希望本文能帮助你在爬虫请求头伪装的道路上迈出坚实的一步。
最后需要提醒的是:技术应当用于合法合规的场景。请遵守相关法律法规,仅将爬虫技术用于学习和研究目的。