ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Fiddler+Python抓取微信公众号历史消息:从接口抓包到CSV落地

用Fiddler+Python抓取微信公众号历史消息:从接口抓包到CSV落地 环境准备、接口定位、参数拆解、代码落地再加上最后跑久了才会遇到的坑一条线走完你照着操作从装好Fiddler到跑出第一份CSV大概就是标题里说的5分钟量级。1. 历史消息接口是什么为什么值得专门分析1.1 从人肉翻历史消息到直接问接口微信公众号的历史消息页就是点进公众号主页后那个查看历史消息的入口。就我平时接触过的场景来说需求大概分两类一类是给自己做个公众号备份防止文章被删另一类是竞品分析、选题调研想看某个垂直领域公众号过去一年发了什么。手动翻的话一次翻个几十篇还凑合要翻几百篇甚至几千篇手指都酸了。其实这些数据根本不是页面而是接口返回的JSON。你在手机上用手滑动一次微信就向服务器发一次请求服务器返回一批文章的数据然后客户端负责渲染成你看到的样子。我们要做的就是通过Fiddler把这条请求和响应截获下来搞清楚它带了什么参数、返回了什么结构再用Python把同样的请求发一遍拿到结构化的数据存成CSV或Excel。这个思路适用于绝大多数App和网页端的数据采集不只是微信公众号。之所以拿历史消息接口开刀是因为它足够典型——带签名参数、带分页游标、返回嵌套JSON这几个特征在其它接口里几乎也都会遇到。把这条链路跑通你分析别的接口基本就是复制粘贴的路子。1.2 Fiddler Python 的组合为什么最顺市面上抓包工具有不少Charles、Fiddler、Wireshark还有人直接用浏览器DevTools但我个人做App接口分析还是优先Fiddler。轻量绿色版解压就能跑不像Charles要先折腾Java环境。HTTPS解密配置很直白钩子勾上、证书一装就能看到明文对新手友好。会话列表清晰支持按域名、按URL关键字过滤这个在微信流量里非常重要因为微信的请求太多了不过滤基本没法看。FiddlerScript支持改请求和响应虽然我们这次用不到但后续做mock返回数据时很香。Wireshark的定位不一样它是网卡级抓包能看到TCP/IP层的东西但要对HTTP明文内容做还原就得费点劲不适合快速看接口。Charles在macOS上确实顺手不过它是付费的Fiddler对Windows免费受众更广。加上Python这边用requests库几十行就能把逻辑写清楚整套技术栈没有任何冗余。下面进入正题按实际操作的顺序走一遍。2. 动手前的三个准备HTTPS解密、代理、证书2.1 打开Fiddler的HTTPS解密开关默认情况下Fiddler只能看到HTTP明文流量而微信的所有接口都是HTTPS所以第一件事是开启解密。打开Fiddler后进入Tools - Options - HTTPS勾选Capture HTTPS CONNECTs和Decrypt HTTPS traffic。勾完它会提示安装根证书一路确认就行。如果弹出自定义根证书警告选择“是”信任。这一步做完Fiddler会生成一个名为DO_NOT_TRUST_FiddlerRoot的根证书后续手机/模拟器装的也是它。需要注意的是某些环境下杀毒软件会对这个根证书报警因为它们对“中间人证书”天然敏感。我自己在Win11上遇到过Defender拦截需要在信任区放行一次否则Fiddler起不来。2.2 手机和电脑连到同一个局域网接下来要让手机或模拟器走Fiddler的代理。手机和电脑连同一个Wi-Fi。手机Wi-Fi设置里手动配置代理IP填电脑的局域网IP端口默认8888。电脑上如果开了防火墙记得放行Fiddler的8866/8888端口默认8888。怎么快速查电脑局域网IPWinR输入cmd然后ipconfig找IPv4 地址那行。我见过新手在这里填了外网IP折腾半天连不上。记住一定是内网IP192.168.x.x或10.x.x.x开头那种。配置完代理用手机浏览器访问http://ip:8888Fiddler的证书下载页面就出来了。也可以直接在Fiddler里用QuickExec输入!cert弹出证书安装向导但手机访问的方式更通用。2.3 证书装完还要在系统里信任这一步很多人卡住证书下载后要安装到手机系统里但安装完成不代表生效。iOS系统下载证书后去设置 - 通用 - 关于本机 - 证书信任设置里面把Fiddler的证书开关打开。如果不做这一步抓到的HTTPS请求会显示Tunnel to ...:443看不到明文。Android系统分两种情况。Android 7.0以下用户装的证书会被微信信任Android 7.0及以上App默认不信任用户证书。微信的场景下低版本Android还好说高版本直接抓不到这在业内是个通用的坎儿。常规思路是让Fiddler同时监听两个端口8888正常收发流量8866用于向手机下发证书和配置(Wi-Fi代理时填8866)然后手机通过这种方式安装Fiddler的CA证书到系统证书目录。具体可以参考Fiddler官方文档的移动端抓包章节根证书方案的适用范围会更好一些。不过说实话微信公众号历史消息接口的正常生效场景在国内手机上通常是在微信内置浏览器里直接访问公众号主页触发的。在这种场景下如果抓包环境一直显示Tunnel优先检查证书是否被系统信任其次是确认微信版本没有启用额外的网络校验。还有一个土办法如果真机上实在抓不透明文可以换Android模拟器配合旧版微信做分析调通接口逻辑后再拿到真机验证。原理和参数都是一样的Fiddler里看到的请求头、请求体不会因为设备不同而变化。3. 在Fiddler里找到微信历史消息的appmsg接口3.1 触发一次有效请求准备工作做完后按下面顺序操作手机/模拟器上打开微信。进入一个公众号的主页点击右上角的“...”或直接进历史消息页再退出反复几次即可。打开Fiddler你会看到大量来自微信的请求混在一起非常乱。如果你使用的Fiddler版本支持分支过滤会话左侧会话列表上方的Filters页签可以直接切到Filters勾选Use Filters在Host栏填微信API常见的域名后缀比如mp.weixin.qq.com。这个域名基本承载了绝大部分公众号相关的页面和接口过滤后会话列表会清爽很多。3.2 认出关键请求的三个特征微信历史消息页对应的接口在Fiddler里看就是一个典型的GET请求Host为mp.weixin.qq.com路径是/mp/profile_ext或者/mp/appmsg具体看微信版本。老一点的微信走/mp/profile_ext新版大多走/mp/appmsg。URL参数里通常能看到下面几个关键字段__biz、appmsg_token、pass_ticket、uin、key。![这里省略了一张Fiddler会话截图你本地抓包时按上面说的特征找即可]认准这几个参数名基本百发九十九中。要注意的是__biz是一串Base64编码的内容内容是公众号的原始ID看起来像MzA3NDI0NjE4NQ。key和uin在不同请求间会变化特别是key它相当于分页游标第一次请求不带翻页之后带上。3.3 快速验证你没找错选中最像的那个请求右侧点击Inspectors - Headers往下翻到请求头区域看Referer。历史消息页的请求Referer一定是https://mp.weixin.qq.com/mp/profile_ext?actionhome__biz...这种格式里面也有__biz参数。同时请求头里大概率出现User-Agent内容是微信内置浏览器的UA标识。再点开JSON页签看响应体返回的JSON结构通常是{ app_msg_list: [ { title: 文章标题, link: 文章链接, digest: 文章摘要, cover: 封面图URL, publish_time: 1710000000 } ], can_msg_continue: true, next_offset: 20 }看到这个结构基本就实锤了。4. 接口参数拆解从能用到清楚为什么能用拿到一个接口后啪啪复制参数能跑通是一回事搞清楚每个参数干嘛的是另一回事。后者决定了当你遇到“请求失败”时能不能自己排查。4.1 参数表与含义参数名含义是否必填备注__biz公众号唯一IDBase64编码是只要换公众号这个值就变appmsg_token访问历史消息的临时令牌是有时效性过期后需要重新进入历史消息页获取pass_ticket会话凭证是和用户登录态绑定uin用户唯一标识是最后一个参数通常带?后面接keykey分页游标翻页时必填第一页不传最后一页传空fjson是固定值count每页数量是10到20左右appmsg_token和pass_ticket怎么来的它们是微信服务器在页面加载时下发的加密参数你直接复制URL里的即可带走也可以在请求头里的Cookie中找到对应字段。实际开发中我们可以每次运行脚本前手动用Fiddler抓一次这两个值填进代码里也可以做一个前置请求去自动获取但自动获取需要先模拟页面跳转复杂度高不少。我个人的建议是先用硬编码跑通后续再考虑自动化。4.2 翻页逻辑和next_offset的推进第一页请求URL尾部是这样的...fjsoncount10actionhomenext_offset0响应中返回can_msg_continuetrue和next_offset10把next_offset的值填到下一次请求的next_offset参数中同时带上上一页返回的key就能翻到下一页。实际情况是有的接口第一页不返回key第二页才返回所以你需要在循环里判断如果key还没拿到就等下一轮响应再取。翻页终止条件有两个can_msg_continue变为false或者app_msg_list为空数组。两个条件满足任意一个就停。4.3 请求头里面少了什么会失败请求头里这几个字段少一个都可能让你得到-3或-6之类的错误码User-Agent必须是微信内置浏览器UA不能是普通Chrome的UA。很多人在这翻车因为直接用Python默认UA会触发拦截。Referer必须是公众号主页URL且带__biz参数。Cookie包含pgv_pvi、pgv_si、news_comm_info等字段雪崩组合其中部分可以留空但uin和pass_ticket对应的Cookie必须有。建立了完整参数的认知下面就可以写代码了。5. Python代码实战完整脚本与逐行说明5.1 代码总览下面的脚本适用于Python 3.8依赖requests。运行前先装库pip install requestsimport requests import json import csv import time from urllib.parse import urlencode, parse_qsl, urlparse # 你需要替换的部分 BIZ MzA3NDI0NjE4NQ # 公众号 __biz 参数 APPMSG_TOKEN 你的_appmsg_token # 从Fiddler里抓到的token PASS_TICKET 你的_pass_ticket # 从Fiddler里抓到的ticket UIN 你的_uin # 用户标识 COOKIE 你的完整Cookie从Fiddler请求头里复制 # HEADERS { User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 NetType/WIFI Language/zh_CN, Referer: fhttps://mp.weixin.qq.com/mp/profile_ext?actionhome__biz{BIZ}scene124, Cookie: COOKIE, } BASE_URL https://mp.weixin.qq.com/mp/appmsg def build_params(biz, token, pass_ticket, uin, keyNone, offset0, count10): params { action: list_ex, begin: offset, count: count, f: json, from: msg, __biz: biz, appmsg_token: token, pass_ticket: pass_ticket, } # key 是游标第一页不带翻页带上 if key: params[key] key if uin: params[uin] uin return params def fetch_history(max_pages50): all_articles [] cursor_key None offset 0 page 0 while page max_pages: params build_params( bizBIZ, tokenAPPMSG_TOKEN, pass_ticketPASS_TICKET, uinUIN, keycursor_key, offsetoffset, count10, ) resp requests.get(BASE_URL, paramsparams, headersHEADERS, timeout10) try: data resp.json() except json.JSONDecodeError: print(f[第{page1}页] 响应不是JSON可能被拦截打印文本前500字) print(resp.text[:500]) break # 业务错误码处理 if data.get(ret) ! 0: err_msg data.get(err_msg) or data.get(msg) or data.get(ret) print(f[第{page1}页] 接口返回错误: {err_msg}) break article_list data.get(app_msg_list, []) if not article_list: print([停止] app_msg_list 为空已抓完。) break all_articles.extend(article_list) # 翻页游标更新 can_continue data.get(can_msg_continue) offset data.get(next_offset, offset len(article_list)) if app_msg_list in data: # 大部分情况下 key 从响应中取如果响应没有就沿用上一次的 new_key data.get(key) if new_key: cursor_key new_key print(f[第{page1}页] 抓取到 {len(article_list)} 篇累计 {len(all_articles)} 篇) if not can_continue: print([停止] can_msg_continue 为 false已到最后一页。) break page 1 time.sleep(1) # 温和策略避免请求过密 return all_articles def save_to_csv(articles, filenamewechat_history.csv): if not articles: print(没有数据可保存。) return with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([title, link, digest, cover, publish_time]) for item in articles: writer.writerow([ item.get(title, ).strip(), item.get(link, ).strip(), item.get(digest, ).strip(), item.get(cover, ).strip(), item.get(publish_time, ), ]) print(f已保存到 {filename}) if __name__ __main__: articles fetch_history() save_to_csv(articles)5.2 代码关键点解释上面脚本里的几个地方不是随便写的每行都有讲究。User-Agent。我写的是模拟手机端微信内置浏览器的UA。你完全可以直接从Fiddler里面复制完整请求的UA字段来填比任何网上的模板都准。有些情况下服务端会校验UA里的MicroMessenger字段是否出现没出现直接拒绝。params 构建逻辑。列表页的offset并不是页面索引而是偏移量。如果每页10篇第一页offset0第二页offset10第三页offset20以此类推。有的版本接口用begin字段代替offset两者本质相同区别不大。我在脚本里用了begin因为你Fiddler里看到的实际请求很可能就是begin。错误码判断。微信接口比较坑的一点是HTTP状态码基本上是200错误信息放在JSON的ret字段里。常见的ret200003表示token过期ret200005表示频率限制或签名错误。所以代码里判断ret ! 0时要打印出来方便快速定位。sleep(1)。每抓一页停1秒这个节奏是我的一个平衡点。太快容易触发风控太慢翻几百篇得等到天荒地老。实测每秒一页比较稳。csv编码。保存CSV时用了utf-8-sig而不是utf-8。这个细节很重要因为Excel打开UTF-8编码的CSV会中文乱码utf-8-sig会在文件头加上BOM标记Excel就能正确识别。5.3 一次运行结果示例跑完脚本控制台输出大概这样[第1页] 抓取到 10 篇累计 10 篇 [第2页] 抓取到 10 篇累计 20 篇 [第3页] 抓取到 10 篇累计 30 篇 ... [第23页] 停止can_msg_continue 为 false已到最后一页。 已保存到 wechat_history.csv打开CSV每一行是一篇文章标题、摘要、封面、链接、发布时间都在列。到这里你已经从打开公众号手动翻历史变成跑一条命令拿到全部历史记录了。6. 从抓包到落地那些文档里不会写的坑和心得6.1 接口返回空data并不一定是代码问题我第一次用类似脚本的时候就遇到过第一页app_msg_list有数据第二页开始返回空但can_msg_continue还是true。折腾很久最后发现是key在翻页时丢失了。还有的情况是页面刚要翻页时必须回传上一次响应中携带的一个continue标志我在脚本里直接没带上导致的报错。排查这类问题时推荐一个笨但稳妥的办法用Fiddler抓两个真实请求——第一页和第二页——然后对比这两个请求之间哪个参数变了。所有变化点就是你需要自己实现的状态流。6.2 签名参数过期时间比你想象中短appmsg_token和pass_ticket不是永久的。实测下来短则十几分钟长则数小时具体微信服务端动态判断。运行脚本如果中途出现大量错误码最有效的处理不是改代码而是回到微信里重新进一次公众号历史消息页再回Fiddler取一次新token和ticket换进代码重跑。这也是插入手动步骤的典型场景整个脚本可以做得很自动化但在token更换这一环手工介入反而是最省时间的。6.3 频率控制与账号保护微信对历史消息接口有风控逻辑如果短时间请求过多轻则返回频率提示重则暂时限制部分功能。我在实际使用中一般遵循单公众号采集总量几百篇以内用上面脚本次序问题不大要采集上千篇就得考虑间歇暂停比如每10页停5-10秒再随机化sleep区间。还需要注意的是如果同时跑多个公众号的采集任务建议逐个来不要并发。并发请求等于主动把账号暴露在风控之下。6.4 数据使用的合规边界接口能拿到文章不代表可以随便用。这里额外多说一句个人备份、学习研究、小范围数据分析这些场景影响一般可控但把文章内容打包公开传播、甚至做成商业产品卖给别人就有很大风险。我不建议对接口做绕过签名、突破访问频率限制之类的操作脚本里也刻意没写任何绕过逻辑。技术是无罪的但使用技术前先想想数据归属和用户隐私这条底线别碰。6.5 为什么有时候Fiddler抓不到微信公众号的TLS请求这个问题在群里被问过很多次。现象是手机代理配了证书也装了Fiddler能看到Tunnel to mp.weixin.qq.com:443但点进去没有明文。常见原因有三个第一证书没在系统层信任微信作为App默认不信任用户证书。第二微信部分版本启用了自带的安全网络框架普通中间人方案会被忽略。第三你用的是iOS高版本系统用户证书和系统证书权限是隔离的。对应解决办法分别是换Android 7.0以下环境、用带系统证书安装能力的抓包机、或者按前面提到的双端口方案系统证书安装处理。核心思路都是让App信任你的根证书而这一步没有Win10那种一键勾选需要针对目标环境做适配。我个人的建议是正式做微信公众号采集分析时不要在绕过微信安全框架这件事上花太多时间因为微信的对抗策略更新很快。用合法的公开接口加上合理的频率控制对高价值的小批量数据做分析效率远比研究破解方案高得多。7. 从单篇文章接口到编辑推荐接口你会踩的第二个同构坑你可能会说历史消息搞定了那公众号主页的自定义菜单、合集、专辑页呢真巧这些接口的抓取逻辑和上面几乎一模一样。小程序里公众号主页的appmsg_comment、appmsg_like、getappmsgext都属于同一批以appmsg为前缀的接口差异主要是参数组合和返回字段。我这里多说一个最常碰到的getappmsgext是文章底部的阅读数、点赞数、评论列表接口。很多做数据分析的朋友想抓这个但它的触发时机是打开文章页之后所以抓包时要先把文章链接打开、再在Fiddler里按getappmsgext过滤才能看到。它的请求参数里同样有__biz、appmsg_token、key另外多了mid和idx分别代表消息ID和文章在消息内的序号。理解了历史消息接口之后再去看这些周边接口你会有一种万物都是换皮的既视感——不是说你不用动脑而是说核心分析思路已经成型了先触发再抓包筛域名找特征参数看返回结构最后写代码复现。我现在做公众号数据采集流程基本固定在10分钟以内完成一个新接口的对接靠的就是上面这套打法。Fiddler本身没什么神秘的它只是一个让你看得见HTTP明文的镜子真正值钱的是你看懂参数之间关系的能力。这篇文章能帮你把第一面镜子擦亮剩下的路多抓两个接口自然就会了。
返回列表