Python 爬虫总被封?从 TLS 指纹到浏览器指纹——反检测的 7 层技术栈
你写了一个视频下载器,跑了没两天就被平台封了 IP。换了代理,过两天又封了。加了 User-Agent,还是封。你开始怀疑:平台是不是能认出我的 Python 脚本?答案是——能,而且很轻松。 你的 requests 库发出的请求,和 Chrome 浏览器发出的请求,在服务器眼里几乎是两个物种:TLS 握手指纹不同、HTTP/2 帧格式不同、请求头顺序不同、甚至 TCP 拥塞控制的行为模式都不同。这篇文章从底层到上层,把 Python 爬虫被检测的 7 个层面逐一拆开,每个层面给出实战对抗方案。
TL;DR:反检测分 7 层:① TLS 指纹(JA3/JA4)→ 用 curl_cffi 或 tls_client 伪装 Chrome 指纹;② HTTP/2 帧 → 用支持 h2 的库;③ 请求头顺序 → 保持和浏览器一致;④ Cookie/Session → 模拟完整的登录态;⑤ JS 挑战 → 必要时上 Playwright;⑥ 浏览器指纹 → headless 检测对抗;⑦ IP/行为分析 → 住宅代理 + 随机延迟。对视频下载器来说,前 4 层搞定基本够用,后 3 层是极端情况。
目录
- 一、你的 requests 和 Chrome 到底差在哪
- 二、第一层:TLS 指纹——最容易被忽略的检测维度
- 三、第二层:HTTP/2 与请求头指纹
- 四、第三层:Cookie 与 Session 管理
- 五、第四层:JavaScript 挑战与无头浏览器
- 六、第五层:浏览器指纹与 Headless 检测
- 七、第六层:IP 信誉与行为分析
- 八、实战:一个高隐匿的视频下载器框架
- 九、合规与温馨提示
一、你的 requests 和 Chrome 到底差在哪
先看一个事实。用 requests.get() 访问 https://httpbin.org/headers 和用 Chrome 访问同一个地址,服务端看到的差异:
requests 的请求:
GET /headers HTTP/1.1
Host: httpbin.org
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Chrome 的请求:
GET /headers HTTP/2
Host: httpbin.org
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
Accept: text/html,application/xhtml+xml,...
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Sec-Ch-Ua: "Chromium";v="120", "Google Chrome";v="120"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
差异远不止 User-Agent 这一行。从传输层到应用层,requests 和 Chrome 有本质区别。大型平台(B 站、YouTube、抖音)的反爬系统,至少会检查其中 3-4 个层面。
二、第一层:TLS 指纹——最容易被忽略的检测维度
2.1 什么是 TLS 指纹
当你访问一个 HTTPS 网站时,浏览器和服务器之间首先要进行 TLS 握手。这个握手过程中,客户端会发送一个 ClientHello 消息,其中包含:
- 支持的 TLS 版本(1.2, 1.3)
- 支持的加密套件列表(几十个)
- 支持的椭圆曲线
- 扩展列表(SNI, ALPN, supported_groups 等)
不同的 HTTP 库发送的 ClientHello 消息内容不同。 这些内容的组合形成一个唯一的"指纹"——JA3 指纹(或新的 JA4 指纹)。
2.2 Python 库的 TLS 指纹对比
| 库 | TLS 实现 | JA3 指纹特征 | 被检测风险 |
|---|---|---|---|
| requests (urllib3) | OpenSSL | Python 特有指纹 | ⚠️ 高 |
| httpx | OpenSSL | Python 特有指纹 | ⚠️ 高 |
| aiohttp | OpenSSL | Python 特有指纹 | ⚠️ 高 |
| curl_cffi | curl + TLS 伪装 | 可伪装 Chrome/Firefox | ✅ 低 |
| tls_client | 自定义 TLS | 可伪装 Chrome | ✅ 低 |
| Playwright/浏览器 | 真实浏览器 | 原生 Chrome 指纹 | ✅ 极低 |
你可以用这个网站测试自己库的 TLS 指纹:https://tls.peet.ws/api/all
2.3 解决方案:curl_cffi
curl_cffi 是目前 Python 生态里最成熟的 TLS 伪装方案。它的用法和 requests 几乎一样:
# 安装
# pip install curl_cffi
from curl_cffi import requests
# 伪装成 Chrome 120
resp = requests.get('https://httpbin.org/headers',
impersonate='chrome120')
# 支持的伪装目标
# chrome120, chrome110, chrome107, chrome101, chrome100
# firefox117, firefox110
# safari17_0, safari15_5
# edge101, edge99
curl_cffi 的底层是 libcurl,但修改了 TLS 握手逻辑,让 ClientHello 的加密套件顺序、扩展列表、椭圆曲线等与目标浏览器完全一致。这样 CDN 层面的 TLS 指纹检测就无法区分你的脚本和真实浏览器了。
2.4 适用于 aiohttp 的替代方案
如果你的项目用了 asyncio,需要异步 + TLS 伪装:
# 方案 1:tls_client(同步,但可以配合 run_in_executor)
import tls_client
session = tls_client.Session(
client_identifier='chrome_120',
random_tls_extension_order=True
)
resp = session.get('https://httpbin.org/headers')
# 方案 2:curl_cffi 的异步模式
from curl_cffi.requests import AsyncSession
import asyncio
async def fetch():
async with AsyncSession() as s:
resp = await s.get('https://httpbin.org/headers',
impersonate='chrome120')
return resp.text
asyncio.run(fetch())
三、第二层:HTTP/2 与请求头指纹
3.1 请求头的顺序和格式
浏览器的请求头有严格的顺序和格式要求。requests 默认发送的请求头不仅内容不同,顺序也不对。
# requests 默认(反爬系统一眼就能看出)
headers = {
'User-Agent': '...',
'Accept': '*/*', # ← 太宽泛
}
# 缺少:Accept-Language, Sec-Ch-Ua, Sec-Fetch-* 等
# 正确的浏览器请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36',
'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,en-GB;q=0.7,en-US;q=0.6',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", '
'"Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
}
关键点:Sec-* 系列请求头是 Chrome 独有的安全策略标记。如果你的请求没有这些头,或者值不对,反爬系统可以判定"这不是浏览器发出的请求"。
3.2 HTTP/2 的必要性
很多大型 CDN(Cloudflare、Akamai、阿里云 CDN)对 HTTP/1.1 的请求会更加严格审查——因为现在几乎所有浏览器都默认用 HTTP/2 或 HTTP/3。如果你的脚本还在用 HTTP/1.1,本身就暴露了"非浏览器"身份。
# requests 默认使用 HTTP/1.1
# 需要额外安装支持 HTTP/2 的适配器
# 方案 1:httpx(支持 HTTP/2)
import httpx
client = httpx.Client(http2=True)
resp = client.get('https://httpbin.org/headers')
# 方案 2:curl_cffi(默认 HTTP/2)
from curl_cffi import requests as curl_requests
resp = curl_requests.get('https://httpbin.org/headers',
impersonate='chrome120')
四、第三层:Cookie 与 Session 管理
4.1 不仅仅是"带 Cookie"
平台反爬系统不光检查"有没有 Cookie",还会检查 Cookie 的使用模式:
正常浏览器:
1. 首次访问 → 无 Cookie → 服务器 Set-Cookie: session_id=xxx
2. 后续请求 → 带上 session_id → 服务器识别为同一会话
爬虫常见错误:
1. 首次请求 → 带了一个固定的 Cookie(从配置文件读的)
2. 服务器发现:这个 session_id 上次在另一个 IP 用过了
3. 判定:可能是盗用的 Cookie → 拒绝或标记
4.2 正确做法
from curl_cffi import requests
# 使用 Session 自动管理 Cookie
session = requests.Session(impersonate='chrome120')
# 先访问首页(获取初始 Cookie,建立会话)
session.get('https://www.example.com')
# 然后再请求视频接口(Cookie 自动带上,且是"新鲜"的)
resp = session.get('https://www.example.com/api/video/12345')
# 如果需要登录态,模拟登录
session.post('https://www.example.com/login', data={
'username': '...',
'password': '...',
})
关键原则:让 Cookie 从"浏览器行为"中自然产生,而不是从配置文件里硬塞进去。
五、第四层:JavaScript 挑战与无头浏览器
5.1 什么时候需要浏览器
当平台使用了以下技术时,纯 HTTP 库无法处理:
- Cloudflare Turnstile / 5 秒盾:需要执行 JS 并提交验证结果
- WebDriver 检测:B 站等平台的播放页会检测
navigator.webdriver - wasm 计算:YouTube 的 n-sig 签名(yt-dlp 已经帮我们处理了)
- Canvas 指纹:通过 Canvas 2D 渲染差异识别浏览器
5.2 Playwright 反检测配置
from playwright.sync_api import sync_playwright
def create_stealth_browser():
"""创建一个反检测的浏览器实例"""
p = sync_playwright().start()
browser = p.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled', # 移除自动化标记
'--no-sandbox',
'--disable-infobars',
'--disable-dev-shm-usage',
]
)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36',
locale='zh-CN',
)
# 注入反检测脚本
page = context.new_page()
# 隐藏 webdriver 标记
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// 伪装 chrome.runtime
window.chrome = {
runtime: {}
};
// 伪装 plugins 和 mimeTypes
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
// 移除 headless 相关的 User-Agent 标记
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en']
});
""")
return page
# 使用
page = create_stealth_browser()
page.goto('https://www.example.com/video/12345')
# 等待视频元素加载
page.wait_for_selector('video', timeout=10000)
# 获取视频地址
video_src = page.evaluate("""
() => document.querySelector('video').src
""")
print(f"视频地址: {video_src}")
page.close()
六、第五层:浏览器指纹与 Headless 检测
即使用了无头浏览器,平台还能通过以下方式检测:
6.1 Headless Chrome 的暴露点
// 平台的反爬 JS 可能检测这些:
// 1. navigator.webdriver(最常见的检测点)
navigator.webdriver === true // Headless 默认 true
// 2. Chrome 自动化标记
navigator.userAgent.includes('HeadlessChrome')
// 3. 权限请求行为
navigator.permissions.query({name: 'notifications'})
// Headless 中返回的状态和普通 Chrome 不同
// 4. WebGL 渲染器
// Headless 的 WebGL 渲染器是 "SwiftShader",普通 Chrome 是显卡型号
// 5. 屏幕分辨率
// Headless 默认视口很小
6.2 用 playwright-stealth 一键隐藏
pip install playwright-stealth
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 一键应用所有反检测措施
stealth_sync(page)
page.goto('https://bot.sannysoft.com') # 检测网站
page.screenshot(path='stealth_test.png')
七、第六层:IP 信誉与行为分析
7.1 IP 层面的检测
即使前 5 层全过了,IP 也可能被标记:
| IP 类型 | 被检测风险 | 说明 |
|---|---|---|
| 机房 IP(阿里云/腾讯云/AWS) | ⚠️ 高 | 大量爬虫使用,信誉分低 |
| 数据中心代理 | ⚠️ 高 | 和机房 IP 一样的问题 |
| 住宅代理 | ✅ 低 | 看起来像普通家庭宽带 |
| 移动代理 | ✅ 极低 | 4G/5G 出口,最难被封 |
7.2 行为层面的检测
正常用户的行为特征:
- 先浏览首页 → 搜索 → 点击结果 → 看视频
- 看视频的时间分布合理(不会 24 小时不间断)
- 鼠标有移动轨迹(即使不点击)
- 滚动行为有自然的不规律性
爬虫的行为特征:
- 直接请求视频接口(跳过首页和搜索)
- 24 小时不间断下载
- 没有任何鼠标/滚动事件
- 请求间隔精确一致(比如每次都间隔 1.000 秒)
7.3 行为伪装
import random
import time
import asyncio
class HumanLikeDownloader:
"""模拟人类行为的下载器"""
def __init__(self):
self.request_count = 0
self.session_start = time.time()
async def human_delay(self):
"""随机延迟,模拟人类的思考时间"""
# 大部分时间 0.5-2 秒,偶尔 5-10 秒
if random.random() < 0.1: # 10% 概率较长延迟
delay = random.uniform(5, 10)
else:
delay = random.uniform(0.5, 2)
await asyncio.sleep(delay)
async def should_take_break(self):
"""模拟人类休息:每下载 20 个视频休息 5-15 分钟"""
if self.request_count > 0 and self.request_count % 20 == 0:
break_time = random.uniform(300, 900) # 5-15 分钟
print(f"模拟休息 {break_time/60:.0f} 分钟...")
await asyncio.sleep(break_time)
async def fetch_with_behavior(self, session, url, page_url=None):
"""模拟人类行为的请求"""
await self.human_delay()
await self.should_take_break()
headers = {
'Referer': page_url or 'https://www.example.com/',
'Origin': 'https://www.example.com',
}
self.request_count += 1
return await session.get(url, headers=headers)
八、实战:一个高隐匿的视频下载器框架
整合上面所有技术:
from curl_cffi import requests
import time
import random
import os
class StealthVideoDownloader:
"""高隐匿视频下载器"""
def __init__(self, proxy=None):
self.session = requests.Session(impersonate='chrome120')
# 完整浏览器请求头
self.session.headers.update({
'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',
'Cache-Control': 'no-cache',
'Sec-Ch-Ua': '"Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
})
if proxy:
self.session.proxies = {
'http': proxy,
'https': proxy,
}
self.request_count = 0
def _human_delay(self):
"""随机人类延迟"""
time.sleep(random.uniform(0.5, 1.5))
def _get_referer(self, url_or_page):
"""自动构造 Referer"""
if url_or_page.startswith('http'):
return url_or_page
return f'https://www.example.com/video/{url_or_page}'
def download_video(self, video_url, page_url, output_path):
"""下载视频(完整鉴权 + 伪装)"""
self._human_delay()
# 检查是否需要换 IP
if self.request_count > 50:
print("请求数过多,建议切换代理")
headers = {
'Referer': self._get_referer(page_url),
'Origin': self._get_referer(page_url).rsplit('/', 1)[0],
}
resp = self.session.get(video_url, headers=headers, stream=True)
if resp.status_code == 403:
# 尝试加强鉴权
resp = self._escalate_auth(video_url, page_url)
resp.raise_for_status()
total = int(resp.headers.get('content-length', 0))
downloaded = 0
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
with open(output_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
downloaded += len(chunk)
self.request_count += 1
return output_path
def _escalate_auth(self, url, page_url):
"""逐级加强鉴权"""
# 级别 1:加 Origin
resp = self.session.get(url, headers={
'Referer': page_url,
'Origin': page_url.rsplit('/', 1)[0],
})
if resp.status_code == 200:
return resp
# 级别 2:重新访问首页获取 Cookie
self.session.get(page_url)
resp = self.session.get(url, headers={'Referer': page_url})
if resp.status_code == 200:
return resp
raise PermissionError(f"无法绕过鉴权,最终状态码: {resp.status_code}")
九、合规与温馨提示
- 本文讨论的反检测技术仅用于合法授权的数据采集和个人学习研究
- 绕过平台的反爬机制可能违反其服务条款,请在使用前阅读目标平台的 robots.txt 和 ToS
- 高频率请求可能对目标服务器造成压力,请合理控制请求频率
- 使用代理时注意代理服务商的合规性,避免使用通过非法手段获取的代理 IP
- 更多法律讨论见 下载视频算侵权吗?聊聊个人备份与版权的那条线
反爬和反检测是一场持续的军备竞赛。今天能用的伪装手段,明天可能就被检测出来。所以最重要的不是"找到终极方案",而是建立一套监控机制——定期用检测工具(如 tls.peet.ws、bot.sannysoft.com)检查你的脚本是否暴露了痕迹,一旦发现新的检测点,及时更新伪装策略。
本文由 VidDown 技术博客原创发布。VidDown 的解析引擎内置了上述多层反检测技术,让你在下载视频时不会被平台误判为爬虫。访问 VidDown 了解更多。