视频加载失败

Python 爬虫 TLS 指纹实战:requests 被挂住时怎么查(CH-004)

1269 字
6 分钟
Python 爬虫 TLS 指纹实战:requests 被挂住时怎么查(CH-004)

本文是 LearnSpider 第 4 关的解密教程。这一关的接口数据本身是明文 JSON,难点在你根本连不上:requests 会一直卡到超时,服务端什么都不返回。答案是 5 页共 50 条关键词的 CPC 之和(保留 2 位小数)。

现象:请求「不报错,也不返回」#

先按常规思路来:F12 找到接口,复制成 cURL,粘到终端跑:

Terminal window
curl "https://spider.jsnote.top/api/challenges/CH-004/cpc?page=1" -H "Cookie: session=..."

结果不是 403、不是 200,而是一直挂着,然后:

curl: (28) Operation timed out

这才是这一关真正的坑:服务端把连接挂住了(黑洞)。它不告诉你为什么,你只能自己判断。

顺手验证一下:浏览器打开靶场页 /cpc 完全正常。也就是说「浏览器能拿到,脚本拿不到」—— 那问题一定出在浏览器和脚本的差异上,而不是参数、Cookie、路径。

差异在哪:TLS 握手里的密码套件顺序#

HTTP 头可以伪造(UA、sec-ch-ua 随便抄),但 TLS 握手里的 ClientHello 抄不了:

客户端首个密码套件
Python requests / httpx / urllib3(OpenSSL 默认)TLS_AES_256_GCM_SHA384
Chrome / EdgeTLS_AES_128_GCM_SHA256
curl视版本而定,多数也不是 Chrome 的顺序

服务端在 nginx 上把 $ssl_protocol / $ssl_ciphers / $ssl_curves 透传给了后端,后端口只看第一个套件:

  • 含 AES256 → 判「不是浏览器」,把连接挂住;
  • AES128 / CHACha20 → 放行。

所以这不是「请求头问题」,也不是「代理问题」——HTTP 层根本没有机会参与。

两个容易踩的细节#

  1. 端口也有讲究:走 443(HTTPS)才有 TLS 指纹可谈。明文兜底端口 :5787 会直接返回「请通过 HTTPS(443 端口)访问本站」,因为那台机器上没有握手信息。
  2. 本地开发是例外:本机用 Vite 代理调试时,代理会预置三个 X-TLS-* 头模拟生产环境,所以本地能跑通;你用脚本直连本地后端反而会被判「不是 HTTPS」。本地想调试就把黑洞时长设成 0(LEARNSPIDER_TLS_TARPIT_SECONDS=0),它会退回 403,而不是挂你 100 秒。

解法:让握手长得像 Chrome#

最省事的是 curl_cffi:

Terminal window
pip install curl_cffi

它的 impersonate="chrome" 会把 ClientHello(套件顺序、扩展、曲线)整套伪装成 Chrome,服务端看第一个套件就是 TLS_AES_128_GCM_SHA256,直接放行。

完整脚本#

"""CH-004:答案 = 5 页 50 条关键词的 CPC 之和(保留 2 位小数)
为什么 requests 不行:
requests 走 OpenSSL 默认套件顺序,ClientHello 首个套件是 TLS_AES_256_GCM_SHA384
(Chrome 是 TLS_AES_128_GCM_SHA256)。服务端按首个套件判定客户端类型,
不合规就把连接挂住(什么都不返回),所以你只能等到自己的 timeout。
curl_cffi 的 impersonate="chrome" 连 ClientHello 一起伪装,才能拿到数据。
"""
from curl_cffi import requests
BASE = "https://spider.jsnote.top"
API_URL = f"{BASE}/api/challenges/CH-004/cpc"
TOTAL_PAGES = 5
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": f"{BASE}/cpc?challenge=CH-004",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
),
}
session = requests.Session(impersonate="chrome") # 关键就这一行
session.headers.update(headers)
session.cookies.set("session", "<把浏览器里的 session cookie 粘到这里>",
domain="spider.jsnote.top")
def main():
total = 0.0
rows = 0
for page in range(1, TOTAL_PAGES + 1):
response = session.get(
API_URL, params={"page": page}, timeout=20, impersonate="chrome"
)
response.raise_for_status()
payload = response.json()
items = payload.get("items", [])
rows += len(items)
page_sum = sum(float(item["cpc"]) for item in items)
total += page_sum
print(f"第 {page} 页:{len(items)} 条,本页 CPC 合计 {page_sum:.2f},"
f"累计 {total:.2f}")
print(f"\n共 {rows} 条,CPC 总和:{round(total, 2)}")
if __name__ == "__main__":
main()

参考实现见 参考代码/s4.py。

排查清单#

  1. 请求「不报错也不返回」→ 先怀疑服务端黑洞,而不是自己网慢(把 timeout 设成 5 秒更快确认)。
  2. 浏览器能开、脚本不行 → 找两者差异:Cookie、请求头、TLS 指纹、HTTP 版本。
  3. 抄全套 HTTP 头没用 → 说明检查点在 TLS 层。
  4. 确认自己走的是 443 的 HTTPS,不是明文兜底端口。
  5. 用 curl_cffi 的 impersonate="chrome"(不是只改 UA)。
  6. 指纹合规了还是 401/403 → 这时候才轮到 Cookie 与参数。
  7. 接口一次只给一页,记得翻完 5 页(网页上只放出前 4 页)。
  8. 求和后保留 2 位小数(判题容差 0.05)。

常见问题#

我改了 User-Agent 和 sec-ch-ua,为什么还是卡住?#

因为那些是 HTTP 头。这一关看的是 TLS 握手里的东西,HTTP 层改什么都到不了判定逻辑。

为什么 curl 也不行?#

不同版本的 curl 用的是本地 OpenSSL 的套件顺序,多数情况下和 Chrome 不一致。要么升级/换 curl,要么直接用 curl_cffi。

impersonate="chrome" 会不会影响别的请求?#

只影响你显式声明的这个 Session。日常抓别的接口不必开,避免不必要的麻烦。

为什么判题的容差是 0.05?#

CPC 是两位小数浮点数,累加会有误差。留一点容差比要求「完全相等」更合理。

最后提醒#

TLS 指纹是真实存在的反爬手段(CDN、风控产品都在用)。它的意义是把「没有浏览器指纹的客户端」和「正常用户」分开 —— 逆向它属于学习范畴,滥用会直接影响服务方。

在自己的靶场里练,别拿去扫别人的站点。去挑战广场提交,然后进 CH-005:数据能拿到了,但每一条都要签名。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Python 爬虫 TLS 指纹实战:requests 被挂住时怎么查(CH-004)
https://jsnote.top/posts/ls-ch4/
作者
xiajiao
发布于
2026-10-02
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
Python 爬虫实战:随机下发路径的资源怎么下载(CH-003)
爬虫靶场本文是 LearnSpider 第 3 关的解密教程。这一关要把图片下载下来,答案是这张图片的字节数。真正的考点是:拿到接口给的 image_id / image_url 之后,别把「字节」和「字符」搞混,也别拖太久(图片是随机下发的)。
2
Python 爬虫入门实战:服务端渲染页面怎么取数(CH-001)
爬虫靶场本文是 LearnSpider 第 1 关的解密教程。它练的不是「怎么破加密」,而是先判断数据到底在哪:这一关的数据只在服务端渲染出来的 HTML 里,源码里没有接口。答案是第一页 HTML 源码的字符数。
3
Python 爬虫接口签名实战:参数签名 + 会话绑定(CH-005)
爬虫靶场本文是 LearnSpider 第 5 关的解密教程。这一关的接口参数要签名,而且取数只认一个「会话标识」(uuid)—— 这个 uuid 是页面自己生成的、并且已经在后端和你的账号绑定过。答案是 5 页共 50 个数字的总和。
4
Python 爬虫接口实战:翻页取完再累加(CH-002)
爬虫靶场本文是 LearnSpider 第 2 关的解密教程。这一关的数据不在 HTML 里,只能从分页接口拿;答案是全部电影「评分总人数」的累加值。重点是:怎么找到接口、怎么确认它没把总量直接给你、以及怎么把 21 页稳稳翻完。
5
Python 爬虫字体映射实战:HTML 里一个字都没有(CH-007)
爬虫靶场本文是 LearnSpider 第 7 关的解密教程。这一关把大众点评那套「字体库 + 坐标」搬了过来:页面 HTML 里只有随机类名,文字全在字体库(SVG 文字地图)里,而「每个类名要把图挪多少像素」是接口加密下发的。目标是拿到 5 段…
随机文章随机推荐

评论区

Profile Image of the Author
xiajiao
写爬虫,也写防爬虫的靶场。
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
15
分类
1
标签
33
总字数
22,317
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录