eFootball 爬虫技术拆解:从 JA3 到 Cookie 大法
我的 eFootball 球员数据爬虫是怎么从 38,000 次请求砍到 1,177 次的——TLS 指纹伪装、Perlin 噪声限速、熔断退避,以及那个救了命的 cookie 发现。
eFootball 爬虫技术拆解:从 JA3 到 Cookie 大法
一份面向「知道我的爬虫能跑、但不清楚里面每个零件为什么存在」的技术笔记。
爬虫到底在干什么
你打开 pesdb.net/efootball/ 时,浏览器做的事很简单:
- 给服务器发一个 HTTP 请求——“把这页 HTML 给我”
- 服务器回一坨 HTML
- 浏览器把它画成你看到的表格
爬虫做的是同一件事,只是把第 3 步换掉了——不画给人看,而是从 HTML 里把球员数据抠出来,存进 CSV。
所以我的脚本只有两个动作,反复做:
- 抓(fetch):发请求,拿回 HTML——
fetch_html() - 解析(parse):从 HTML 里提取数据——
parse_list_page()
pesdb 有 37,650 个球员。如果每个球员一个详情页,就是 3.7 万次”抓”。太多了——而我这个项目最聪明的地方,就是把这个数字砍到了 1,177 次。
核心杀招:Cookie 列表页大法
什么是 cookie
Cookie 是网站存在你浏览器里的一张小纸条。每次你再访问这个网站,浏览器会自动把纸条夹在请求里一起发过去,网站就能”认出”你、记住你的偏好。
pesdb 的球员列表页默认只显示几列(名字、位置、总评……)。但这个网站允许你自定义要显示哪些列——而”你选了哪些列”这个偏好,它就存在一张叫 columns 的 cookie 里。
关键洞察:如果你在 cookie 里把全部 118 列都勾上,那么列表页一次就会把这一页所有球员的全部数据都吐出来——包括原本要点进详情页才看得到的所有能力值、技能、踢球风格。
这意味着什么
| 方案 | 要抓的页面数 |
|---|---|
| 笨办法:爬每个球员的详情页 | 37,650 次 |
| 我的方案:cookie + 列表页 | 1,177 次 |
请求量直接砍掉 97%。 这就是为什么我能用单 IP、不用代理、几分钟就跑完——根本原因不是别的优化,而是这一招把”要敲门的次数”压到了极低。
# _COLS_PARAM 里列出了全部 118 列
_COLS_PARAM = "id,club_number,…,P05,P06,P07"
def _get_session():
session.cookies.set(
"columns",
urllib.parse.quote(_COLS_PARAM, safe=""),
domain="pesdb.net",
)
这段就是在伪造那张”会员卡”,告诉 pesdb:“我这个用户想看全部 118 列”。
网站是怎么识别「你不是真人」的
既然爬虫就是”发请求”,那网站凭什么拦我、放行真人?因为程序发的请求和真浏览器发的请求,在很多细节上长得不一样。主要有四层:
| 层级 | 网站看什么 | 真人 vs 裸爬虫的区别 |
|---|---|---|
| ① TLS 指纹 | 建立加密连接时的”握手方式” | Python 默认库的握手和 Chrome 完全不同 |
| ② HTTP 头 | 浏览器型号、语言偏好… | 裸爬虫的头又少又假 |
| ③ 行为节奏 | 请求的频率、规律性 | 真人有快有慢有停顿,爬虫匀速猛敲 |
| ④ IP 地址 | 请求来源网络地址 | 单个 IP 短时间海量请求 = 可疑 |
我对这四层各有一件武器:
- ① → TLS 指纹伪装(curl_cffi)
- ② → 浏览器身份轮换(BrowserProfile)
- ③ → 拟人化限速 + 熔断退避
- ④ → 代理池(但我这个量用不上)
武器一:TLS 指纹伪装(JA3 / curl_cffi)
访问 https:// 网站时,浏览器和服务器要先握手,商量好用什么加密方式。握手的第一步,客户端会发一份”我支持这些加密套件、这些扩展、按这个顺序”的清单(ClientHello)。
不同的软件,这份清单的内容和顺序都不一样——Chrome 是一种排法,Firefox 另一种,Python 标准库又是另一种。把这份清单压缩成一串哈希值,就得到一个指纹。JA3 是最有名的指纹算法(还有更新的 JA4)。
Python 默认网络库(requests、urllib)的握手手感是”Python 味”的,和任何真浏览器都对不上。反爬系统一摸就知道:“这不是浏览器,是个脚本。“
我怎么解决的
curl_cffi 借用了 Chrome 同款的底层加密引擎(BoringSSL)和 Firefox 同款的(NSS),所以它发出的 ClientHello 和真 Chrome / 真 Firefox 一模一样。而且它不只骗 JA3——它连 HTTP/2 的通信指纹(另一种更新的识别手段)也一起复刻了。
from curl_cffi.requests import Session as CurlSession
session = CurlSession(impersonate="chrome136")
impersonate="chrome136" 就是在说:“握手的时候,假装你是 Chrome 136。“
稳定性提醒
impersonate 的值必须是你安装的那个 curl_cffi 版本认识的型号。我现在用的是 0.15.0,代码里 6 个型号都支持。但要小心:
- 换台机器装到更老的 curl_cffi,可能不认识
chrome136→ 直接报错 - 真实 Chrome 一直在升版本,我代码里最老的
chrome116在 2026 年已经是个”老古董指纹”,长期看应该把伪装目标整体往新版本挪
武器二:浏览器身份轮换(BrowserProfile)
就算每个请求都伪装成 Chrome,如果几千个请求全都是”同一个 Chrome 136、同一种语言设置”,也很可疑。
我准备了 6 套不同的浏览器+语言组合:
_PROFILES = [
("chrome136", "en-US,en;q=0.9"),
("chrome124", "en-GB,en;q=0.9,de;q=0.7"),
("chrome120", "en-US,en;q=0.8,zh-CN;q=0.5,zh;q=0.3"),
("chrome116", "ja,en-US;q=0.9,en;q=0.8"),
("firefox135", "en-US,en;q=0.9"),
("firefox133", "en-GB,en;q=0.8,fr;q=0.3"),
]
关键不是每次请求都换——频繁横跳反而不自然。我是在被限速、触发熔断之后才换一套身份重新出现。就像”这个身份被盯上了,那我换身衣服再进来”。
武器三:代理池(ProxyPool)——我用不上的那个
代理服务器就是一个”中间人”,帮你把请求转发出去。网站看到的 IP 是代理的 IP,不是你的真 IP。代理池 = 一群代理轮流用,分散 IP。
我的代码里有 ProxyPool 类,从 proxies.txt 读代理。但这个文件在我的运行环境里根本不存在,所以全程用真实 IP 直连。
而这完全没问题,因为 cookie 那招已经把请求总量从 3.7 万压到 1,177——这点量单 IP 完全扛得住。
代理解决的是”同一个 IP 请求太多”的问题;而我靠 cookie 把请求次数压得极低,从源头上就没产生这个问题。那封 anyIP 的销售邮件想卖给我的,正是我不缺的东西。
武器四:拟人化限速(Perlin 噪声)
机器人最容易暴露的特征是节奏太规律——每隔正好 1.5 秒敲一次,几千次分毫不差。真人不可能这样。
我没有用 time.sleep(1.5) 固定等。而是以 1.5 秒为基准,用 Perlin 噪声让每次的间隔平滑地上下浮动:
n1 = noise.pnoise1(self._t * 0.06) * 0.25 # 慢波:整体节奏漂移 ±25%
n2 = noise.pnoise1(self._t * 0.35) * 0.15 # 中波:短时爆发/暂停 ±15%
n3 = noise.pnoise1(self._t * 2.00) * 0.08 # 快波:单次请求抖动 ±8%
interval = base * (1 + n1 + n2 + n3)
普通随机数是”跳跃的”——这次 0.1 下次 0.9,毫无关联。Perlin 噪声是一种”平滑的随机”:值随时间连续、缓慢地起伏,像真实的潮汐、像人手的自然抖动。三种快慢不同的波叠在一起,整体就是有机的、不规律的节奏。
另外每隔 3590 个请求,还会随机插入 1.55 秒的”走神停顿”——模拟人类停下来想一想。
武器五:熔断器与退避(ThrottleGate / ReliabilityTracker)
前面都是主动伪装。这一节是”被发现后怎么自保”。
当请求太频繁,服务器通常回 HTTP 429(Too Many Requests)或 503(Service Unavailable)。
ThrottleGate:总闸门
一旦收到 429/503,ThrottleGate 就像一个总闸门啪地关上,让所有工人全部暂停,进入冷却。冷却时间是阶梯递增的:
COOLDOWNS = [20, 40, 60, 90, 120] # 第1次停20秒,第2次40秒……最多120秒
冷却结束后闸门重新打开,并顺手换一套浏览器身份再继续。
ReliabilityTracker:自适应退避
光停一下不够——如果一直在被限速,说明整体节奏就太快了,得永久性放慢:
- 每被限速一次,基准间隔乘以 1.6(变慢)
- 每成功 100 次,间隔乘以 0.95(慢慢恢复)
- 间隔有上下限,最慢不超过基准的 3 倍
这套”踩刹车—松刹车”让我始终贴着网站能容忍的最快速度跑。
| 组件 | 角色 | 时间尺度 |
|---|---|---|
ThrottleGate | 出事时全员急停冷却 | 短期(秒级) |
ReliabilityTracker | 调整长期巡航速度 | 长期(整轮渐变) |
HumanizedRateLimiter | 控制每一步的具体间隔 | 每个请求 |
并发:3 个工人同时干活
我用 3 个线程同时抓不同的页面——ThreadPoolExecutor 调度。为什么是 3 不是 30?开太多 = 对网站太凶 = 立刻被限速。3 是”够快”和”不惹事”之间的平衡。
3 个工人同时写 CSV、同时改进度,可能撞车,所以用锁保护:
with csv_lock:
writer.writerow(row)
每个工人还有自己独立的 session(threading.local()),各自的 cookie 和身份互不干扰。
断点续爬 + 仪表盘
每抓完 50 页存一次进度到 efootball_progress_v2.json。中途断电、报错、Ctrl+C——下次启动自动跳过已完成页,CSV 追加模式不覆盖。
仪表盘用 rich 库画的实时面板:进度条、ETA、当前身份、冷却状态、HTTP 错误统计。每 0.25 秒刷新。纯属好看——删掉它爬虫照样跑。
一次请求的完整旅程
看抓第 500 页这一次请求经历了什么:
- ThreadPoolExecutor 把任务派给某个空闲工人
_gate.wait()— 如果正在冷却,工人在这里等闸门打开_rate.wait()— Perlin 噪声算出的间隔停一下(约 1.5 秒上下浮动),偶尔走神更久_get_session()— 拿到本工人专属的、伪装成 Chrome 136 的会话,带好全 118 列的 cookie_proxy_pool.next()— 返回 None,直连- 用伪装的 TLS 指纹 + 浏览器头发请求
- 判结果:200 → 记成功并微微提速;429/503 → 退避 + 熔断冷却 + 换身份重试
parse_list_page()— 从 HTML 抠出几十个球员、每人 118 列- 抢到 csv_lock 后写入 CSV,标记第 500 页已完成
- 后台线程刷新仪表盘
1177 页各自走一遍这个流程(3 个工人并行),全部跑完就得到完整的 efootball_players.csv。
总结:三句话
- 真正的杀手锏是 cookie 列选择器——把请求量砍掉 97%,让其余一切变得轻松
- TLS 指纹伪装是请求层面的”换脸”,让脚本的握手长得像真 Chrome;身份轮换、拟人限速、熔断退避都是为了”既快又不惊动网站”
- 代理池我用不上,因为请求量低到单 IP 完全能扛——那封 anyIP 的邮件卖的正是我不缺的东西
工具栈:Python 3.12 · curl_cffi · BeautifulSoup · Rich · noise · ThreadPoolExecutor