简介:一份围绕手机APP数据传输抓取与解析的PDF技术文档,面向具备Python基础、了解HTTP请求的爬虫爱好者与研发工程师,聚焦Fiddler抓包定位接口、模拟客户端登录并解析JSON数据的完整思路。内容以超级课程表APP为例,讲解如何构造HTTP请求、添加必要Header与登录表单,借助CookieJar维持会话,并从返回的JSON中提取学校、内容、时间等字段;同时涉及下拉加载更多、时间戳参数传递与递归获取话题列表等实现要点。读者可据此理解从接口捕获、会话保持到数据提取处理的链路,并对照示例调整请求头与参数。示例基于Python2.7并经过实际测试,文中也提示不同平台反爬机制与合规使用边界。包内共1个PDF文件,压缩后约143KB,轻量便于查阅。已有368人学习,适合希望把抓包分析推进到定制化信息采集的读者参考。
1. 一次登录失败与真正要解决的问题
某同学照着抓包结果复现一个课程表类 APP 的话题列表,接口地址、表单字段、加密串全都抄对了,返回的却始终是一行登录失败。问题不在加密算法,而在他把浏览器那套 User-Agent 塞进了请求头,服务端只认 APP 客户端那串 Dalvik 标识。这种“参数全对、结果全错”的场面,在手机 APP 传输数据抓取里出现的频率远高于想象。
Python 爬虫对接 APP 接口,本质上就三件事:用 Fiddler 把客户端发出的 HTTP 请求完整抄下来;按原样重放,Header、表单、Content-Length 一个不差;再把返回的 JSON 解析成结构化记录。这套东西难的不是算法,是细节对齐。下面从抓包测绘一路写到登录重放、游标翻页和验收口径,适合要交毕设课设的人照着搭,也适合要给内部系统接一条采集链路的研发。
2. 抓包与接口测绘:用 Fiddler 把 APP 请求抄成一份接口清单
抓包这一步决定后面所有代码的上限。抄漏一个 Header、少一个尾部&,后面调试两小时都未必能定位到。所以我的习惯是先把接口“测绘”成一张表,再动手写第一行代码。
2.1 抓包环境怎么搭:监听端口、证书与目标过滤
让手机和 PC 处在同一个局域网,抓包工具在 PC 上开远程连接监听,手机关掉移动数据、连同一个 WiFi,把网络出口指向 PC 的监听端口。目标是 HTTPS 接口时还要在手机上装一次抓包工具的根证书,否则看到的全是加密隧道,只有连接信息没有内容。这一步做完,先在手机上随便打开一个网页,抓包工具里能看到明文请求,才算链路通了。
真正的技巧在“少动作”。很多人一开抓包就满屏滚动,几百条请求里根本分不清哪条是登录、哪条是列表。我的做法是:清空会话 → 手机上只做登录这一个动作 → 回来看新增的请求 → 再清空 → 只做“打开话题页”这一个动作。一次只抓一个动作,接口和业务行为就一一对应上了。抓到之后立刻按 Host 过滤,把目标域名之外的请求全部隐掉,剩下的列表就是这份方案的采集面。
这里有个容易被忽略的点:登录接口和业务接口往往不是同一个 Host,或者至少不是同一个路径前缀。示例项目里登录走/V2/StudentSkip/loginCheckV4.action,话题列表走/V2/Treehole/Message/getMessageByTopicIdV3.action,两者共享同一个域名和同一套 Header,但参数完全不同。把它们放在一张表里管理,比散落在代码各处强得多。
2.2 一条请求里必须抄走的三样东西
抓到请求后不要急着复制粘贴,先看清楚它由哪几块组成。第一块是请求行和路径,决定你往哪发;第二块是 Header,决定服务端认不认你;第三块是表单体,决定服务端给你什么数据。这三块里最容易出错的是 Header,因为它看起来“可有可无”,实际上承担了客户端身份识别的作用。
| Header 字段 | 抓到的典型值 | 作用与坑点 |
|---|---|---|
| Content-Type | application/x-www-form-urlencoded; charset=UTF-8 | 声明表单编码方式,写成 json 服务端直接不解析 |
| User-Agent | Dalvik/1.6.0 (Linux; U; Android 4.1.1; M040 Build/JRO03H) | 客户端身份标识,换成浏览器 UA 会被判为非法客户端 |
| Host | 目标接口域名 | 与目标一致,拼接错误时部分网关会返回默认站点 |
| Connection | Keep-Alive | 保持长连接,批量翻页时能省下大量握手开销 |
| Accept-Encoding | gzip | 声明可接受压缩,返回体需要按 gzip 解压 |
| Content-Length | 与实际表单字节数一致 | 写死数值是最常见的翻车点,必须按真实长度动态计算 |
表单体部分要区分“固定字段”和“可变字段”。设备型号、品牌、系统版本、渠道号、客户端版本号这些属于设备指纹,一次抓包之后基本不变,可以固化到配置里;账号、密码是加密后的密文串,与设备绑定,换设备或换账号就得重新抓;时间戳、话题 ID、性别筛选这类是每次请求都要变的业务参数。
| 表单字段 | 含义 | 是否随请求变化 |
|---|---|---|
| phoneBrand / phoneModel / phoneVersion | 设备品牌、型号、系统版本 | 固定 |
| platform / channel / versionNumber | 平台标识、应用市场渠道、客户端版本 | 固定 |
| deviceCode | 设备唯一码 | 固定,与加密串绑定 |
| account / password | 加密后的账号与密码 | 固定但会过期,需重抓 |
| timestamp | 分页游标,首页传 0 | 每次翻页变化 |
| topicId | 目标话题板块 ID | 按采集目标变化 |
| genderType / selectType | 筛选与排序类型 | 一般固定 |
返回体也要当场看清楚结构,不要等写完代码再猜。示例接口返回的是一层 JSON:顶层有status表示业务状态,data下面是timestampLong游标和messageBOs数组,数组中每条记录包含content、schoolName、messageId、issueTime,以及嵌套的studentBO.gender。层级记住了,解析代码就是照着结构抄,不会出现靠try/except硬扛的情况。
提示:抓包时把响应体也一起存下来,离线存成几个 json 文件。后面写解析逻辑时可以反复回放,不用每次都请求服务端,调试效率差好几倍。
2.3 把接口清单落成配置
测绘完就落表,别让它停留在笔记里。我一般会在项目里放一个endpoints配置,把每个接口的方法、路径、必需 Header、参数模板写清楚,代码只从配置里取,不硬编码。
| 接口名 | 方法 | 路径 | 关键参数 | 是否需要登录态 |
|---|---|---|---|---|
| login | POST | /V2/StudentSkip/loginCheckV4.action | account、password、deviceCode | 否 |
| topic_list | POST | /V2/Treehole/Message/getMessageByTopicIdV3.action | timestamp、topicId、selectType | 是 |
| topic_more | POST | /V2/Treehole/Message/getMessageByTopicIdV3.action | timestamp 取上一页返回值 | 是 |
这么做的好处很实际:接口换版本、换域名、加字段时,只改配置不改逻辑;答辩时把这张表往 PPT 上一放,评审一眼就能看出你把系统边界摸清了,比你口头解释十分钟管用。
3. 总体设计:三层结构与四个模块的职责划分
代码能跑和方案能交付是两件事。前者靠调通一个请求,后者靠结构——接口变了改哪、Cookie 丢了查哪、字段缺了补哪,都要能一眼定位。这套方案按传输、会话、解析三层切,配四个模块,规模不大但边界清楚。
3.1 三层结构:传输层、会话层、解析层
传输层只干一件事:把“路径 + Header + 表单体”组装成一个 HTTP 请求发出去,拿回原始字节。它不关心返回的是什么业务数据,也不关心 Cookie 从哪来。所有与协议相关的细节——Content-Length 怎么算、gzip 怎么解、超时怎么设、失败怎么重试——都收在这一层。这样做的直接好处是,抓包结果一变(比如客户端升级换了 UA),只改传输层的配置,解析和存储一行都不用动。
会话层负责登录态。它持有 Cookie 容器和 opener,负责首次登录、登录态检测、过期重登。判断登录态是否有效不能只看 HTTP 200,要看业务状态码:示例接口里status == 1才算成功,返回其他值往往是登录失效或者参数被拒。会话层还要控制请求节奏,串行发、加间隔,别一上来就并发打满。
解析层负责把 JSON 变成记录。它对接的是数据结构而不是接口,输入是一个 dict,输出是一条扁平化的记录。字段缺失、类型异常、嵌套对象为空,全部在这一层处理掉,让下游拿到的是干净数据。
三层之间的依赖是单向的:解析层不碰网络,传输层不碰业务字段,会话层夹在中间只负责凭证。分层听着像套话,但真到了改需求的时候就知道值——比如要新增一个“按学校筛选”的参数,只需在传输层的参数模板里加一行;要新增一个“发帖时间格式化”,只动解析层。
3.2 会话保持:Cookie 容器与登录态生命周期
APP 接口和网站一样,登录成功后靠 Cookie 维持会话。Python 2.7 里的标准做法是CookieJar配合HTTPCookieProcessor构造 opener,之后所有请求都走这个 opener,Cookie 会自动带上、自动更新。这里最常见的错误是每个请求都新建一个 opener——第一枪登录成功,第二枪列表就 401,因为新 opener 的 Cookie 罐子是空的。
登录态的生命周期要设计成可检测、可恢复。我的做法是在会话层暴露一个ensure_login():每次业务请求前先看本地标记,标记为失效就重登一次再重试;业务请求返回的状态码不是成功值时,把标记置为失效并抛出可识别的异常,由上层决定是重试还是终止。重登不能无限循环,设一个上限(比如连续两次重登后仍失败就中止),否则账号被风控锁了你还以为程序在正常跑。
另外一个细节是 Cookie 的持久化。如果采集任务要跨进程、跨天跑,可以把 Cookie 序列化到本地文件,下次启动先加载再校验。这一步不是必须的,但做完毕设要演示“断点续采”时,有没有这个能力差别很大。
3.3 数据模型与游标设计
话题记录的字段不多,但每个字段的来源和可空性要写清楚,否则入库时到处是None。
| 字段名 | 来源路径 | 类型 | 可空 | 说明 |
|---|---|---|---|---|
| message_id | data.messageBOs[].messageId | string | 否 | 全局唯一,去重主键 |
| content | data.messageBOs[].content | string | 是 | 部分记录无正文,需判空 |
| school_name | data.messageBOs[].schoolName | string | 是 | 发帖人所属学校 |
| gender | data.messageBOs[].studentBO.gender | int | 是 | 嵌套对象可能为空 |
| issue_time | data.messageBOs[].issueTime | string | 是 | 原始时间串,入库时统一格式 |
| crawl_time | 本地生成 | string | 否 | 采集时间,用于增量判断 |
游标设计是这套方案的核心。服务端不给页码,只给一个timestampLong,你在下一次请求时把它原样回传,服务端就返回更早的一批数据;首页传0。这个游标不能自己造,也不能做加减运算,它不是时间戳而是服务端编码过的位置标记。判断采集是否结束也有讲究:不是看本页条数是否小于某个阈值,而是看新返回的游标是否与传入的相同——相同就说明没有更早的数据了,继续请求只会拿到重复内容。
四个模块的职责到这里就清楚了:调度模块决定采哪些话题、跑多少页;会话模块管登录与 Cookie;解析模块做 JSON 到记录的映射与清洗;存储模块按message_id去重落库。规模上一个主文件加两三个辅助文件就够,不必为了“架构感”硬拆微服务。
4. 核心实现:登录重放、JSON 解析与游标翻页
设计定完就落地。下面三段代码是这套方案的骨架,跑在 Python 2.7 上,只用标准库,不装任何第三方包也能复现。示例里的域名、设备码、加密串全部替换成了占位值,实际使用时换成自己抓包得到的内容。
4.1 登录重放:表单拼接与 Content-Length 动态计算
登录是整个链路的第一道门,也是最容易卡住的地方。要点有三个:字段顺序按抓包原样拼、末尾的&保留、Content-Length用真实字节数算。
# -*- coding: utf-8 -*- # 运行环境:Python 2.7,仅依赖标准库 import urllib2 import json from cookielib import CookieJar BASE = 'http://app-gateway.example-campus.cn' # 抓包得到的接口域名 LOGIN_PATH = '/V2/StudentSkip/loginCheckV4.action' # 设备指纹:全部来自抓包,换设备需重新抓 DEVICE = { 'phoneBrand': 'Meizu', 'platform': '1', 'deviceCode': '868033014919494', 'phoneVersion': '16', 'phoneModel': 'M040', 'channel': 'MXMarket', 'versionNumber': '7.2.1', } def build_login_body(account_cipher, password_cipher): """按抓包顺序拼表单,末尾的 & 必须保留,服务端按原串做校验""" fields = dict(DEVICE) fields['account'] = account_cipher # 抓包里的加密账号串 fields['password'] = password_cipher # 抓包里的加密密码串 order = ['phoneBrand', 'platform', 'deviceCode', 'account', 'phoneVersion', 'password', 'channel', 'phoneModel', 'versionNumber'] return '&'.join('%s=%s' % (k, fields[k]) for k in order) + '&' def make_opener(): """一个 opener 贯穿整个会话,Cookie 才不会丢""" return urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) def login(opener, account_cipher, password_cipher): body = build_login_body(account_cipher, password_cipher) headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.1.1; M040 Build/JRO03H)', 'Host': 'app-gateway.example-campus.cn', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip', 'Content-Length': str(len(body)), # 按真实字节数算,写死必翻车 } req = urllib2.Request(BASE + LOGIN_PATH, body, headers) raw = opener.open(req).read() payload = json.loads(raw) if payload.get('status') != 1: raise RuntimeError('login failed: %s' % raw[:200]) return payloadbuild_login_body用固定顺序而不是字典序,是因为部分服务端会对表单串做整体校验,顺序错了虽然字段都在,仍可能被判为异常请求。Content-Length用len(body)计算,中文字符在 UTF-8 下占多个字节,手写常量迟早出错。make_opener里把CookieJar和HTTPCookieProcessor绑在一起,登录成功后服务端下发的 Cookie 会存进这个罐子,后续请求自动携带。
失败时的处理也要写清楚:状态码不是 1 就抛异常并打印返回体前 200 个字符。别只打印“登录失败”四个字,那样你永远不知道是密码错、设备码失效还是字段被截断。
4.2 返回体解析:把嵌套 JSON 拆成扁平记录
话题接口返回的结构不复杂,但嵌套很深,直接取值很容易在某一层遇到空值。解析函数要做的第一件事是判空,第二件事是兜底,第三件事才是提取。
# -*- coding: utf-8 -*- import json def parse_topic_page(raw): """输入原始响应字节,输出 (下一页游标, 记录列表)""" payload = json.loads(raw) if payload.get('status') != 1: raise RuntimeError('bad status: %s' % raw[:200]) data = payload.get('data') or {} cursor = data.get('timestampLong') # 下一页游标 items = data.get('messageBOs') or [] # 本页话题列表 records = [] for item in items: content = item.get('content') if not content: # 无正文的记录直接跳过 continue student = item.get('studentBO') or {} # 嵌套对象可能为空 records.append({ 'message_id': item.get('messageId'), 'content': content.strip(), 'school_name': item.get('schoolName', ''), 'gender': student.get('gender'), 'issue_time': item.get('issueTime'), }) return cursor, recordspayload.get('status') != 1是第一道闸,非成功状态一律不进解析。data.get('timestampLong')单独取出而不是在循环里读,避免列表为空时拿不到游标。studentBO用or {}兜底,直接链式取gender在部分记录上会抛TypeError。正文为空的记录跳过而不是写空字符串,因为后面的去重和统计都依赖正文内容。
返回值设计成元组而不是列表,是为了把“游标”和“数据”分开传递。很多人图省事把游标塞进每条记录里,翻页时再从记录里读,一旦本页为空就断了链条。
4.3 游标翻页:把递归改成循环并加去重
示例项目里翻页用的是递归,load函数里调自己。这种写法在演示时很优雅,实际跑起来有三个问题:Python 默认递归深度约一千层,数据量大时直接抛RecursionError;每层递归都持有上一层的引用,内存只涨不降;中间某一页失败时整个调用栈直接断掉,没法续采。
# -*- coding: utf-8 -*- import urllib2 TOPIC_PATH = '/V2/Treehole/Message/getMessageByTopicIdV3.action' BASE_HEADERS = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.1.1; M040 Build/JRO03H)', 'Host': 'app-gateway.example-campus.cn', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip', } def crawl_topic(opener, topic_id, max_page=200): """生成器:一页一页产出记录,调用方决定怎么存""" cursor, page, seen = 0, 0, set() while page < max_page: body = ('timestamp=%s&phoneBrand=Meizu&platform=1&genderType=-1' '&topicId=%s&phoneVersion=16&selectType=3&channel=MXMarket' '&phoneModel=M040&versionNumber=7.2.1&' % (cursor, topic_id)) headers = dict(BASE_HEADERS) headers['Content-Length'] = str(len(body)) # 每页长度不同,逐次重算 req = urllib2.Request(BASE + TOPIC_PATH, body, headers) raw = opener.open(req).read() next_cursor, records = parse_topic_page(raw) for rec in records: if rec['message_id'] in seen: # 游标没前进时会出现重复 continue seen.add(rec['message_id']) yield rec if not next_cursor or next_cursor == cursor: # 游标不前进即终止 break cursor, page = next_cursor, page + 1三处设计值得说。用生成器yield而不是把结果攒成一个列表返回,内存占用与页数无关,跑几千页也不会涨。用seen集合做页内和跨页去重,因为服务端在边界情况下会重复返回同一条记录,不去重的话入库数量会虚高。终止条件写的是next_cursor == cursor,而不是“本页条数小于 N”,后者在服务端分页大小调整后就会误判。
headers = dict(BASE_HEADERS)每次都新建一份,而不是在原来的字典上改Content-Length。看起来多此一举,但如果你后续要给请求加并发,共享字典会被多个线程同时写,长度错乱就是玄学故障。血泪经验,别省这一行。
4.4 落地存储与增量:按 message_id 去重
采下来的数据总要有地方放。数据量小的时候 CSV 够用,要支持增量就上 SQLite——标准库自带,不用装东西。表结构按前面的数据模型建,message_id设主键,插入时用INSERT OR IGNORE,重复记录自动跳过,不用自己写判断。
增量采集的关键是记录上次跑到哪。游标是服务端编码的,跨进程不保证有效,所以更稳的做法是记“最后一次采集时间”,下次从头开始跑,靠主键去重把已采过的记录过滤掉。代价是会重复请求前几页,收益是不用担心游标失效,对课设这种数据量完全划算。
如果要做“断点续采”,可以在每页处理完后把游标写进一个状态文件,进程重启时先读状态文件恢复。加上失败重试(每页最多重试两次,间隔递增),整条链路的健壮性就够了。
5. 常见问题排查:五个让抓取半夜挂掉的坑
调通第一个请求只是开始,真正消耗时间的是各种“昨天还能跑,今天全是空”的场面。下面五条是这类项目里复现率最高的,每条按现象、原因、解决写,遇到了直接对号入座。
5.1 登录一直返回失败,但字段一个不差
现象:表单字段、加密串、接口地址全部照抄抓包结果,返回的却是登录错误或者一段 HTML。
原因:几乎都是 Header 的问题。最常见的三种是 User-Agent 用了浏览器的、Content-Length写成固定值、Host与目标域名不一致。服务端做客户端鉴权时,第一眼看的不是密码对不对,而是这个请求像不像自家 APP 发出来的。少一个Accept-Encoding,某些网关甚至会直接返回压缩前的错误页。
解决:把抓包里的 Header 原样复制,一个字段都别删。Content-Length改为str(len(body))动态计算,Host从配置里读,不要手打。改完先在本地打印完整的请求行、Header 和 body,和抓包结果逐字比对,通常差异一眼就能看出来。
5.2 列表接口第二枪就失效
现象:登录成功,紧接着请求话题列表,返回未登录或空数据,但用抓包工具重放同一个请求是正常的。
原因:Cookie 没被带上。要么每次请求都build_opener()新建了 opener,要么登录和业务请求用了两个不同的 opener,Cookie 罐子不在一个里面。还有人把Cookie手动塞进 Header 里,结果登录后服务端下发的更新 Cookie 覆盖不了它。
解决:全局只创建一个 opener,登录、列表、翻页全部复用它。不要在 Header 里手写 Cookie,交给HTTPCookieProcessor管理。排查时可以打印cookieJar里的条目数量,登录后应该从 0 变成至少 1,如果还是 0,说明登录响应里根本没下发 Cookie,问题在登录而不是会话。
5.3 翻到几百页后程序崩了
现象:跑一段时间后抛RuntimeError: maximum recursion depth exceeded,或者内存占用一路涨到几个 G。
原因:用了递归翻页。Python 的递归深度有上限,默认在一千层左右,每层还持有上一层的栈帧和局部变量。数据量一大必然崩,而且崩之前没有任何征兆。
解决:改成while循环加生成器,一层栈帧跑到底。yield出去的记录由调用方即时消费,不要在中途累积成一个大列表。顺手加上最大页数保护,防止服务端游标异常导致死循环。
5.4 采集数量比实际多出一截
现象:入库的记录数明显大于页面显示的话题数,同一个message_id出现两三次。
原因:分页边界重复。服务端在游标推进的临界点上会重复返回上一页末尾的几条,这是分页实现方式决定的,不是 bug。不去重的话统计结果就会虚高,答辩时被问到“你这个数据准不准”就尴尬了。
解决:解析阶段维护seen集合,按message_id过滤;存储阶段把message_id设为主键,用INSERT OR IGNORE兜第二层。游标不前进(next_cursor == cursor)时立刻终止,别硬着头皮继续请求。
5.5 昨天抓得好好的,今天全部鉴权失败
现象:代码没动过,加密账号密码还是原来那串,返回一律是鉴权失败。
原因:加密串与设备绑定,且有有效期。客户端升级、账号在其他设备登录、服务端风控策略调整,都会让旧密文失效。这类参数不是“一次抓包终身可用”的。
解决:把account、password、deviceCode从代码里挪出去,放到独立的配置文件,失效时只改配置不改逻辑。同时给登录失败加明确的告警日志,打印状态码和返回体摘要,而不是只打一行“登录失败”。做演示前一定重抓一次,别拿几天前的密文上台。
注意:采集频率和采集范围要有节制。串行请求、页间加几百毫秒间隔、只采自己需要的话题板块,这是最基本的自我约束。并发拉满导致对端服务异常,性质就变了。
6. 验收与答辩:怎么证明这套抓取链路能交付
功能跑通只是及格线。评审真正想看的是:你怎么证明它可靠、边界在哪、出了问题能不能定位。我一般按“交付物清单 + 量化指标 + 现场演示”三块准备,缺一块都容易被追问到哑口。
| 验收项 | 判定口径 | 合格线 |
|---|---|---|
| 登录可用性 | 连续 10 次登录的成功次数 | ≥ 9 次 |
| 单页解析完整率 | 有效记录数 / 响应中记录数 | ≥ 95% |
| 翻页终止正确性 | 游标不前进时是否停止 | 必须停止,无死循环 |
| 数据去重 | 入库记录中重复 message_id 数量 | 0 |
| 异常可定位 | 失败时日志是否含状态码与返回体摘要 | 必须包含 |
| 断点续采 | 中断后重启能否继续 | 支持或说明取舍 |
量化指标不用多,五六项就够,但每一项都要能当场复现。比如“单页解析完整率”这一项,你就准备一页原始响应存成 json,现场跑解析函数,打印出记录数和跳过数,对比一下就行。比口头说“我做了容错处理”有说服力得多。
答辩时的讲解顺序,我建议按请求的生命周期走,而不是按代码文件走:先展示抓包工具里那条原始请求,指出 Header 里哪几个字段是关键;再展示你构造出来的请求,逐字比对差异;然后展示返回的 JSON 结构,说明字段是怎么映射的;最后展示翻页终止的判断条件。这条线讲完,听众自然理解你为什么这么设计,而不是觉得你在背代码。
最容易被追问的三个问题,提前准备好答案。第一个是“服务端参数变了怎么办”,答案是接口配置化,改配置不改逻辑,现场可以演示改一行配置跑通。第二个是“数据重复怎么处理”,答案是双层去重,解析层用集合、存储层用主键,两句话讲完。第三个是“为什么不用多线程”,答案不是“不会”,而是“这套接口有会话态且对请求频率敏感,串行加间隔是当前的正确选择,并发需要先解决会话隔离和限速,属于下一阶段”。能讲清取舍边界,比堆功能更能体现你懂这套东西。
现场演示有个小技巧:别拿最新抓的密文跑,很容易因为过期翻车。提前半天重抓一次,把配置和原始响应都固化到本地,演示时优先跑离线的解析和翻页逻辑,把网络请求放在最后一步。真出了网络问题,你还有一半内容能讲完。
从那以后我每次做这类采集方案,都会在动手写代码前先把接口测绘表填满,把 Header 和表单字段的来源标清楚,再开工。看似多花了半小时,实际省下的是后面反复试错的整个下午。希望帮到你。
本文还有配套的精品资源,点击获取