Python 爬虫实战:随机下发路径的资源怎么下载(CH-003)
- 1Python 爬虫入门实战:服务端渲染页面怎么取数(CH-001)
- 2Python 爬虫接口实战:翻页取完再累加(CH-002)
- 3Python 爬虫实战:随机下发路径的资源怎么下载(CH-003)本文
- 4Python 爬虫 TLS 指纹实战:requests 被挂住时怎么查(CH-004)
- 5Python 爬虫接口签名实战:参数签名 + 会话绑定(CH-005)
- 6Python 爬虫加密实战:ROT + Base32 + 凯撒 + HMAC-MD5 组合(CH-006)
- 7Python 爬虫字体映射实战:HTML 里一个字都没有(CH-007)
- 8Python 爬虫字体加密实战:码位混淆 + 自定义字体文件(CH-008)
- 9Python 爬虫验证码实战:AES 加密报文 + 每页算术验证码(CH-009)
- 10第 10 关:RSA 传密钥 + AES 加密报文(顺带一层 JavaScript 混淆)
- 11第 11 关:无限 debugger + VM 混淆 + 签名请求头
- 12第 12 关:接口下发 JS + 字符串混淆 + 内置 MD5 + 反调试
- 13第 13 关:点选验证码 + 一次一密的轨迹报文 + 行为风控
- 14第 14 关:Cookie 签名 —— 七层复合加密,和「密钥写在前端」这件事
- 15第 15 关:蜜罐与爬虫封禁 —— 数据是假的,陷阱是真的
本文是 LearnSpider 第 3 关的解密教程。这一关要把图片下载下来,答案是这张图片的字节数。真正的考点是:拿到接口给的
image_id/image_url之后,别把「字节」和「字符」搞混,也别拖太久(图片是随机下发的)。
现象:图片地址在页面源码里找不到
在挑战广场点开 CH-003,抽屉里有一个「获取图片」按钮。点一下,页面会请求:
GET /api/challenges/CH-003/image响应:
{ "challenge_id": "CH-003", "image_id": "e33a1790898939fa", "image_url": "https://spider.jsnote.top/api/challenges/CH-003/image/e33a1790898939fa"}图片本体在 image_url 上,但它不是磁盘文件名:
- 磁盘上图片一直叫
challenge-01.jpg、challenge-02.jpg(稳定名字,方便维护); - 对外暴露的是随机的
image_id,接口用它反查磁盘文件再send_file。
所以「猜路径下载」这条路是堵死的:每次都要先调一次接口拿 image_id + image_url。
目标是「字节数」不是「字符数」
图片是二进制,正确姿势是 len(response.content):
| 写法 | 结果 |
|---|---|
len(response.content) | ✅ 图片真实字节数 |
len(response.text) | ❌ 二进制被当文本解码,乱码 + 长度不对 |
len(response.text.encode("utf-8")) | ❌ 双重编码,更长,而且可能报错 |
而且这次提交不只是填一个数字:还要带上 image_id,服务端要靠它找到你这一张图去核对大小。
完整脚本
"""CH-003:答案 = 随机下发那张图片的字节数(提交时带 image_id)"""import requests
BASE = "https://spider.jsnote.top"PICK_URL = f"{BASE}/api/challenges/CH-003/image"
session = requests.Session()session.headers.update({ "Accept": "*/*", "Referer": f"{BASE}/", "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 + 登录 cookiesession.cookies.set("session", "<把浏览器里的 session cookie 粘到这里>", domain="spider.jsnote.top")
def main(): # 1) 先让服务端发一张图 pick = session.get(PICK_URL, timeout=10) pick.raise_for_status() data = pick.json() image_id = data["image_id"] image_url = data["image_url"] print("拿到图片:", image_id)
# 2) 用返回的地址下载本体(别自己拼路径:路径就是随机 image_id) image = session.get(image_url, timeout=20) image.raise_for_status()
size = len(image.content) # ⚠️ content(字节),不是 text(字符) print("Content-Type:", image.headers.get("Content-Type")) print("Content-Length(服务端声明的):", image.headers.get("Content-Length")) print("实际字节数:", size)
# 存的文件是个 JPEG:开头 FFD8 print("文件头:", image.content[:4].hex())
print(f"\n答案:{size} (提交时 body 带上 image_id = {image_id})") print("提交示例:POST /api/challenges/CH-003/submit " f'{{"answer": {size}, "image_id": "{image_id}"}}')
if __name__ == "__main__": main()为什么每一步都要在同一个 Session 里
GET /api/challenges/CH-003/image 会把这张图片记进当前会话(session["ch003_image_id"]):
- 如果你提交时没带
image_id,服务端会退回用会话里最近下发的那张 —— 换个 Session 就对不上了; - 图片是随机下发的,再点一次「获取图片」就是另一张、另一个大小。
所以正确顺序是:拿图 → 立刻下载 → 立刻算大小 → 立刻提交。跨天再提交,很可能「图片对不上」。
排查清单
- 别猜图片路径,先调
GET /api/challenges/CH-003/image。 - 用返回的
image_url原样请求(它是绝对地址,服务端按当前请求算出来的)。 - 大小用
len(response.content)。 - 顺手打一下
Content-Length,和实际字节数对不上就先怀疑传输被截断。 - 检查文件头(JPEG 是
ffd8,PNG 是89504e47),确认下下来的真是图片而不是错误页。 - 全流程放在一个 Session 里,别中途换 Cookie。
- 提交时带上
image_id。
常见问题
为什么我下载下来的字节数和浏览器看到的文件大小不一样?
先看是不是被 gzip 了(图片一般不会)。再看是不是拿 .text 数了字符 —— 二进制文件用文本方式读,长度必然不对。
同一张图片能反复下载吗?
能,只要 image_id 还有效(challenge_images 表里登记着)。但大小是固定的,重复下载不会让答案变化;换个 image_id 才会变。
接口返回的 image_url 为什么是完整域名?
服务端用 url_for(..., _external=True) 生成的,这样你在任何页面上都能直接拿去请求,不用自己拼域名。
最后提醒
下载资源类接口在真实站点上通常带鉴权与防盗链(Referer 校验、临时签名 URL)。练习里只在自己的靶场跑;真实站点的图片请遵守版权与使用条款。
去挑战广场提交,然后进 CH-004:这次的伪装不在数据里,而在你的网络握手上。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!












