平时喜欢研究爬虫的朋友,应该不少人都遇到过类似需求:看到某个收藏品平台上的图片挺好,想批量保存下来整理成资料。今天就拿卡淘平台举个例子,把从抓包到采集图片的完整链路走一遍。卡淘是专门做球星卡、收藏卡交易的平台,页面和APP里展示的卡片图清晰度高、信息密度大,很适合用来练习抓包和图片采集。这篇文章会从抓包工具配置讲起,一直写到 Requests 脚本保存图片,中间还会穿插我踩过的几个坑,适合有一定 Python 基础、想搞懂动态请求和图片下载原理的同学。
标题里看起来简单,可真做起来有几个地方会卡人:抓包工具配好了但看不到请求,接口找到了但图片地址拼不出来,图片能访问但代码下载时 403。这些问题我一个个拆开说。
1. 为什么选“抓包+采集图片”这套方案
1.1 抓包才是万能的入口
很多初学者习惯直接在网页上按 F12,去 Elements 里找<img src>,然后拿 requests 去抓 HTML。这个思路在小网站可以,但遇到卡淘这种动态加载的站点很容易翻车。页面上的图片不是一开始就写在 HTML 里的,而是网页加载完以后,JavaScript 再去调接口,拿到 JSON 数据,把图片地址动态渲染出来的。你直接 requests 那个 HTML,得到的只是一堆空壳 div,图片地址一个都看不见。
APP 就更不用说了,你根本没法打开开发者工具去“审查元素”,唯一能看到网络请求的方式就是抓包。抓包工具相当于一个中间人代理,所有请求都会经过它,你在工具里能看到客户端每次请求了哪个 URL、带了什么参数、服务器返回了什么内容。这个能力很关键,因为它让你绕开了“页面长什么样”,直接看到“数据从哪来”。用生活类比的话,网页是成品菜,抓包是进后厨,你能看到厨师放了什么料、开多大火,自然就知道菜是怎么做的。
所以我做图片采集类项目时,第一步永远是抓包,而不是急着写爬虫代码。先把接口理清楚,代码只是重复发送请求而已,难度会降一截。
1.2 卡淘平台的页面和数据特点
卡淘平台定位是球星卡、收藏卡交易,核心内容是卡片图片、价格、卖家信息。这类平台有个普遍特点:图片多、详情字段多,但接口设计相对规整。卡片列表一般是一个 JSON 接口,每一条记录里包含了卡片 ID、标题、价格、缩略图地址、详情图地址。而且图片通常放在 CDN 或对象存储上,地址会有比较明显的域名和路径规则。
另外,卡淘的图片加载是懒加载模式,你滚动页面时才请求后面的图片。这个特点决定了你不能只抓 HTML,因为 HTML 里只有前几屏的图片,后面全是接口二次触发出来的。只有把列表接口整个抓下来,按页码拿到所有数据,才能完整采集。
还要注意一点,卡淘的网页端和 APP 端接口可能不一样。网页端是 AJAX 请求,APP 端可能是 JSON 接口,两者的字段结构也有差异。我建议入门阶段先抓网页端,因为网页端请求头简单,没有 APP 那么多加密参数,适合先跑通流程。
1.3 工具选型:Fiddler、Charles、Requests 怎么配合
抓包工具我常用两个:Windows 上用 Fiddler,Mac 上用 Charles,两者本质都是中间人代理,只是界面风格不同。Fiddler 偏工具型,Charles 偏图形化,但核心功能都一样,都能看到完整的请求和响应,也都能导出 HAR 文件。
工具角色定位:
| 工具 | 用途 |
|---|---|
| Fiddler / Charles | 抓包、分析接口、查看响应结构 |
| Fiddler 的 Copy as cURL | 把接口请求直接复制成 curl 命令,方便转换到 Python |
| Python Requests | 根据抓包得到的接口编写批量采集脚本 |
| json 模块 / XPath | 从 JSON 或 HTML 中提取图片地址 |
把这些工具串起来就是完整流程:先抓包确认接口,再用 curl 转代码,最后写循环批量下载。这套流程不只适用于卡淘,其他动态网站、APP 甚至小程序,都能用同样的套路。记住:工具只是辅助,核心是你要能看懂接口、找到图片地址的变化规律。
2. 抓包环节:找到图片接口是关键
2.1 配置抓包环境(Fiddler / Charles 手机与 PC)
很多人在抓包第一步就栽了:工具开着,APP 完全没网,或者全是 CONNECT 请求,看不到实际内容。原因通常是 HTTPS 证书没配好。现在绝大多数接口都是 HTTPS,如果你不做中间人解密,抓包工具只能看到域名,看不到路径和参数。
Fiddler 配置流程我过一遍:
- 打开 Fiddler,菜单栏找到 Tools > Options > HTTPS,勾选 Decrypt HTTPS traffic。
- 如果提示安装证书,直接同意,并选择“受信任的根证书颁发机构”。
- 手机和电脑连同一个 WiFi,查看电脑的局域网 IP(命令行 ipconfig)。
- 手机 WiFi 设置里打开 HTTP 代理,填写电脑 IP 和端口 8888。
- 手机浏览器访问
http://电脑IP:8888,在页面里点 FiddlerRoot certificate 下载证书。 - 安装证书后,进入手机的系统证书信任设置,把刚刚安装的证书开启完全信任。Android 9 以上和 iOS 都需要这一步,否则 HTTPS 请求依然看不到内容。
这里有个细节:Android 7 之后,很多 APP 默认不信任用户证书,即使安装证书也可能抓不到包。轻量解决方案是用 Charles 配合手机上的“安装到系统证书”操作,或者直接用抓包工具自带的模拟器。如果你的目标就是卡淘网页版,直接在电脑上用浏览器打开卡淘,然后让 Fiddler 作为系统代理也可以,比手机配置简单,适合入门。
Charles 的配置逻辑一样,区别是默认端口为 8888,证书下载地址是http://charlesproxy.com/getssl。手机和电脑在同一个局域网的前提下,设置代理后访问这个地址下载证书即可。
2.2 卡淘接口的分析思路
配置好抓包工具后,先在电脑浏览器里打开卡淘的卡片列表页,随便往下滚动几屏。这时回到 Fiddler 里,你会看到密密麻麻的请求。不要慌,先按“Filter”或者按域名过滤,把图片 CDN 域名排除掉,只看主站接口。
我习惯按这几个步骤筛:
- 看请求类型:找
GET请求,大部分列表数据都是 GET。 - 看响应类型:找后缀带
.json或者Content-Type: application/json的请求。 - 看请求路径:路径里包含
list、search、card、goods这类关键词的优先看。 - 在 Fiddler 右侧选中请求,点击“JSON”选项卡预览响应,如果能看到
imageUrl、imgList这些字段,那就是目标接口。
定位到目标接口后,右键复制Copy as cURL,先保存到文本文件里。这个操作在后续构造 Python 请求时会非常省力,因为 curl 命令里包含了全部请求头、Cookie 和参数,你只需要稍作转换就能变成 requests 代码。
2.3 从响应数据里定位图片真实地址
拿到接口响应后,通常是一长串嵌套 JSON。举个例子,接口返回结构可能是:
{ "code": 0, "data": { "page": 1, "list": [ { "goodsId": "12345", "title": "2023 Panini Prizm Card", "cover": "https://img.katao-example.com/12345.jpg?x-oss-process=image/resize,w_300" } ] } }这个cover字段就是缩略图地址,注意地址尾部可能带处理参数,比如?x-oss-process=image/resize,w_300。这种参数会导致你拿到的不是原图,图片尺寸被压缩过。想拿原图的话,可以先把问号后面的部分去掉再请求。但有些 CDN 会校验参数,去掉后可能返回超大图,或者干脆 403,这时候需要自己实测一下。
有的接口里图片不是一个地址,而是一个数组,比如imgList,里面包含多张高清图。这时要用循环提取,不要只取cover,否则你会丢掉详情页里的高清图。
图片地址如果只写了路径没写域名,比如/group1/M00/AA/123.jpg,那还需要在前面拼接主域名。具体域名从哪里找?很简单,在响应里搜索http,看看其他字段是不是都指向同一个域名,把那个域名取出来拼上去。
3. 爬虫代码实战:从接口到图片文件
3.1 构造请求与关键参数
抓包搞定接口后,就可以写 Python 代码了。这里我强烈建议先用抓包工具里复制出的 cURL 来验证接口,直接在终端跑一遍 curl,确认返回数据无误后,再转换成 requests 代码。
举个例子,你复制出来的 curl 大概长这样:
curl 'https://api.katao-example.com/card/list?page=1&size=20' \ -H 'User-Agent: Mozilla/5.0 ...' \ -H 'Referer: https://www.katao-example.com/' \ -H 'Cookie: session_id=abc123'转换成 Python:
import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", "Referer": "https://www.katao-example.com/", "Cookie": "session_id=abc123" } url = "https://api.katao-example.com/card/list" params = { "page": 1, "size": 20 } resp = requests.get(url, headers=headers, params=params, timeout=10) print(resp.status_code) print(resp.text[:500])这里有两个重点。第一,headers里的Referer和User-Agent尽量不要省略,很多图片接口和列表接口都会校验这两个字段,少了可能拿不到数据。第二,建议用requests.Session()而不是裸的requests.get(),因为 session 会自动保存 Cookie,后续请求同一个域名的图片时,Cookie 也能带过去,减少 403 概率。
还有一个小细节:如果响应内容有乱码,先别急着改 encoding,看一下resp.apparent_encoding或者响应头里的charset。大多数 JSON 接口是 UTF-8,直接用resp.json()没问题,但如果是网页 HTML 响应,可能是 GBK,就需要按实际编码转换。
3.2 解析 JSON 并提取图片 URL
拿到 JSON 后,解析本身就是一层层地取字段。用resp.json()得到字典,然后按结构取值:
data = resp.json() card_list = data["data"]["list"] for item in card_list: goods_id = item["goodsId"] cover_url = item["cover"] img_list = item.get("imgList", []) or [] # 只处理有效的图片地址 all_imgs = [cover_url] + img_list for img_url in all_imgs: if not img_url.startswith("http"): img_url = "https://img.katao-example.com" + img_url print(goods_id, img_url)这一步逻辑不复杂,但容易踩字段不存在的坑。很多卡片可能没有imgList,直接item["imgList"]会 KeyError。用.get()加默认值是最稳妥的写法拉。另外,列表里某些商品可能下架了,cover字段是空字符串,也要跳过。
实际项目里我不会只打印,而是写一个生成器函数,专门负责翻页并提取所有图片地址。
def fetch_all_image_urls(max_page=10): session = requests.Session() session.headers.update(headers) seen = set() for page in range(1, max_page + 1): resp = session.get( url, params={"page": page, "size": 20}, timeout=10 ) if resp.status_code != 200: continue data = resp.json() card_list = data.get("data", {}).get("list", []) if not card_list: break for item in card_list: cover = item.get("cover", "") img_list = item.get("imgList", []) or [] for img_url in [cover] + img_list: if img_url and img_url not in seen: seen.add(img_url) yield img_url # 控制请求节奏 time.sleep(0.8)注意我用了seen集合去重,因为翻页时可能同一张图出现多次(比如热门卡片被推荐到多个页面)。去重能大大减少重复下载,节省时间。
3.3 下载保存与并发优化
图片地址收集好后,进入下载环节。最简单的下载代码长这样:
def download_image(img_url, save_path): headers_for_img = { "User-Agent": headers["User-Agent"], "Referer": "https://www.katao-example.com/" } resp = requests.get(img_url, headers=headers_for_img, timeout=15) if resp.status_code == 200: with open(save_path, "wb") as f: f.write(resp.content) else: print(f"下载失败: {img_url} -> {resp.status_code}")但直接一把梭下载几千张图片会非常慢,而且容易触发反爬。建议用线程池加一点并发,但不要太大。
import os from concurrent.futures import ThreadPoolExecutor import time save_dir = "katao_images" os.makedirs(save_dir, exist_ok=True) def download_one(img_url): # 用图片地址的哈希做文件名,避免特殊字符问题 file_name = str(hash(img_url)) + ".jpg" save_path = os.path.join(save_dir, file_name) if os.path.exists(save_path): return try: download_image(img_url, save_path) except Exception as e: print(f"异常: {img_url}, {e}") time.sleep(0.3) with ThreadPoolExecutor(max_workers=5) as pool: pool.map(download_one, all_image_urls)max_workers=5是我实测比较稳的值,既能加速下载,又不会对服务器造成太大压力。如果你想要更完整的文件名,可以用goodsId + 序号来命名。但需要先把业务 ID 传进函数,逻辑上稍微复杂一点,不过能避免 hash 命名导致后续找不到对应卡片的问题。
3.4 反爬应对与频率控制
采集图片这件事,难点不在“下载”,而在“别被封”。卡淘接口虽然不会像大厂一样有复杂风控,但请求频率一旦过高,照样会返回 403 或者验证码。
我常用的几招:
- 每次请求之间加随机延迟,用
time.sleep(random.uniform(0.5, 1.5)),不要固定延时。 - 同一个 Session 复用,减少握手次数。
- 控制并发数,5 个线程足够,不要在 ThreadPoolExecutor 里开 50 个。
- 设置重试机制。比如
requests的urllib3 Retry或者简单循环,捕获超时和 5xx 状态码后自动重试 2 到 3 次。
简单重试代码:
def get_with_retry(session, url, headers, retries=3): for i in range(retries): try: resp = session.get(url, headers=headers, timeout=15) if resp.status_code in (200, 302): return resp except requests.exceptions.RequestException: pass time.sleep(1) return None请求头不用每次换,保持稳定有时候反而比随机换更不容易被识别。很多平台风控看的是你是否像“真人”,固定 IP 固定 UA 固定频率,反而更像一个正常用户。真正容易触发风控的是没有规律的大并发和高频请求。
4. 常见问题与排查技巧实录
4.1 抓不到包的几种典型原因
抓包工具配置半天,结果 APP 或浏览器像没走代理一样,这是最常见的坑。我整理了一个速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| APP 显示网络异常 | 代理配置错误或端口不对 | 检查电脑 IP 和 Fiddler 端口 8888,手机上重新设置代理 |
| 只能看到 CONNECT 请求 | HTTPS 证书未安装或未信任 | 重新安装证书,iOS 还需要在“证书信任设置”里打开完全信任 |
| 请求时有时无 | 手机和电脑不在同一网段 | 确认连接同一个 WiFi,关掉电脑防火墙或者放行 8888 端口 |
| 浏览器打开没有代理效果 | 系统代理未开启 | 在 Fiddler 菜单 Tools > WinConfig 里开启 Windows 代理 |
还有一类情况,卡淘 APP 可能做了“禁止代理”检测,一旦检测到 WiFi 代理就拒绝连接。这种场景下,建议直接用电脑浏览器打开卡淘网页版来抓包,流程简单,而且入门阶段完全够用。
4.2 图片 403 / 防盗链怎么处理
接口能拉到数据,图片地址也能复制到浏览器直接打开,但一到代码下载就 403。这种情况十有八九是防盗链。
防盗链的原理其实很简单:网站检查 HTTPReferer字段,如果不是来自自己的域名,服务器就拒绝返回图片资源。你的浏览器能打开,是因为它会自动带上卡淘的页面地址作为 Referer,而 Python 默认是不带 Referer 的。
处理方式:
img_headers = { "User-Agent": "Mozilla/5.0 ...", "Referer": "https://www.katao-example.com/" }在下载图片的请求中主动加上Referer,指向图片所在页面的域名,一般就能解决。如果加了 Referer 还是 403,再看一下图片地址是不是 302 跳转到另一个域名,跳转后的域名可能要求不同的 Referer 规则。这就不太好一概而论,需要实际抓包看响应头。
4.3 动态参数 / 签名问题
有时候列表接口里会出现sign、ts、nonce这类参数。这类参数一看就知道是签名校验,目的就是防止有人直接构造请求。遇到这种情况,先不要头铁去逆向 JS,先看是不是真的必须带。
有些平台的签名参数是可选的,你手动把 URL 里的签名删掉,照样能返回数据。有些平台则是必带的,不带签名直接 401。区分方法很简单:构造一个不带签名的请求试试,返回正常就说明校验形同虚设;返回错误就说明需要处理签名。
如果必须带签名,入门阶段最省力的方式是“复用真实请求”。什么意思?就是你在卡淘网页上正常操作,抓包工具会拿到一个带着真实sign的完整 URL,你把这个 URL 存下来直接请求,短期内有缓存时间,可以用来小规模采集。但这种方式的局限很明显:签名会过期,而且你没法随意翻页。真要突破签名,需要去看前端 JS 里怎么生成参数,属于另一个更深的主题,这里不展开。
4.4 容易踩的坑:编码、路径、去重
编码问题不只在网页里有,JSON 接口偶尔也会遇到。如果使用resp.json()抛异常,可以先打印resp.text看看是不是被转义了。一般在响应头里会有charset=utf-8,但有些老接口是gbk,可以采用resp.content.decode("utf-8", errors="ignore")兜底。
路径问题主要在 Windows 上踩。图片地址里如果包含?、/、:,这些字符不能直接作为文件名。所以我在上面代码里用了hash(img_url)作为文件名,就是避免这个坑。你也可以用goodsId作为文件名,但要注意 goodsId 可能是字符串也可能带空格,先做一次strip()。
去重问题的核心是避免重复下载。我建议在生成图片地址阶段就去重,用set()收集所有 URL,而不是下载阶段再判断。下载阶段判断os.path.exists虽然也可以,但多几次磁盘访问,影响效率。更好的做法是在代码里维护一个已经成功下载的 URL 列表,进程重启后还能继续跑。
5. 合规与个人经验(结尾)
5.1 关于数据使用,必须先说清楚
采集图片这件事,自己研究技术没问题,但用的时候要克制。卡淘平台的图片属于平台用户和平台方,版权归属不一定你下载了就属于你。批量抓取别人平台的资源,无论目的是整理资料还是二次发布,都可能违反平台用户协议,甚至涉及版权问题。
我写这篇内容的初衷是拆解“抓包 + 动态接口分析”这套通用技能。你在学习时可以把卡淘当成练习对象,但不要大规模、长时间、高频率地采集,更不要用采集到的图片去做商业用途。真实项目里如果需要卡淘数据,建议先找官方开放接口,或者和平台方沟通合作,这才是正道。
5.2 我的一些实操心得
做了这么多采集项目,图片类采集最值得记住的一句话是:先找接口,再写代码。很多人急着写爬虫,结果被反爬卡得怀疑人生,回头发现只是没找对数据源。抓包工具就是你找数据源的地图,多用几遍就会形成本能反应。
具体到卡淘这类平台,我个人的经验是:先用网页端抓包跑通整个流程,再考虑 APP 端。网页端请求头简单,没有 app 层的加解密,入门效率高。跑通后再去抓 APP 端会容易很多,因为你已经知道要关注哪些字段、哪个接口是核心。
另外,抓包得到的 HAR 文件建议保存下来。不要只存在 Fiddler 里,可以右键导出为 HAR 文件。以后想重新写解析逻辑,直接用 HAR 文件里的记录就能还原接口,比重新抓包方便得多。我自己遇到接口字段变动时,第一反应就是翻旧 HAR 对比差异。
最后分享一个小技巧:批量采集前一定要先用 1 页数据跑通全流程。比如先请求第 1 页,只下载 5 张图试试水,确认文件名、路径、下载成功率都符合预期,再去循环所有页面。我就是吃过“下载到一半才发现图片全是缩略图”的亏,当时 5000 多张图白下了,白白浪费将近一个小时。这个小步骤只要花两分钟,能省下后面一大半调试时间,值得养成习惯。