资讯动态

Fiddler抓包还原APP接口:Python登录态与timestamp翻页

发布时间:2026/10/29 19:12:27 来源:尧图企业网站定制
简介这份PDF面向具备Python基础、希望入门移动端数据采集的开发者围绕手机APP传输数据的抓取与解析展开以超级课程表为例串起Fiddler抓包、HTTP请求构造、Header与登录表单处理、JSON字段提取等关键环节。压缩包内共1个PDF文件约143KB篇幅精炼适合作为专题案例随时查阅。已有367人学习说明其在爬虫入门与APP接口分析场景中具有一定参考价值。内容不仅给出模拟登录思路还展示登录成功后读取账号信息、解析话题列表、按时间戳下拉加载更多话题的代码组织方式读者可据此理解从抓包定位接口到请求复现、再到数据清洗落地的完整流程。案例基于Python2.7编写并经过实际测试同时提示需关注平台规则与法律法规适合想了解APP接口请求结构、Header作用、Cookie会话保持及JSON解析细节的技术爱好者参考。1. 从一次抓包说起手机APP的数据不在页面里在请求链里很多人第一次接触APP数据抓取会把它想成“破解加密”。实际拆开看多数校园话题类APP的接口并不神秘登录是一个POST列表是另一个POST返回体就是JSON。麻烦的是请求链——Header少一个字段就登录失败Cookie没带过去列表就返回未登录timestamp不更新就只能拿到第一页。这份Python爬虫脚本围绕一个模拟项目A校园话题类APP展开用Fiddler抓手机APP的HTTP请求在Python2.7里复现登录和话题列表接口再做JSON解析和timestamp翻页。它适合有Python基础、想跑通“抓包→登录→列表→解析”整条链路的人也适合已经写过网页爬虫、但一碰到APP接口就翻车的研发。下面按请求怎么发、登录态怎么带、分页怎么翻、坑在哪的顺序拆。2. 抓包与协议还原用Fiddler把登录请求拆成URL、Header、表单三块2.1 为什么APP抓取要先做协议还原网页爬虫可以从HTML里找链接APP接口不行。APP把请求参数、登录态、设备信息都塞在HTTP请求里页面只是一个壳。你看到的“话题列表”在客户端里对应一次POSTURL、Header、表单体三者缺一不可。常见做法是先用Fiddler把手机请求截下来确认三件事请求发到哪个域名和路径、Header里带了哪些校验字段、表单体里哪些值是固定的、哪些是每次请求变的。这个过程叫协议还原说白了就是把APP客户端干的事用Python重放一遍。选Fiddler而不是浏览器开发者工具原因很直接手机APP的请求不经过浏览器浏览器里看不到。Fiddler能监听手机发出来的HTTP和HTTPS请求还能把请求体、响应体、Header完整展示出来。对于模拟项目A这种返回JSON的接口抓包结果就是最好的“接口文档”。我一般会先把登录请求和列表请求各抓一次保存成原始报文后面写代码时逐字段对照避免凭印象补参数。注意抓包只针对自己有权限的账号和公开数据不采集手机号、密码明文、位置等隐私字段。遇到证书校验或加密参数先确认测试授权别硬绕。2.2 Fiddler监听配置与请求定位Fiddler装好后默认监听端口是8888。让手机和电脑连同一个局域网在手机Wi-Fi设置里把服务器填成电脑的局域网IP端口填8888保存后手机请求就会先到Fiddler。只抓HTTP的话这一步够了抓HTTPS还需要在手机里安装并信任Fiddler根证书否则看到的是加密隧道不是明文请求。配置完成后在Fiddler里按Host过滤把模拟项目A的域名留下其他请求清掉界面会干净很多。定位登录请求的办法是“操作一次看一次”。在手机上退出登录再登录Fiddler里会新增一条POSTURL里通常带login、check、auth这类词。点开Inspectors先看URL再看Headers最后看WebForms或TextView里的表单体。登录请求的Header里一般有Content-Type、User-Agent、Host、Connection、Accept-Encoding表单体里是phoneBrand、platform、deviceCode、account、password、channel、phoneModel、versionNumber这些字段。account和password往往是加密后的串不要试图在Python里还原加密算法直接从抓包结果里取值原样POST过去。2.3 登录请求的字段表与Header表把抓包结果整理成表比在代码里瞎猜参数可靠。下面两张表是模拟项目A登录请求的字段说明值用占位符表示实际值以你抓到的为准。字段抓包值示例是否可改作用phoneBrandBrandX不建议改设备品牌服务端可能做校验platform1不建议改平台标识Android固定值deviceCode000000000000000可替换为测试设备值设备唯一标识accountACCOUNT_CIPHER不可手改加密后的账号phoneVersion16不建议改系统版本标识passwordPASSWORD_CIPHER不可手改加密后的密码channelChannelX可保持抓包值渠道标识phoneModelModelX可保持抓包值机型标识versionNumber7.2.1可保持抓包值客户端版本号Header抓包值示例作用Content-Typeapplication/x-www-form-urlencoded; charsetUTF-8告诉服务端表单编码User-AgentDalvik/1.6.0 (Linux; U; Android 4.1.1; ModelX Build/JRO03H)模拟手机客户端Hostapi.example.com目标域名ConnectionKeep-Alive保持连接Accept-Encodinggzip响应压缩标识Content-Length按表单体字节数计算表单体长度这里最容易翻车的是Content-Length。原脚本里写死207、159、147换一个账号或改一个字段长度就变了服务端可能直接返回登录错误。稳妥做法是用len(body)动态计算并且按字节算不是按字符算。表单体里全是ASCII时两者一样一旦有中文或编码差异就会出问题。2.4 把抓包结果落成可复现的请求骨架抓包看明白后先把字段落成Python字典再拼表单体。这一步不急着发请求先把“请求长什么样”固定下来后面登录和列表都复用同一套Header。# -*- coding: utf-8 -*- # 把抓包得到的登录字段先落成字典方便逐项对照 login_fields { phoneBrand: BrandX, platform: 1, deviceCode: 000000000000000, account: ACCOUNT_CIPHER, phoneVersion: 16, password: PASSWORD_CIPHER, channel: ChannelX, phoneModel: ModelX, versionNumber: 7.2.1, } # 拼成 x-www-form-urlencoded 格式末尾保留 和抓包一致 body .join(%s%s % (k, v) for k, v in login_fields.items()) print body print Content-Length:, len(body)这段代码只做一件事把抓包字段还原成表单体并打印长度。字段顺序在多数服务端不敏感但末尾的建议保留和抓包报文保持一致减少服务端解析差异。account和password是加密串不要用明文替换deviceCode可以用测试设备的值但不要伪造他人设备。打印出来的长度直接填到Header的Content-Length里或者像下一章那样在代码里动态算。协议还原做完后面写代码就是“填空”URL填登录地址Header填抓包值body填表单体Cookie交给CookieJar。真正需要动脑的是登录态怎么保持、列表分页怎么停。3. 登录态构造用urllib2和CookieJar跑通第一个POST3.1 Python2.7环境下的选型urllib2CookieJar够不够这份脚本的运行环境是Python2.7标准库里有urllib2和cookielib不装第三方包也能完成登录。urllib2负责构造Request、发POST、读响应CookieJar负责接住服务端返回的Set-Cookie并在后续请求里自动带上。对于模拟项目A这种“登录一次后面靠Cookie”的接口这套组合够用。选它不是因为比requests强而是因为原脚本就是这套栈复现成本低老环境里不用折腾依赖。常见做法是用urllib2.build_opener(urllib2.HTTPCookieProcessor(cookieJar))生成一个带Cookie处理的opener后面所有请求都走这个opener。不要每次请求都新建opener否则CookieJar是空的列表接口会返回未登录。我一般会把opener定义在模块级登录和列表共用同一个实例这样登录成功后拿到的Cookie会自动带到列表请求里。3.2 登录代码逐行拆解下面这段代码可以直接跑前提是把URL、Header和表单体换成你抓包得到的值。代码里保留了gzip标识如果响应出现乱码先看第5章的排查记录。# -*- coding: utf-8 -*- import urllib2 from cookielib import CookieJar import json LOGIN_URL http://api.example.com/V2/StudentSkip/loginCheckV4.action HEADERS { Content-Type: application/x-www-form-urlencoded; charsetUTF-8, User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.1; ModelX Build/JRO03H), Host: api.example.com, Connection: Keep-Alive, Accept-Encoding: gzip, } login_body ( phoneBrandBrandXplatform1deviceCode000000000000000 accountACCOUNT_CIPHERphoneVersion16passwordPASSWORD_CIPHER channelChannelXphoneModelModelXversionNumber7.2.1 ) # 动态计算长度避免写死导致登录错误 HEADERS[Content-Length] str(len(login_body)) cookie_jar CookieJar() opener urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) req urllib2.Request(LOGIN_URL, login_body, HEADERS) resp opener.open(req) raw resp.read() print raw login_json json.loads(raw) print status:, login_json.get(status) print data:, login_json.get(data)逐行看几个关键点。LOGIN_URL来自抓包路径里的StudentSkip/loginCheckV4.action是模拟项目A的登录入口不要改成猜的地址。HEADERS里的User-Agent必须和手机客户端一致服务端会用这个字段判断请求来源Host要和URL域名一致用域名访问时不要带IP。login_body是表单体字段值来自抓包account和password是加密串原样带。HEADERS[Content-Length]用len(login_body)算不要抄抓包里的数字因为换账号后长度会变。CookieJar和opener是登录态的核心后面列表请求复用同一个opener。3.3 参数表哪些字段必须原样带哪些可以动态算参数来源处理方式说明URL抓包原样使用路径和参数名不要改Content-Type抓包原样使用表单提交必须带User-Agent抓包原样使用服务端校验客户端Host抓包与域名一致用域名访问时填域名Content-Length计算len(body)按字节算不写死account抓包原样使用加密串不还原password抓包原样使用加密串不还原deviceCode抓包或测试设备可替换不要伪造他人设备versionNumber抓包保持版本号可能参与校验这张表里最容易被忽略的是Content-Length和User-Agent。前者写死会在换账号时翻车后者缺失会直接返回登录错误。原脚本开头就提到“一开始没有加header得到的是登录错误”这是血泪经验服务端不是只认账号密码它还在认请求像不像手机客户端。3.4 登录成功的验证信号与常见误判登录成功后响应一般是一串JSON里面带账号信息。验证信号看三个HTTP状态码是不是200、JSON里status是不是1、data里有没有账号字段。不要只看“有没有返回内容”有些失败响应也返回200内容是一段错误JSON。代码里打印raw和login_json.get(status)就是为了把这两个信号分开看。常见误判是“登录成功但列表未登录”。登录接口返回成功只说明账号密码对了列表接口能不能用取决于Cookie有没有被opener接住。如果登录和列表用了两个不同的opener或者列表请求重新建了CookieJarCookie就丢了。另一个误判是“返回登录错误就改密码”其实多数时候是Header缺失或Content-Length不对。先对照抓包报文逐字段比Header再比表单体最后才怀疑账号。登录跑通后后面的话题列表就是同一套方法抓包拿到列表URL和表单体复用opener解析JSON。区别在于列表有分页需要用一个游标字段不停地翻。4. 话题列表解析从messageBOs里提字段用timestamp翻页4.1 JSON骨架先画出来data、messageBOs、timestampLong列表接口返回的JSON不用猜抓包响应里已经写清楚了。模拟项目A的话题列表大致是三层最外层有statusdata里有timestampLong和messageBOsmessageBOs是一个数组每个元素是一条话题话题里又有content、schoolName、messageId、issueTime以及嵌套的studentBO.gender。解析代码要做的就是把这些字段提出来拼成扁平字典。先看骨架再写代码能少走很多弯路。timestampLong是下一页的游标第一次请求传0服务端返回一个新值下次请求把这个值传回去就能拿到更早或更新的内容。messageBOs里不是每条都有content有些是系统消息或空内容提取时要跳过。studentBO也可能为空取gender时要用get做容错不要直接下标。JSON路径含义提取方式status接口状态json_data.get(status)data.timestampLong下一页游标data.get(timestampLong, 0)data.messageBOs话题数组data.get(messageBOs) or []messageBOs[].content话题内容each.get(content)messageBOs[].schoolName学校名称each.get(schoolName)messageBOs[].messageId话题IDeach.get(messageId)messageBOs[].issueTime发布时间each.get(issueTime)messageBOs[].studentBO.gender发帖人性别(each.get(studentBO) or {}).get(gender)4.2 fetch_data字段映射与容错写法fetch_data负责把一条JSON响应拆成timestamp和topicList。下面这版在原脚本基础上加了容错data为空时返回空列表content为空时跳过studentBO为空时不报错。字段名保持和抓包一致方便对照。# -*- coding: utf-8 -*- import json def fetch_data(json_data): data json_data.get(data) or {} timestamp data.get(timestampLong, 0) messages data.get(messageBOs) or [] topic_list [] for each in messages: # 没有正文的条目直接跳过避免把系统消息混进来 if not each.get(content): continue student each.get(studentBO) or {} topic { content: each.get(content), schoolName: each.get(schoolName), messageId: each.get(messageId), gender: student.get(gender), time: each.get(issueTime), } topic_list.append(topic) return timestamp, topic_list这段代码的关键是“不假设字段一定存在”。data用or {}兜底messageBOs用or []兜底studentBO用or {}兜底取gender时不会因为空对象报错。content为空就跳过是因为列表里可能混着没有正文的条目存下来会干扰后续分析。messageId建议保留后面去重和定位都用得上。schoolName和time按需保留如果只做内容分析可以只存content和messageId。4.3 load递归翻页的终止条件原脚本用递归翻页拿到timestamp后再次请求直到status不为1。这种写法能跑但有两个风险没有页数上限接口异常时可能无限递归递归深度大了会栈溢出。更稳的做法是改成迭代加一个max_pages和“空页终止”。下面这版复用第3章的opener和HEADERS把翻页逻辑写成循环。# -*- coding: utf-8 -*- import urllib2 import json TOPIC_URL http://api.example.com/V2/Treehole/Message/getMessageByTopicIdV3.action def load(timestamp, headers, url, opener, max_pages20): page 0 all_topics [] while page max_pages: body ( timestamp%sphoneBrandBrandXplatform1genderType-1 topicId19phoneVersion16selectType3 channelChannelXphoneModelModelXversionNumber7.2.1 ) % timestamp headers[Content-Length] str(len(body)) req urllib2.Request(url, body, headers) raw opener.open(req).read() result json.loads(raw) if result.get(status) ! 1: print load fail:, raw break timestamp, topics fetch_data(result) all_topics.extend(topics) page 1 # 没有新内容或游标为0时停止避免空转 if not topics or timestamp in (0, None): break return all_topicstimestamp是游标第一次传0后面传上一页返回的值。max_pages是保险丝防止接口异常时无限翻页实际跑的时候可以先设5页验证再放开到20页。headers[Content-Length]每次按新的body长度重算不能沿用登录请求的长度。result.get(status) ! 1时打印原始响应并退出保留现场方便排查是Cookie丢了还是参数错了。not topics和timestamp in (0, None)是终止条件避免服务端返回空数组还继续请求。4.4 存储落地JSONL与CSV两种写法抓下来的话题不要只打印在终端落盘才能复盘。我一般用JSONL一行一条追加写入断点跑也不容易坏文件。需要给非技术同事看时再转CSV。# -*- coding: utf-8 -*- import json def save_jsonl(topics, pathtopics.jsonl): with open(path, a) as f: for topic in topics: f.write(json.dumps(topic, ensure_asciiFalse) \n) def save_csv(topics, pathtopics.csv): import csv with open(path, wb) as f: writer csv.writer(f) writer.writerow([messageId, schoolName, gender, time, content]) for topic in topics: writer.writerow([ topic.get(messageId), topic.get(schoolName), topic.get(gender), topic.get(time), topic.get(content), ])JSONL用追加模式适合边抓边存ensure_asciiFalse保留中文。CSV在Python2.7里用wb打开避免空行字段顺序固定方便Excel直接打开。存储这一步不复杂但别等到抓完才写接口翻页到一半失败时前面的数据已经落盘不用重跑。5. 避坑与排查APP接口抓取最容易翻车的五件事5.1 先按请求链排查不要先改代码APP接口抓取出问题八成不是Python语法错而是请求链某个环节和抓包不一致。排查顺序建议固定先看URL和路径对不对再看Header是否逐字段一致再看表单体字段和长度最后看Cookie有没有带过去。这个顺序能避免“一上来就改代码”的瞎试。下面五条是这类脚本里最常见的翻车记录每条按现象、原因、解决写。5.2 五条踩坑记录现象、原因、解决第一条登录返回“登录错误”但账号密码在手机上能登。现象是Python发POST后返回一段错误JSON手机客户端同样的账号却正常。原因通常是Header缺失或Content-Length不对。服务端不只看账号密码还看User-Agent、Host、Content-Type这些字段Content-Length写死时换账号或改字段后长度不匹配服务端解析表单体失败。解决办法是把抓包Header逐字段抄下来Content-Length用len(body)动态算先只改账号密码其他字段保持抓包值。第二条响应是乱码或JSON解析报错。现象是json.loads(raw)抛异常打印raw看到一堆二进制。原因是Header里带了Accept-Encoding: gzip服务端返回gzip压缩体urllib2不会自动解压。解决办法有两个一是去掉Accept-Encoding让服务端返回明文二是保留gzip用gzip模块解压后再解析。我一般先在调试时去掉gzip确认字段没问题再决定要不要加解压逻辑。第三条登录成功列表接口却返回未登录。现象是登录响应里status1列表请求返回“请先登录”或空数据。原因是Cookie没复用。常见写法是登录用一个opener列表又新建一个openerCookieJar是空的。解决办法是把cookie_jar和opener定义在模块级登录和列表共用同一个opener每次请求都走opener.open(req)不要用urllib2.urlopen绕过Cookie处理。第四条只能拿到第一页或者程序停不下来。现象是列表只返回一页数据或者递归一直跑。原因是timestamp没更新或者递归没有终止条件。原脚本用递归接口异常时可能无限调用。解决办法是改成迭代每次从响应里取timestampLong空数组或游标为0时退出再加max_pages保险丝。调试时先把max_pages设成3确认能翻页再放开。第五条接口返回加密串或者HTTPS抓不到明文。现象是响应体不是JSON而是一串密文或者Fiddler里看到的是隧道不是请求内容。原因是接口做了额外加密或者HTTPS证书没信任。遇到加密串不要硬猜算法先确认有没有公开的解密入口没有就放弃换公开数据源。HTTPS抓不到明文时检查手机有没有安装并信任Fiddler根证书确认抓包工具监听正常。边界要清楚只处理自己有权限的接口不碰隐私字段不绕过授权校验。现象优先检查常见原因处理登录错误Header、Content-Length字段缺失或长度写死逐字段对照抓包动态算长度JSON解析失败响应原始字节gzip未解压去掉gzip或手动解压列表未登录opener、CookieJarCookie没复用登录和列表共用一个opener只一页/停不下来timestamp、终止条件游标没更新或递归无上限改迭代加max_pages密文/抓不到接口加密、证书额外加密或证书未信任确认授权不硬绕6. 从能跑到可用三个验证技巧和接口变更后的自查顺序6.1 用原始响应做回归样本脚本第一次跑通后我一般会把登录响应和列表响应各存一份原始JSON放到fixtures/目录。后面改Header、改分页、换账号时先用这份样本跑fetch_data确认字段提取没坏再发真实请求。这样做的好处是把“网络问题”和“解析问题”分开解析挂了看样本样本没问题再看请求。接口字段改了样本也能第一时间暴露KeyError。# -*- coding: utf-8 -*- import json with open(fixtures/topic_page_1.json, rb) as f: sample json.loads(f.read()) timestamp, topics fetch_data(sample) print timestamp:, timestamp print topics:, len(topics) assert topics, 样本解析为空先检查字段名这段代码不联网只验证解析逻辑。assert topics是低成本保险样本解析为空时立刻停不会带着坏逻辑去跑全量。6.2 把递归改成迭代并加限速原脚本的递归能跑但我不建议直接上生产。改成迭代后加一个time.sleep(1)每页之间停一秒既降低对方服务压力也减少被限流的概率。限速不是玄学是给自己留后悔药接口突然返回空时你能从日志里看到是哪一页、哪个timestamp停的。跑全量前先用max_pages3试确认三页数据不重复、时间戳在往前走再放开页数。接口变更后的自查顺序我固定为先抓一次新包对比URL和Header再跑登录看status再跑一页列表看JSON骨架最后才改解析字段。从那以后我每次改这类脚本都强制先跑样本、再跑一页、最后跑全量不再一上来就递归到底。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑