简介:这份PDF文档面向具备Python基础、了解HTTP请求与抓包流程的爬虫学习者和研发工程师,围绕手机APP传输数据的抓取与解析展开,重点涉及Fiddler抓包定位登录接口、urllib2构造POST请求并携带Header与表单数据、Cookie会话保持以及JSON字段提取等环节。文档以超级课程表为案例,串联模拟登录、话题数据获取和下拉加载更新的完整流程,并给出可参考的代码实现、Python2.7实测环境说明以及平台反爬变化时的调整提醒。资源包共1个PDF文件,大小约143KB,篇幅紧凑,结构围绕抓包、登录、取数、解析组织,适合作为移动端数据采集的入门到进阶实战材料。已有367人学习,读者可从中了解接口分析、请求头校验、CookieJar使用、JSON结构解析与分页递归加载等具体做法,也能获得合规使用边界方面的提示;不足是仅单一PDF,需结合本地环境动手实践。
1. 登录接口返回「账号或密码错误」:八成不是密码错了,是 header 没带全
用 Python 爬虫做 APP 数据抓取,最让人抓狂的不是参数加密,而是你把 Fiddler 里的表单原封不动抄下来,POST 过去却回一句「账号或密码错误」。我第一次照着抓包报文写登录请求时就栽在这——参数一个没漏,唯独没带 header,服务端直接把它当成非法客户端。这类故障在 APP 接口抓取里极其常见:header 少一项、Content-Length 对不上、Cookie 没接上、返回体是 gzip 没解,任何一处都能让请求静默失败,连报错都不给你。下面按我踩坑的顺序,把登录失败、抓不到数据、JSON 解析炸掉、下拉加载翻车这几类问题拆开讲,每条都给到能直接抄的代码和定位顺序。
2. POST 出去被判定非法请求:User-Agent、Content-Length、Cookie 三处最容易崩
APP 接口和普通网页接口最大的差别,是它对「你是不是自家客户端」这件事相当敏感。网页接口一般只看 Cookie 和参数,APP 接口还会看 User-Agent 里的运行环境标识、Host、Connection 这些字段。少了它们,请求在服务端看来就是一个裸奔的脚本,走的是完全不同的分支:返回结构一样是 JSON,status却不是 1,message写着账号或密码错误。这一章把三个最常见的崩点按发生概率排一遍。
2.1 从 Fiddler 的 Raw 视图逐行还原 header
Fiddler 抓到那条登录请求后,切到 Raw 标签页,你看到的是完整 HTTP 报文——第一行是POST /V2/... HTTP/1.1,往下每一行都是一个 header。很多人的做法是只抄Content-Type和User-Agent,剩下觉得「默认就行」,翻车就翻在这。我的一般做法是把 Raw 视图里的 header 原样搬进字典,先一项不加、一项不减地跑通,再逐项删,看删哪一项会挂,这样能反推出最小必需集合,也顺便知道自己到底在和什么机制打交道。
# -*- coding: utf-8 -*- import urllib2 LOGIN_URL = 'http://app.example.com/V2/StudentSkip/loginCheckV4.action' # header 全部照抄 Fiddler 的 Raw 视图,先跑通再逐项删减 headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.1.1; ModelX Build/JRO03H)', 'Host': 'app.example.com', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip', }逻辑说明:这里刻意没有写Content-Length,原因在 2.2 节会讲。Content-Type必须是application/x-www-form-urlencoded,因为这个接口收的是表单串而不是 JSON body,写成application/json服务端解析不出任何字段,同样回登录失败。
参数说明:User-Agent里那段Dalvik/1.6.0 (Linux; U; Android 4.1.1; ...)是安卓运行时的固定格式,前半段Dalvik加上版本号说明这是安卓虚拟机在发请求,后面括号里是系统和机型。服务端用它做客户端合法性判断,你换成Mozilla/5.0大概率被判非法。Host在 urllib2 里其实会由 URL 自动补全,但保留着更贴近原始报文,也方便你复制到 curl 里复现。
2.2 Content-Length 写死为什么必翻车
原始代码里有一处特别典型的坑:header 中写死'Content-Length': '207',登录时用 207,抓话题时手动改成'147',下拉加载时又改成'159'。这种写法只要表单串长度变一个字符,请求就废了。服务端按声明的长度读 body,读多了会阻塞等后续字节,读少了就把 body 截断,截断后的表单串最后一个参数直接丢失,表现出来依然是「账号或密码错误」。更隐蔽的是,有些中间层会直接回 400,有些则默默丢掉多余部分,让你以为参数写错了。
正确做法是让 urllib2 自己算,或者手动用字节长度算:
# -*- coding: utf-8 -*- import urllib params = [ ('phoneBrand', 'BrandX'), ('platform', '1'), ('deviceCode', '000000000000000'), ('account', 'FCF030E1F2F6341C1C93BE5BBC422A3D'), # 抓包得到的 32 位大写十六进制 ('phoneVersion', '16'), ('password', 'A55B48BB75C79200379D82A18C5F47D6'), ('channel', 'MarketX'), ('phoneModel', 'ModelX'), ('versionNumber', '7.2.1'), ] body = urllib.urlencode(params) # Python2 里 len(str) 得到的正是字节数,与 Content-Length 语义一致 headers['Content-Length'] = str(len(body))逻辑说明:用urllib.urlencode拼接,顺序、转义、&分隔都由它保证,末尾也不会多出一个孤零零的&。原文的表单串每个都以&结尾,多数解析器能容忍,但遇到严格的实现会多解析出一个空键,属于给自己埋雷。
参数说明:account和password是抓包时就已经加密好的 32 位十六进制串,不是明文,直接当普通字符串传即可,不要在脚本里再去做一次 MD5,那会让结果彻底对不上。这类字段是 APP 客户端算好再发的,抓包时看到什么就传什么。deviceCode是设备唯一标识,服务端可能拿它和账号做绑定,反复变会导致风控。
注意:如果你用 Python3 的 requests 重写这段,直接传data=dict就不会有这个长度问题,requests 会自己算。但如果你手动传data=body_string,记得自己 encode 成 bytes,否则长度算错,症状和上面一模一样。
2.3 Cookie 会话没接上:CookieJar 与 opener 的绑定顺序
登录接口成功后一般会下发 Cookie,后面抓话题时要靠它认人。urllib2 的urlopen不会自动保存 Cookie,必须建一个CookieJar并装进OpenerDirector,而且登录和后续请求必须共用同一个 opener 实例。见过不少人每个函数里都urllib2.urlopen一下,Cookie 全丢,于是登录成功、抓话题却返回未登录。
# -*- coding: utf-8 -*- import urllib2 from cookielib import CookieJar # 全局唯一的 cookie 容器和 opener,登录与抓取共用 cookie_jar = CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) def post(url, body, headers): req = urllib2.Request(url, body, headers) return opener.open(req).read()逻辑说明:HTTPCookieProcessor负责在响应回来时把Set-Cookie写进 jar,在下次请求时自动带上。它必须挂在 opener 上,urllib2.urlopen用的是默认 opener,跟你的 jar 毫无关系。
参数说明:cookie_jar在同一个进程里全局共享即可,多线程抓取时要加锁或者给每个线程建独立 jar,具体看服务端是否按会话限流。日志里可以print cookie_jar一下,能直接看到拿到几个 Cookie、域名对不对,这一步能省掉大量瞎猜。
3. 登录通了却抓不到话题:表单参数拼错与接口选错的两类静默失败
登录成功只说明账号这条链路通了,话题列表走的是另一个接口,参数也完全不一样。这一层的失败特别阴——HTTP 状态码是 200,返回也是合法 JSON,但status不是 1,data是空的,脚本一声不吭往下跑,最后你得到一个空列表还以为是自己解析写错了。这一章讲怎么把「登录成功」和「拿到数据」这两件事分开验证。
3.1 表单串拼接的三个细节:末尾的&、重复键、编码
话题接口的表单里有一堆看似无关的参数:phoneBrand、platform、genderType、topicId、phoneVersion、selectType、channel、phoneModel、versionNumber,还有一个真正起作用的timestamp。这些「陪跑参数」在抓包时存在,在构造请求时也必须存在,服务端很可能把它们拼起来做校验。
第一个细节是末尾的&。原文每段 body 都以&收尾,如果你用字符串格式化拼接,很容易少一个或者多一个,最后timestamp=1690000000变成timestamp=1690000000&,多出来的空键在某些实现里会触发参数校验失败。
第二个细节是重复键。有的接口会同时出现两次同名参数,urlencode传列表可以保留,传字典则会去重丢一个,这时必须用列表。
第三个细节是编码。表单值里只要出现中文,Python2 的urllib.urlencode默认按 ASCII 处理会直接抛UnicodeEncodeError,得先encode('utf-8')。
# -*- coding: utf-8 -*- import urllib def build_topic_body(timestamp, topic_id='19'): params = [ ('timestamp', str(timestamp)), ('phoneBrand', 'BrandX'), ('platform', '1'), ('genderType', '-1'), ('topicId', topic_id), ('phoneVersion', '16'), ('selectType', '3'), ('channel', 'MarketX'), ('phoneModel', 'ModelX'), ('versionNumber', '7.2.1'), ] return urllib.urlencode(params) # 顺序与抓包一致,不额外加结尾 &逻辑说明:用列表而不是字典,是为了保证顺序和抓包一致,也为了将来要加重复键时不用改写。timestamp显式转成字符串,避免传 int 时urlencode在某些版本上行为不一致。
参数说明:topicId决定抓哪个话题板块,改这个值就能换板块,是最值得先试的一个参数。selectType在原文里出现过328、3、366三种写法,多半是抓包时的分页或排序标记,建议按抓到的值原样传,不要自己猜。
3.2 用 Raw 报文做参数 diff,而不是靠肉眼比对
参数一多,肉眼比对必然漏。我习惯写个十几行的小工具,把抓包得到的表单串和自己构造的表单串都解析成字典,直接打印差异。这套动作在排查「为什么我构造的请求和 Fiddler 里的一模一样却失败」时特别管用。
# -*- coding: utf-8 -*- def parse_form(s): """把 a=1&b=2& 形式的表单串解析成字典,忽略空段""" d = {} for pair in s.strip('&').split('&'): if not pair: continue k, _, v = pair.partition('=') d[k] = v return d raw = 'timestamp=0&phoneBrand=BrandX&platform=1&genderType=-1&topicId=19&phoneVersion=16&selectType=3&channel=MarketX&phoneModel=ModelX&versionNumber=7.2.1&' mine = build_topic_body(0) a, b = parse_form(raw), parse_form(mine) print 'captured but missing:', set(a) - set(b) print 'sent but not captured:', set(b) - set(a) for k in set(a) & set(b): if a[k] != b[k]: print 'diff %s: captured=%r mine=%r' % (k, a[k], b[k])逻辑说明:strip('&')先去掉首尾多余的&,partition('=')只在第一个等号处切分,值里带等号也不会切坏。三行输出分别对应「漏参数」「多参数」「值不同」,基本上一眼就能定位。
参数说明:raw直接粘贴 Fiddler 里 Raw 视图最后一行 body 即可,注意别把换行带进去。跑出来的差异如果只有timestamp不同,那参数层就没问题,问题在别处,比如 Cookie 或者 header。
3.3 别只看 HTTP 200:业务状态码才是成功判据
原文里有一处判断写得挺有代表性:
loginStatus = json.loads(loginResult).get('data', False) if loginResult: print 'login successful!'loginStatus算出来了却没用,if判断的是响应体非空——只要服务端返回了任何内容,哪怕是一句「密码错误」,都会打印成功。这种 bug 不报错、不中断,属于标准的黑匣子式翻车,等到后面抓话题拿到空列表才回头怀疑人生。
改成先取业务码、再判断、失败时把原始返回打出来:
# -*- coding: utf-8 -*- import json def check(resp_text, ok_codes=(1,)): """解析业务状态码,失败时打印原始返回便于定位""" try: payload = json.loads(resp_text) except ValueError: print 'not json:', repr(resp_text[:300]) return None status = payload.get('status') if status not in ok_codes: print 'business fail, status=%r, msg=%r' % (status, payload.get('message')) print 'raw:', repr(resp_text[:300]) return None return payload逻辑说明:先解 JSON,再判业务码,两层失败都打印原始返回的前 300 个字符。这个长度足够看出是 HTML 错误页、是 gzip 二进制、还是正常的业务错误 JSON。
参数说明:ok_codes做成参数是因为有些接口成功码是1,有些是0或200,登录和列表接口未必统一。用元组传,调用处一目了然。
4. JSON 解析报错:从 ValueError 到 KeyError 的四条分岔路
json.loads抛异常的时候,很多人第一反应是去改解析代码,其实十次里九次是服务端返回的根本不是 JSON。这一章按报错形态分四条路走,每条都给出最省时间的确认动作。记住一个原则:解析之前先看清楚手里那串字节到底是什么。
4.1 返回一堆乱码字节:gzip 没解
header 里带了Accept-Encoding: gzip,服务端就很诚实地把响应体压成 gzip 二进制。urllib2 不会自动解压,你拿到的read()结果以\x1f\x8b开头,json.loads直接抛ValueError,打印出来是一屏乱码。这是「照抄抓包 header」最典型的副作用——Fiddler 展示时会自动解压,让你误以为服务端返回的是明文。
两条路:要么把Accept-Encoding从 header 里去掉,服务端就不压缩了,代价是流量大一点;要么自己解。
# -*- coding: utf-8 -*- import gzip import StringIO def read_body(opener, req): raw = opener.open(req).read() # gzip 魔数:前两个字节 1f 8b if raw[:2] == '\x1f\x8b': raw = gzip.GzipFile(fileobj=StringIO.StringIO(raw)).read() return raw逻辑说明:用魔数判断比看 header 更可靠,有些中间层会忽略你的Accept-Encoding照样压缩,也有些会返回不带压缩的明文,两种都得兼容。StringIO是为了把字节串包成文件对象喂给GzipFile。
参数说明:Python3 下换成gzip.decompress(raw)一行就够,raw是 bytes。判断条件写成raw[:2] == b'\x1f\x8b'。
4.2 解析到一半报 ValueError:返回的其实是 HTML 错误页
502、504、限流页、登录失效跳转页,返回的都是 HTML,状态码有时还是 200。json.loads遇到<html>立刻ValueError: No JSON object could be decoded。
# -*- coding: utf-8 -*- import json def safe_json(text): try: return json.loads(text) except ValueError: head = text[:200].replace('\n', ' ') print 'parse failed, head=%r, len=%d' % (head, len(text)) return None逻辑说明:把前 200 个字符打出来,直接就能看出是 HTML、是空串、还是二进制。len也很有用——长度是 0 说明服务端返回了空体,多半是被风控拦了。
参数说明:替换换行是为了日志一行一条,方便grep。如果head里出现<!DOCTYPE,基本不用再往下查 JSON,去查请求本身。
4.3 KeyError: 'data':字段结构变了,用.get兜底
解析成功不代表结构对。原文data = json_data['data'],一旦服务端在某个分支下不返回data,直接KeyError崩掉。更麻烦的是messageBOs里嵌套studentBO,某一层缺失就是链式 KeyError。
# -*- coding: utf-8 -*- def extract_topics(payload): data = payload.get('data') or {} rows = [] for each in data.get('messageBOs') or []: student = each.get('studentBO') or {} rows.append({ 'content': each.get('content', ''), 'schoolName': each.get('schoolName', ''), 'messageId': each.get('messageId', ''), 'gender': student.get('gender', ''), 'time': each.get('issueTime', ''), }) return data.get('timestampLong'), rows逻辑说明:or {}和or []是关键,它同时挡住「键不存在」和「键存在但值是 null」两种情形,后者用.get(k, default)是挡不住的。timestampLong一并返回,下一页要用。
参数说明:messageBOs是列表,空列表时循环直接跳过,不会抛错。issueTime是原始时间字段,格式为字符串,别急着转 datetime,先落盘再说。
4.4 中文变成\uXXXX:编码与 ensure_ascii
json.loads出来的是 unicode,print到终端时 Python2 会按终端编码再编一次,遇到 GBK 终端就UnicodeEncodeError,很多人误以为是解析错了。写文件时如果直接json.dump,默认ensure_ascii=True,中文会被转成一堆\u4e2d\u6587,可读性归零。
# -*- coding: utf-8 -*- import codecs import json with codecs.open('topics.jsonl', 'a', 'utf-8') as f: for row in rows: f.write(json.dumps(row, ensure_ascii=False) + u'\n')逻辑说明:codecs.open指定 utf-8,写入时不用担心 uincode 编码问题;ensure_ascii=False让中文原样输出。用 jsonl 而不是一个巨大的 JSON 数组,是为了中断后能直接续写,不用把整个文件读回来。
参数说明:追加模式'a'是刻意的,配合第 5 章的断点续爬。如果要做去重,用messageId建一个 set,落盘前过滤。
5. 排查清单:无限下拉加载翻车的五类现象与对应根因
话题列表是典型的「下拉加载更多」接口,每次带上一页返回的timestamp请求下一批。原文用递归实现,load()里再调load(),看起来简洁,实际把五个坑埋在了一起。这一章按现象归类,每条给出现象、原因、解决,遇到哪条直接翻哪条。
5.1 现象:程序不报错,但一直打印同一页内容
原因:timestamp没真正更新。要么函数里拿到新值但没返回,要么调用处忽略了返回值,要么返回体里timestampLong字段名变了、.get成了 None,'timestamp=%s' % None得到字符串'None',服务端把它当 0 处理,于是永远返回第一页。
解决:更新timestamp的地方加一条断言,值不前进就停。
# -*- coding: utf-8 -*- def next_timestamp(old_ts, new_ts): """校验分页游标是否前进,不前进就终止,避免死循环""" if not new_ts: print 'no timestamp in response, stop' return None if str(new_ts) == str(old_ts): print 'timestamp not advanced: %r, stop' % new_ts return None return new_ts逻辑说明:把「游标必须变化」当成硬约束写进代码,比靠肉眼盯日志靠谱。str比较是因为有时是字符串有时是数字,类型不同但值相同的坑也要挡。
参数说明:new_ts一般取data['timestampLong'],有些接口叫timestamp或nextCursor,以抓包实际字段为准,不要照抄别人的字段名。
5.2 现象:抓到几百条后开始返回空或直接失败
原因:请求频率太高触发服务端限流。下拉加载接口是给人手动滑的,人的节奏是几秒一次,脚本一秒十次,特征太明显。表现往往是先返回空列表,再返回错误 JSON,最后连接被拒。
解决:加固定间隔加失败退避,别用死循环硬怼。
# -*- coding: utf-8 -*- import time import random def polite_sleep(fail_count=0): """正常间隔 1~2 秒,连续失败时指数退避""" if fail_count == 0: time.sleep(1 + random.random()) else: time.sleep(min(2 ** fail_count, 60))逻辑说明:加随机抖动是为了避免固定间隔形成明显周期。min(..., 60)是上限,防止退避到几十分钟不动。连续失败三次以上就该停下来看日志,而不是继续重试。
参数说明:fail_count由外层统计,成功一次清零。基础间隔按接口实际承受能力调,不要照搬这个数值。
5.3 现象:跑一会儿抛 RecursionError 或直接栈溢出
原因:原文的load()在函数内部递归调用自己,每翻一页压一层栈。Python 默认递归深度 1000 左右,话题一多必然炸;就算没炸,中途异常也会把整个调用链一起带走,没有任何中间状态留下。
解决:改成 while 循环加最大页数阈值。
# -*- coding: utf-8 -*- def crawl_all(fetch_page, max_pages=200): ts = 0 page = 0 while ts is not None and page < max_pages: payload = fetch_page(ts) if payload is None: break new_ts, rows = extract_topics(payload) save_rows(rows) ts = next_timestamp(ts, new_ts) page += 1 polite_sleep() print 'done, pages=%d' % page逻辑说明:循环替代递归,栈深度恒定。max_pages是后悔药,接口出问题时最多跑 N 页就停,不会一晚上刷出几十万条重复数据。
参数说明:fetch_page传入一个只负责「给定 timestamp 返回 payload」的函数,好处是分页逻辑和网络逻辑解耦,换接口只改一个函数。max_pages按需调,调试时设成 3。
5.4 现象:中途断了只能从头再抓
原因:timestamp只在内存里,进程一挂全丢。下次启动又是timestamp=0,前面抓的全白干,还多刷了一遍请求。
解决:每页抓完把游标落盘,启动时读回来。
# -*- coding: utf-8 -*- import json import os CKPT = 'ckpt.json' def load_ckpt(): if os.path.exists(CKPT): with open(CKPT) as f: return json.load(f).get('timestamp', 0) return 0 def save_ckpt(ts): with open(CKPT, 'w') as f: json.dump({'timestamp': ts}, f)逻辑说明:写游标放在写数据之后,顺序反了会出现「游标前进了但数据没落盘」的丢页。文件写用'w'覆盖,游标只有一个值,不需要追加。
参数说明:CKPT建议和输出文件放同一目录,方便整体搬迁。多进程跑同一任务时这个方案不适用,得换成带锁的共享存储。
5.5 现象:换台机器跑,同一份代码登录就失败
原因:header 里的deviceCode、phoneModel这些设备标识和账号是绑定的,有些服务端会记录「这个账号常用哪个设备」。你在 A 机器上用抓包得到的设备串跑通了,换台机器顺手改了个deviceCode,立刻被判定异常。另外Content-Length如果还是写死的,换台机器上表单值长度一变也会挂。
解决:设备相关参数统一放配置里,整组一起用,不要单独改某一个;Content-Length一律动态计算。
# -*- coding: utf-8 -*- DEVICE = { 'phoneBrand': 'BrandX', 'phoneModel': 'ModelX', 'deviceCode': '000000000000000', 'phoneVersion': '16', 'channel': 'MarketX', 'versionNumber': '7.2.1', } # 整组取用,禁止在业务代码里单独改某一项逻辑说明:把这些参数集中成一份常量,任何请求都从这里取值,能避免「登录用了 A 设备、抓取用了 B 设备」这种自相矛盾的情况。
参数说明:deviceCode是 15 位左右的数字串,抓包看到什么就写什么,不要用随机数生成——随机会让每次请求都像新设备。
注意:抓取任何 APP 的数据之前,先确认目标服务的用户协议和 robots 相关约定,只抓自己有权限访问的公开内容,控制频率,别把别人家的接口当自家数据库用。
6. 快速定位:把抓包报文变成可复现请求的固定套路
排查这类问题,最耗时间的从来不是写代码,而是「到底哪一步和抓包不一样」。我后来固定了一套三步动作,基本能在十分钟内把范围缩到一处。
6.1 报错与先查位置对照表
| 现象 | 最可能的位置 | 第一个动作 |
|---|---|---|
| 返回「账号或密码错误」,参数看着没问题 | header 不全 | 从 Raw 视图整段复制 header,逐项删减验证 |
ValueError: No JSON object could be decoded | 响应体不是 JSON | 打印前 200 字节,看是不是 HTML 或 gzip |
KeyError: 'data' | 业务码失败或缺字段 | 先打印完整 payload,再改.get兜底 |
| 一直返回第一页 | timestamp未前进 | 在更新处打日志,比对前后值 |
| 跑到几百条后失败 | 触发限流 | 加间隔与退避,降低频率 |
| 换机器就登录失败 | 设备参数不一致 | 核对该组参数与 Content-Length |
6.2 三步复现法
第一步,把 Fiddler 里的请求原封不动导出成 curl。右键请求,选择复制为 curl 命令,直接在终端跑。这一步的目的是确认「抓包那一刻的请求,现在还能不能用」。如果 curl 也失败,说明是会话过期或接口变了,跟你的 Python 代码没关系,别在代码里找原因。
第二步,把 curl 的成功结果逐项搬到 Python 里,每搬一项跑一次。先搬 URL 和 body,再搬 header,最后搬 Cookie。搬哪一步挂了,问题就在哪一步。这比一次性重写完整脚本再调试快得多,因为失败点唯一。
第三步,把脚本跑出来的请求也用 Fiddler 抓一次做对比。给脚本设置代理指向 Fiddler 的监听端口,就能在 Fiddler 里看到脚本真正发出去的东西,和手机端那条并排放着比。header 的差异、body 的长度、Cookie 的数量,一眼就出来了。这一步是我认为最值钱的动作,因为它把「我以为我发了什么」变成「我实际发了什么」。
# 让脚本的请求也经过 Fiddler,方便和手机端请求并排比对 export HTTP_PROXY=http://127.0.0.1:8888 export HTTPS_PROXY=http://127.0.0.1:8888 python2 crawl_topics.py逻辑说明:urllib2 会读取这两个环境变量,脚本不需要改一行代码。跑完记得unset,否则后续所有请求都会走代理。
参数说明:端口按 Fiddler 实际设置填,默认 8888。如果目标接口是 https 且证书校验严格,脚本侧可能需要处理证书,这一步视情况决定要不要做。
从那以后我每次接一个新的 APP 接口,都强制走一遍「curl 复现 → 逐项搬移 → 双向抓包对比」,不管多简单的接口都不跳过。血泪经验是:省下这十分钟,通常要拿两小时去找一个没带上的 header。希望帮到你。
本文还有配套的精品资源,点击获取