亿企赢逆向手记(五):大规模采集的工程化——分页、断点续传、熔断

张 张站长 · 2026-09-08 17:55 · 技术-逆向 · 24 次浏览 · 0 条评论

两个采集接口的完整入参、分页双判据、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

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用