做插画备份这件事我前后折腾过三轮。第一轮用最土的办法手动右键另存存了不到两百张就放弃了第二轮写了个脚本硬跑跑到第三天账号被临时限制本地还留下一堆 0 字节的 jpg第三轮才把整件事拆成“会话管理 节奏控制 落盘结构”三块做成一个能挂后台、断了能续、跑一个月不用管的东西。这篇就把这套东西完整拆开讲——如何把关注的画师的历史作品和后续新作批量下载到本地怎么把登录会话维持住不失效为什么人机验证压根不该去“过”而应该去“避”以及那个每次换机器必跳出来的 SSL 证书报错到底该怎么归类和修。适合谁来读写过一点 Python、能看懂 requests 基本用法、但一上规模就翻车的人。纯新手也能跟着抄但建议先把第 2 章和第 5 章看完再动手那两章决定了你这套东西能不能长期跑老手可以直接跳到第 3、4 章看我踩过的具体坑。1. 动手前先算清楚6 万张作品、13 万个文件、100GB 磁盘很多人写爬虫的第一步是打开编辑器我的第一步是拿计算器。这不是强迫症是血泪——我第二轮翻车有一半原因就是跑到第三天才发现磁盘只剩 4G脚本开始疯狂写半截文件。1.1 先给需求做个规模估算假设你关注了 300 位画师这个数字不算多常年混插画社区的人关注列表轻松过千。按人均 200 件作品算总作品量是 6 万其中大概 15% 是多图作品人均 3 页那么图片文件数约等于60000 60000 × 0.15 × (3 - 1) ≈ 78000再算上缩略图和头像实际落在 10 万到 13 万个文件之间。单张原图平均按 800KB 估有的插画师一张 5MB有的 300KB取中位总容量 60GB 到 100GB 量级。这两个数字直接决定了后面所有设计规模指标估算值对应的设计决策作品数6 万必须有本地清单表不能靠文件夹名反查文件数10 万单目录不能放超过 2 万个文件否则 ls、资源管理器都会卡死总容量60-100GB落盘前先检查剩余空间低于阈值暂停单次请求耗时200-800ms全量跑完至少 8-15 小时必须支持断点续传我第一次跑的时候没做目录分片13 万个文件全堆在一个文件夹里结果文件管理器打开要转圈半分钟脚本里用os.listdir判断是否已下载也要几秒一次。后来改成按画师 ID 分目录单个目录最多几百个文件一切就顺了。1.2 一次性全量备份和长期增量同步是两套架构这两个需求长得像实现差别很大最忌讳混在一起写。一次性全量备份的优化目标是吞吐可以容忍较高的并发可以不做精细的去重跑完就完事,代码写崩了重跑一遍也无所谓。这种场景反而不需要复杂的清单表一个简单的“文件存在就跳过”的判断就够了。长期增量同步的优化目标是稳定和低打扰每天或每周跑一次只拉新增作品请求量必须压到极低不能让站方觉得你在刷。这种场景清单表是必需品而且要记录每次同步的时间点和游标。我最终的方案是做成同一个程序、两种模式--mode full首次初始化把关注列表里所有画师的历史作品全拉一遍并发拉高跑通宵。--mode incr日常增量只查每个画师作品列表的第一页比对本地最大作品 ID有新的才往下翻。增量模式的日请求量大概是这样300 位画师 × 每人 1 次列表请求 300 次加上平均每天 50 件新作品的详情和图片总共不到 500 次请求。这个量级分散在一天里跟正常用户刷页面的行为没区别。1.3 明确哪些事不做这一段我必须写在最前面因为它是整个项目能不能长期活下去的前提。不碰需要额外权限才能看的内容。有些作品是限定可见的有些高清原图是付费订阅才能拿的。这些东西哪怕技术上能碰到也不该碰——它既违反站方规则也是对创作者最直接的伤害。我的脚本里会检查作品的可访问状态字段不可访问的直接标记跳过并存日志。不做验证码的自动识别。这一点第 3 章会详细讲结论先给技术上不划算风险上不划算效果上更不划算。不做二次分发。下载到本地是自己的备份打包上传到网盘分享、做成图站、拿去训练模型这些都不在“备份”的范畴里。我在脚本里给每个画师目录生成一个 README写明来源和作者信息就是为了提醒自己这堆东西的归属。不追求“零遗漏”。有些作品会被作者删除有些画师会注销账号有些图片链接会失效。这些是常态不是 bug。我的清单表里专门有一个status字段记录ok / gone / forbidden跑完之后统计一下比例如果gone超过 5%那说明是我的解析逻辑出问题了而不是真的删了那么多。提示先把“我要什么”和“我不要什么”写进配置文件跑起来之后再改成本很高。我见过太多人一开始只想备份自己的收藏跑着跑着就变成“顺手把排行榜也抓了”然后就被限速了。2. 登录态是整个项目的地基也是最容易塌的地方这一章是全文最核心的部分。后面所有关于反爬、关于人机验证的讨论前提都是你的登录态是健康的。登录态一旦不稳你会把所有问题都误判成“被反爬了”然后去做一堆没用的对抗。2.1 三条登录路径我为什么放弃了“模拟填表”要拿到登录后的会话理论上只有三条路路径实现难度稳定性维护成本我的选择官方开放授权OAuth 类低高极低首选有就用手动登录后导出 Cookie 复用低中低次选实际采用程序化提交登录表单高低极高放弃第一条路最好很多社区都提供开放接口走标准授权流程拿 tokentoken 有明确的过期时间和刷新机制一切都很干净。缺点是权限范围通常受限有些接口不开放。第二条路是我实际用的在浏览器里正常登录从开发者工具里把 Cookie 导出成 JSON 或者 Netscape 格式的 cookies.txt脚本启动时加载。这条路的关键认知是——你复用的是“你自己手动登录产生的会话”不是“破解登录”。登录这一步由人完成脚本只负责携带这个会话去发请求。这个边界要划清楚。第三条路我试过放弃了。原因很实在登录接口的表单里通常带动态生成的加密字段这些字段的生成逻辑会随站方前端更新而变化你每跟一次更新就要重写一次更麻烦的是登录接口本身往往就是人机验证的重点防护对象你在这儿撞墙的概率比在任何地方都高。投入产出比极差。2.2 Cookie 持久化的几个工程细节导出的 Cookie 直接塞进 headers 里能用但很脆。我现在的做法是封装一个 Session 工厂import json import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(cookie_path: str, ua: str) - requests.Session: s requests.Session() with open(cookie_path, r, encodingutf-8) as f: jar json.load(f) # [{name: .., value: .., domain: ..}, ...] for c in jar: s.cookies.set(c[name], c[value], domainc[domain]) s.headers.update({ User-Agent: ua, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://site-domain/, }) retry Retry( total3, backoff_factor1.5, status_forcelist[429, 500, 502, 503, 504], allowed_methods[GET], respect_retry_after_headerTrue, ) adapter HTTPAdapter(max_retriesretry, pool_connections8, pool_maxsize8) s.mount(https://, adapter) return s三个细节值得单独说。第一UA 必须和导出 Cookie 时的浏览器一致。这是最容易崩的点。我用 Chrome 登录导出 Cookie脚本里却写了个python-requests/2.31的默认 UA结果第一个请求就跳登录页。原因很简单服务端会话记录里存了“这个会话是用什么客户端建立的”你换了客户端但会话还是那个会话就是明显异常。第二Cookie 不要写进代码也不要进 git。我把它放在~/.config/mybackup/cookies.json权限设成 600。曾经有次差点把带会话票据的代码推上去从那以后.gitignore第一条就是这个文件。第三不要打印 Cookie 到日志。调试的时候图方便print(session.cookies.get_dict())结果日志文件里全是会话票据。日志是要长期留存的东西这个习惯必须改掉最多打一下 cookie 的 name 列表和 value 的哈希前六位。2.3 登录态失效的三种表现以及怎么自动判定会话不会突然消失它是有征兆的。我总结下来是三种第一种接口返回 200但 body 里带错误字段。很多接口的设计是 HTTP 层永远 200业务错误放在 JSON 里。这种情况最阴险因为你的重试逻辑、状态码判断全都认为“成功了”实际拿到的是个空对象最后表现为“下载了 0 张图但脚本说全部完成”。第二种请求被 302 重定向到登录页。这种好判断设allow_redirectsFalse看一眼 Location 就行。第三种返回 200 且是个正常 HTML但其实是通用首页。这种最难判因为页面结构看起来没错。判断方法是检查响应里有没有你的用户标识或者检查返回的内容长度是不是和未登录时一样。我的做法是设一个哨兵请求每次批量任务开始前先请求一个需要登录、返回体极小、结果稳定的接口比如“我自己的账号基本信息”。判定逻辑def check_login_alive(s: requests.Session) - bool: r s.get(https://site-domain/ajax/me, timeout10, allow_redirectsFalse) if r.status_code in (301, 302, 401, 403): return False if r.status_code ! 200: return False # 交给上层重试不当成失效 try: data r.json() except ValueError: return False # 返回了 HTML基本就是掉登录了 return bool(data.get(user_id)) # 业务层字段按实际接口调整哨兵不通过就直接sys.exit(2)让人工介入去重新导出 Cookie。千万不要写“检测到失效就自动重新登录”的逻辑那就是第 2.1 节说的第三条路你会陷进一个无限循环。注意哨兵请求不要设成每个请求都跑一遍。那样请求量翻倍还容易把哨兵接口本身撞出限制。我的做法是每处理完一位画师跑一次300 位画师一天跑 300 次哨兵完全在合理范围内。3. 人机验证的正确解法是“不触发”不是“过验证”这个话题很多人上来就问“怎么识别那个点选图”。我的回答一直是一样的别识别。这一章讲清楚为什么以及不识别的情况下怎么把活干完。3.1 先把触发的真实原因找出来人机验证组件不是随机弹的它背后有一套风控评分。我观察了几个月触发率和我以为的因素关系不大和这几个因素关系很大请求速率。这是第一因素占我遇到的情况的七成以上。具体阈值不好说但我实测下来对详情类接口保持 1 秒 1 次、对图片 CDN 保持 3-5 并发连续跑几个小时都没事一旦把详情接口压到 1 秒 5 次十分钟内必弹。**访