公众号历史文章获取,听起来像是个搜索一下就能搞定的事,真上手才知道坑有多深。我一个做内容迁移的项目里,需要把某个公众号从注册到现在的全部文章完整导出来,看了一圈资料,看似每条路都能走,实际每条路都有限制:有的只能拿到最近几篇,有的需要登录态,有的抓一半就被风控拦下来。后来我把能走通的方式、抓包链路和数据处理细节都捋了一遍,才算是稳住了。
这篇文章就是从我那次实操出发,把公众号历史文章获取的路径、协议细节、存储清洗和风险边界一次性讲清楚。不管你是因为账号迁移要备份自己的内容,还是做行业内容盘点需要整理素材,又或者只是想把散落的文章沉淀进个人知识库,都可以参考这里的思路。
1. 为什么要把历史文章“捞回来”:三个高频场景
先别急着聊技术。我见过很多人一开始就抱着抓包工具猛冲,结果连“自己要拿什么、能拿到什么、拿回来干什么”都没想明白,最后要么被平台限制,要么拿回来的数据烂在硬盘里。搞清楚场景,后面每一步才有意义。
1.1 内容迁移与备份
最刚需的一类:自己运营的公众号要迁移到新平台,或者担心内容被误删、账号被限制,想把历史文章全部备份到本地。
这类场景的特点是——文章是你自己的,你有权访问后台,但后台并没有提供“一键导出全部文章正文”的功能。你能看到后台的素材列表和已发表记录,但想要结构化的正文、图片、发布时间、原文链接,依然得靠程序化手段去凑。备份的核心诉求是完整性和可回溯性,丢一篇都不行。
1.2 行业内容盘点与素材沉淀
第二类是研究某个垂直领域的内容,把头部公众号的历史文章采集回来后做话题分析、内容趋势复盘,或是作为自己写作的素材库。
这类场景对“最近一年”或“最近几百篇”的内容更敏感,时效性大于完整性,对抓取频率和去重逻辑的要求更高。你需要的不只是文章正文,还包括阅读数据、发布时间、作者署名这些可用于统计的字段,这就对后面说的“结构化存储”提出了要求。
1.3 个人知识库的长期存档
第三类比较个人化:看到的好文章不想只躺在收藏夹里,想整理进自己的知识库,方便以后检索和引用。
收藏夹的问题在于,文章可能被删除、迁移,连接失效。把正文和元数据拉下来本地化保存,是很多人做知识库的第一步。这类场景对单篇抓取质量要求更高,排版和图片损失是主要痛点,后面清洗环节我会专门讲。
2. 公众号历史文章到底存在哪里:先搞清数据边界
在动手写代码之前,我建议你先花十分钟理解公众号历史文章在服务端是怎么组织的。很多人抓不到,不是因为代码写得差,而是压根不知道数据在哪里、分页逻辑长什么样。
2.1 文章的双层组织关系:公众号 → 发布批次 → 图文
公众号的文章不是按单篇一个个平铺在服务器上的。它有一个双层组织关系:第一层是“发布批次”,也就是我们平常看到的某月某日推送的那一栏;第二层是“图文消息”,一个批次里可能只有一篇,也可能是两篇、三篇。
历史文章接口翻页时的单位不是“篇”,而是“批次”,每个批次里包含若干篇文章。理解这一点,你才能明白为什么有的接口返回的字段里会有comm_count、app_msg_ext_info这种多级嵌套结构,也才知道怎么把批次拆成单篇去入库。
2.2 所以“获取历史文章”本质上是分页请求
把历史文章理解成一批一批的推送记录之后,“获取历史文章”这个词就变得具体了:你需要通过一个分页接口,从最新一个批次开始逐页往前翻,每一页返回N个批次,接着用上一页最后一条记录的偏移量去请求下一页,直到服务端告诉你“没有更多了”。
这个翻页过程是程序化获取的核心。真正的难点在于,这个分页接口不是普通用户直接能调用的公开API,它需要携带公众号身份标识和用户登录态,这两样东西怎么拿,决定了你的方案是“开箱即用”还是“自己挖路”。
2.3 可见性边界:公开链接≠免登录抓取
很多人有个误解:公众号文章点开就能看,那它的链接就是公开的,我直接构造链接去抓不就行了?
问题在于,你能通过搜索引擎或好友分享点开某一篇文章,不代表你能拿到“这个公众号全部文章的列表”。列表接口和文章详情接口是两套东西:详情链接只要有__biz、mid、idx、sn这几个参数就能访问,但你要凑齐这几个参数,前提是已经知道有哪些文章存在。知道有哪些文章,恰恰就是“历史文章列表接口”干的事。
说得直白一点:你缺的不是“看某篇文章”的权限,而是“知道某个号发过哪些文章”的索引。所有获取方案,本质上都是在想办法拿到这个索引。
3. 从零到一我能跑的四种获取路径(省力级→硬核级)
网上关于历史文章获取的方法满天飞,但很多已经失效,或者只能覆盖极小范围。我把近一年内实际能跑通的路径按投入成本从低到高排了一遍,你对照自己的场景选就行。
3.1 运营者后台导出与浏览器自动化
如果你是公众号运营者,最低成本的方式是登录微信公众平台后台,在内容管理里翻已发表的文章,一条条复制正文。手动操作适合文章量在二三十篇的小号,超过一百篇就非常痛苦。
我的做法是用浏览器自动化工具,模拟登录后台,逐页读取文章列表,再逐个打开编辑页面提取正文。这个方案安全性最高,因为用的是后台自己的数据,不触发额外的风控,但速度慢,而且后台页面结构经常变,脚本维护成本不低。
适合场景:自己运营的号,文章量在几百篇以内,愿意接受慢速迁移。我曾经用这个方案给某开发者的个人号做过一次全量备份,三百多篇文章跑了将近小半天,但胜在稳定。
3.2 搜狗微信搜索的批量上限问题
搜狗微信搜索可能是普通人最容易接触到的“公众号文章搜索引擎”,但它的定位更像是一个“检索工具”,而不是“批量导出工具”。你在搜索框里输入公众号名称,能看到这个号最近发布的文章,再配合翻页理论上可以拉到不少历史内容。
实际上,它的问题有两个:第一,搜索结果不是按时间全量展示的,尤其老文章很容易翻不到;第二,它返回的是搜索结果页,需要二次解析才能拿到每篇文章的真实链接,而且它的HTML结构经常改,校验逻辑也比以前严格,抓取频率稍高就会被要求验证码。
我的判断是:搜狗微信搜索可以用于“补充遗漏”,不适合作为主力方案。我在一次行业盘点里用它查某公众号两年前的一篇专题文章,找是找到了,但拿到手的信息也只有标题和摘要,正文还是得点进原文链接去抓。
3.3 手机客户端“摇一摇”式的滚动翻页
很多文章讲“在微信客户端里不断上滑加载历史文章”,听着很原始,但它确实是一个真实可用的路径,尤其适合文章量不算特别大、你又不想碰任何接口的场景。
原理也很简单:公众号主页是支持上滑加载历史消息的,每滑一次就往深处多加载一批。配合手机上的自动化脚本模拟上滑操作,每隔几秒截屏或者抓包一次,就能把所有历史文章的相关请求记录下来。
但这条路的短板很明显:效率太低,一次完整加载可能要几百次滑动,而且客户端版本更新后加载策略可能变化,脚本大概率要跟着修。我自己只在验证某个“偏门公众号”是否真的只有十来篇文章时用过一次,日常不建议当主力。
3.4 面向服务端协议的历史文章分页接口
真正高效、也是我主力使用的方案,是直接请求微信公众平台的历史文章分页接口。这个接口的路径大致是:
https://mp.weixin.qq.com/mp/profile_ext?action=getmsg请求时需要携带公众号的身份标识__biz、当前用户的登录态appmsg_token,以及分页参数offset和count。返回的数据是一个JSON结构,里面包含了当前页所有发布批次的图文信息,还有一条关键字段next_offset,告诉你要翻到下一页应该把offset设成多少。
这个方案的好处是数据量可控、速度极快,一次请求就能拿到十到二十个批次的文章列表,几千篇文章也就是几百次请求的事。代价是它需要你自己去抓取登录态参数,而且有一定风控风险,我在后面专门用一整章来讲这条链路。
4. 一条真正可行的抓包链路复盘(核心干货)
这一章是整个实操里最硬核的部分。我会拿一个公众号为例,完整复盘怎么从零开始把历史文章列表接口调通,以及中途最容易踩的坑。
4.1 定位公众号身份参数 __biz
每一个公众号都有一个固定的__biz参数,它是URL里的身份证号,长度大概在二十到三十个字符之间,由字母、数字和==结尾的填充符号组成。
拿__biz最直接的方式:在微信客户端里打开任意一篇该公众号的文章,点击右上角菜单选择“复制链接”,把链接粘到电脑上,你会在URL里找到一个__biz=xxxxxx的参数。这个参数是公开的,不涉及登录态,相当于公众号的提货码。
我以往的经验是,可以顺便把mid(文章编号)和idx(该批次内的序号)也记下来,后面正文页请求会用到。注意复制链接时,微信客户端偶尔会生成短链接,短链接里没有这些参数,你需要点开短链接让页面跳转后,再从浏览器的地址栏里拿完整长链接。
4.2 从开发者工具里拿到 appmsg_token
拿到__biz之后,在电脑微信客户端里打开同一篇文章,按F12打开开发者工具,切到Network面板,勾选Preserve log避免页面跳转时清空记录。
这时在公众号主页上拉加载几次历史文章,Network面板里会出现名为profile_ext的请求。点开这个请求,Headers里能看到完整的请求地址,其中除了__biz,还有appmsg_token和pass_ticket。
appmsg_token相当于你电脑微信登录态的短期凭证,它有过期时间。根据我的实测,这个token的时效一般在几十分钟到几个小时不等,过期后接口会返回错误码,你需要重新触发一次上拉加载来刷新token。
需要提醒的是,不要试图从别人的电脑或手机里窃取token,这里只讨论你自己有访问权限的账号。
4.3 构造历史文章列表请求并处理翻页
有了__biz和appmsg_token,就可以直接构造请求了。第一页的offset=0,count我从10到20都试过,建议用10,一次拿10个发布批次,单次请求压力小,也更容易控制频率。
请求返回的JSON结构大致如下:
{ "ret": 0, "next_offset": 10, "general_msg_list": "{\"list\":[...]}" }其中general_msg_list是一个被转义成字符串的JSON,里面才是真正的发布批次列表。每个批次里包含文章标题、摘要、封面链接、发布时间、文章详情的URL等字段。next_offset是下一页的分页偏移量,如果它等于当前偏移量,说明已经到最后一页了。
我第一次顺着这个逻辑写脚本时,犯了个低级的错误:把general_msg_list当成普通JSON直接解析,结果返回的是字符串,折腾了半天才发现要先做一次json.loads,当时特别无语。这种数据嵌套在微信接口里很常见,看到字段名带list也别惯性处理,先打印出来看一眼结构。
4.4 正文页解析与图片回源
列表接口拿到的是文章摘要和链接,不是正文。要拿正文,需要打开app_msg_ext_info里的content_url字段,它指向文章详情页。详情页是一段HTML,文章正文在id="js_content"的节点里。
正文解析有几条经验可以分享:
第一,正文里的图片地址通常不是src里的直链,而是放在>import requests from bs4 import BeautifulSoup def extract_article(url, headers): resp = requests.get(url, headers=headers) soup = BeautifulSoup(resp.text, "html.parser") content = soup.select_one("#js_content") if not content: return None for img in content.find_all("img"): real_src = img.get("data-src") or img.get("src") img["src"] = real_src return content.get_text("\n", strip=True)
5. 抓下来之后不能直接用:内容清洗与去重存储
接口能跑通只是第一步,接踵而来的问题是怎么把几千篇文章变成一份可以检索、可以长期保存的数据资产。这一章讲我处理数据时的完整流程。
5.1 正文文本抽取
从HTML里抠出正文之后,第一件事是去掉导航、推荐阅读、点赞在看按钮这些跟文章内容无关的东西。js_content选出来的节点里一般只有正文,但正文里会有各种字体加粗、行内样式,直接get_text会丢失段落结构。
我的做法是保留段落标签,把p、h2、h3、li这些块级元素转成换行,再把空格压缩掉。这样导出的txt或Markdown文件阅读体验会好很多,而不是一整坨文字挤在一起。另外,正文里的代码块要用pre标签包起来,保留原本的换行和缩进,否则代码全部粘连成一行,等于白抓。
5.2 图片与样式资源本地化
公众号文章里的图片都存在微信的CDN上,链接很长,而且有过期风险。既然做了本地备份,图片就应该一并下载下来,并把正文里的图片链接替换成本地路径。
下载图片时要注意两个问题:一是图片数量可能很大,几千篇文章动辄几万张图,需要用多线程或异步去下载,但并发太高容易被拦截,我一般控制在4到8个并发;二是图片重名问题,不能直接拿URL里的文件名当磁盘文件名,我习惯用自己的规则命名,比如文章ID加图片序号。
样式资源(CSS)一般不用单独保存,因为正文内容本身就是文字和图片,样式只影响阅读体验。但如果特别在意还原度,可以把每个>
手写UEFI引导程序:从EDK2环境搭建到U盘启动盘实战
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
PCA9422与PIC32MZ1024EFE144的低功耗电源管理方案实战
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
Files文件管理器:三阶归档逻辑实现结构化文件整理
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
PCA9422与PIC18LF46K80的低功耗电源管理实战:从电源树到状态机
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
基于PCA9422与STM32F417ZG的嵌入式智能电源管理设计
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
基于PCA9422与PIC18的低功耗电池电源管理方案设计
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …