亿企赢逆向手记(五):大规模采集的工程化——分页、断点续传、熔断
两个采集接口的完整入参、分页双判据、JSONL 断点续传、连续失败熔断——决定几千客户的活能不能干完。
「亿企赢逆向手记」收尾篇。前四篇解决「怎么进去」,这篇讲「进去后怎么把几千个客户、每个再翻好几页的数据稳稳跑完」——含两个采集接口的完整入参、分页双判据、JSONL 断点续传、连续失败熔断。自己账号的数据导出自动化,技术学习向。
逆向出接口只是开始。要遍历几千个客户、每个再翻好几页时,决定这活能不能干成的是工程健壮性:网络会抖、会话会过期、跑到一半机器会睡。这篇把三件让采集「可中断、可续跑、不失控」的事讲全。
两个采集接口的完整入参
客户列表(一次性拉全量,做前置缓存):
POST /xmanage/manage/v2/customer/queryCustomers
{'newStatusList': ['2', '4', '5'], # 只要这几种状态的客户
'key': '', 'pageNo': page_no, 'pageSize': 100}
某客户的商品/税收分类数据(每个客户翻页):
headers = {..., 'xsrf-token': xsrf,
'customerGroup': 'YQDZ', # ← 这三个头缺一不可
'customerOrderType': 'YQDZ',
'serviceVersion': '5'}
POST /einvoicepfweb/api/qd/cargo/management/getPageList
{'count': True, 'pageIndex': page_no, 'pageSize': 200,
'customerId': customer_id, 'sortOrder': '',
'accountId': self.journalIdList, 'platformId': self.companyId}
getPageList 那三个自定义头(customerGroup / customerOrderType / serviceVersion)是逆向时容易漏的——少了它们接口会拒。遇到「参数都对却还是失败」,先回头核对请求头里有没有这种业务自定义头。
分页:total 与「返回条数」双判据收尾
平台列表是标准 pageNo/pageSize,但终止条件要写稳。只看 total 不够(有的接口 total 不准),只看「返回是否为空」也不够(边界页可能正好整除)。两个判据都要:
page_no, page_size, all_items = 1, 100, []
while True:
body = post(url, json={'pageNo': page_no, 'pageSize': page_size, ...})['body'] or {}
lst = body.get('list') or body.get('records') or [] # 字段名兜底
total = body.get('total') or 0
if not lst:
break # 空页
all_items.extend(lst)
if len(all_items) >= total or len(lst) < page_size:
break # 够数,或最后一页没满
page_no += 1
顺带:列表字段有时叫 list、有时叫 records(同平台不同接口都可能不一样),body.get('list') or body.get('records') or [] 这种兜底能省掉一堆「换个接口就 KeyError」。
断点续传:JSONL 追加,天然可恢复
几千客户跑下来中途断很正常。若「全部跑完再一次性写文件」,断了全白干。正确姿势是每完成一个立刻落盘一行,用 JSONL:
def append_result(record): # 每个客户跑完立刻追加
with open("spgl_progress.jsonl", 'a', encoding='utf-8') as f:
f.write(json.dumps(record, ensure_ascii=False) + '\n')
def load_done_ids(): # 启动时扫出已完成的 id
done = set()
if not os.path.exists("spgl_progress.jsonl"):
return done
for line in open("spgl_progress.jsonl", encoding='utf-8'):
try:
done.add(json.loads(line)['customerId'])
except Exception:
pass # 容忍最后一行是半截(断电写到一半)
return done
JSONL 相对「一个大 JSON 数组」的两个好处,正是断点续传要的:① 追加写不用读全量——已跑几万条,写第 N+1 条只是 append,O(1);② 天然可恢复——启动扫一遍已完成集合,跑时跳过:
done_ids = load_done_ids()
for cust in all_customers:
if cust['customerId'] in done_ids:
continue # 已完成,跳过
... # 只跑没跑过的
load_done_ids 里那个 try/except 不是摆设:上次若在写某行中途被强杀,最后一行可能是半截 JSON,解析会抛——吞掉它、把前面完整的行照常加载,是让「断点」真能「续」的关键容错。客户列表本身也缓存(先看 kehu_all.json 在不在),把「慢且不常变」的前置数据缓存下来,是整个流程能反复重跑而不心疼的前提。
异常熔断:连续失败才停,偶发失败放过
单次请求失败很常见,为一次失败就整体退出太脆;但若是会话过期或被限流,再重试也没用,硬跑只是浪费。区分二者用连续失败计数:
error_count = 0
for cust in all_customers:
try:
spgl = svc.fetch_spgl(cust['customerId'])
except Exception:
error_count += 1
if error_count >= 10: # 连续 10 次 → 大概率系统性问题
print("连续异常过多,停止。下次运行会从此处续跑。")
break
continue
error_count = 0 # 成功一次就清零 ← 关键
append_result({...}) # 立刻落盘
error_count = 0 放在成功之后:偶发单次失败让计数 +1,但紧接着的成功清零,所以零星抖动永远攒不到阈值;只有连续失败才累加到 10 触发停止。而因为有断点续传,这个「停止」是安全的——修好问题(换 cookie、等限流恢复)重跑,自动从断点接着走。
骨架
一个能跑几千客户、随时可中断的采集主循环,就这么几层:
缓存前置数据(客户列表) → 有就读,没有才拉
读断点(已完成 id 集合) → 决定从哪继续
逐个处理:
已完成? → 跳过
请求成功? → 立刻 append 落盘、错误计数清零
请求失败? → 计数 +1,连续超阈值就熔断退出(可续跑)
逆向拿到接口是「能不能进」,这套「分页双判据 + JSONL 断点 + 连续失败熔断」决定「能不能把活干完」。前者是技巧,后者是工程——两者都齐,一次几千客户的批量导出才能真正稳稳落地。
—— 「亿企赢逆向手记」系列完。从登录链路、验证码、指纹头、会话状态到采集工程化,五篇串起来就是一次完整的登录态平台逆向实战。
评论 0