微信历史文章全量爬取:Node脚本实现PC与手机端配合抓取

发布时间:2026/10/27 16:16:41
微信历史文章全量爬取:Node脚本实现PC与手机端配合抓取 简介这份资源是一款基于 Node.js 的微信公众号历史文章全量爬取工具通过微信 PC 端与手机端配合完成历史消息采集自动抓取指定公众号所有文章并按 JSON 格式落盘适合内容分析、数据备份与内容聚合场景也适用于有 JavaScript 基础的开发者和新媒体运营人员快速搭建公众号数据采集流程。压缩包共 12 个文件体积仅 39KB包含 6 个核心 js 脚本、json 配置与数据样例、babelrc 与 package 等工程配置以及 md、txt、docx 说明文档源码按库、入口等模块组织便于定位和复用。核心脚本覆盖消息列表获取、正文解析与 JSON 导出环节配合说明文档可快速理解抓取链路。已有 112 人学习下载。借助该工具可获得可直接运行的爬虫逻辑、请求头与参数配置思路、JSON 结果样例和配套说明文档能显著降低从零构建公众号爬虫的门槛方便后续做历史文章存档、内容盘点或二次开发。1. 微信公众号历史文章全量爬取从 PC 端和手机端配合到 Node 脚本我在做公众号内容备份时遇到一个很现实的问题微信没有官方历史文章导出接口文章链接里的签名参数隔天就失效人工翻页只能看到最近一小部分。于是我把目光放到微信 PC 端和手机端配合这条路上用 Node 把整个流程自动化先通过手机端/PC 端拿到公众号主页历史列表的请求参数再由脚本自动翻页把指定公众号所有可见历史文章抓下来统一保存为 JSON 文件字段包含标题、原文链接、封面、摘要、发布时间。这套方案我已经跑通了近十个公众号适合要做内容分析、数据备份和内容聚合的开发者也适合想给公众号做二次开发但缺少数据源的人。2. 抓取原理与前期准备微信 PC 端和手机端配合获取历史列表2.1 为什么不能直接抓文章链接微信接口的访问限制微信公众平台的文章页虽然可以直接打开但链接里携带的chksm、mid、idx、sn是一次性签名只能定位到单篇文章。你想要拿到一个公众号的全部历史文章必须走另一个接口mp.weixin.qq.com/mp/profile_ext?actionhome。这个接口需要当前用户的会话身份单独拿一个文章链接是触发不了列表返回的。这就是为什么工具要设计成 PC 端和手机端配合手机端进入公众号主页后能提供一个稳定的入口去触发历史列表请求PC 端微信登录以后在抓包工具里能看到完整的profile_ext请求头和参数。两者配合能凑齐__biz、uin、key、appmsg_token四个关键参数。这四个参数各管一段__biz是公众号身份标识uin是微信用户编号key和appmsg_token是会话令牌负责让接口认为你是合法用户翻页时还要用到offset控制拉到哪个位置。缺一个请求大概率会失败或返回空列表。我第一次抓的时候就是漏了appmsg_token结果接口只回来一个错误码后来对比抓包数据才发现参数名拼错了。所以前期准备不是先写代码而是先把这一条请求链路在抓包工具里完整跑通记清楚每个参数从哪里来。另外要注意公众平台后台虽然也有图文素材接口但那只有公众号管理员能看到拿来做别人的号备份不现实。这里说的方案是你自己微信里有这个公众号通过客户端列表页拿数据适用面更广。2.2 准备 Node 运行环境先装 nvm 再装依赖抓取脚本是基于 Node.js 的所以第一个准备就是运行环境。我一般先用 nvm 安装 Node而不是直接下载官网安装包原因是资源包里有些依赖对 Node 版本敏感用 nvm 可以随时在 Node 16 和 Node 18 之间切换遇到兼容问题不用重装系统。如果你在 Linux 服务器上离线使用最稳妥的办法是在一台联网机器上用 nvm 装好指定版本再把~/.nvm整个目录压缩带走到目标机器解开后配置NVM_DIR和 PATH这样比在服务器上现装省事得多。项目初始化就三条命令mkdir wechat-history-crawler cd wechat-history-crawler npm init -y npm install axios cheerio dayjs这里选了 axios 而不是 Node 原生 fetch因为历史列表接口的请求头多超时和重试都要单独控制axios 的拦截器写起来更直观。cheerio是为了应对部分公众号列表页偶尔返回 HTML 的情况dayjs用来格式化发布时间。如果接口稳定返回 JSONcheerio可以不用装先保留也不影响运行。装完以后用node -e require(axios); console.log(ok)验证模块能被正常加载。老项目常在这里翻车报Cannot find module axios原因多半是 npm 安装到了别的目录检查一下node_modules是否在当前项目根目录下。2.3 配置文件把易失效参数外置抓包拿到参数后不要直接写死在代码里因为key和appmsg_token隔一段时间就会失效写死在代码里每次都要改代码。我习惯把它们放到config.json里脚本启动时读进来。{ __biz: Mzkz..., uin: MzIx..., key: 3f08..., appmsg_token: 1050_..., cookie: rewardsn...; pgv_pvid..., maxPages: 50, delay: 4000 }__biz和uin是较稳定的值key、appmsg_token、cookie是会话相关值建议每次抓取前重新抓包刷新。maxPages50的含义是单次最多翻 50 页防止公众号文章过多时脚本无限跑下去delay是每页间隔的毫秒数我给默认值 4000实际使用中低于 3000 很容易触发风控。换公众号时只需要把配置里的__biz替换成目标公众号的值再同步刷新会话参数脚本主体不用动。为了安全config.json不要提交到公开仓库里面包含你自己的 Cookie 和令牌。2.4 参数新鲜度检查先用 curl 验证再进脚本写 Node 脚本之前我建议先直接用 curl 验证一次参数是否可用。因为抓包拿到的 key 可能在你复制的时候就已经过期与其写代码调试半天不如先在命令行里确认链路是通的。curl -G https://mp.weixin.qq.com/mp/profile_ext \ --data-urlencode actionhome \ --data-urlencode __biz你的配置 \ --data-urlencode scene124 \ --data-urlencode uin你的配置 \ --data-urlencode key你的配置 \ --data-urlencode appmsg_token你的配置 \ --data-urlencode fjson \ --data-urlencode x51 \ -H Cookie: 你的cookie \ -H User-Agent: Mozilla/5.0如果返回内容里能看到general_msg_list字段说明参数有效如果返回invalid key或者freq control说明 key 已过期或请求太频繁先去微信 PC 端重新打开一次公众号主页刷新抓包数据再试。这一步能过滤掉大部分“代码没问题但就是抓不到数据”的疑难杂症。3. 全量抓取脚本实现从列表页到 JSON 文件3.1 第一步请求公众号历史列表页把配置准备好以后先写一个最基础的请求函数把参数原样拼到profile_ext接口上。核心代码如下。const axios require(axios); const config require(./config.json); // 请求历史列表首页 async function fetchHomePage() { const url https://mp.weixin.qq.com/mp/profile_ext; const params { action: home, __biz: config.__biz, scene: 124, uin: config.uin, key: config.key, pass_ticket: , wxtoken: , appmsg_token: config.appmsg_token, x5: 1, f: json }; const response await axios.get(url, { params, headers: { // 用常见浏览器 UA 减少被识别成脚本的概率 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Cookie: config.cookie } }); return response.data; }这段代码里actionhome是获取公众号主页第一页scene124是历史消息列表的固定场景值fjson让接口返回 JSON 而不是 HTML。pass_ticket和wxtoken虽然经常是空的但参数名必须保留少一个微信就会返回签名错误。按这个函数实际请求一次返回对象里能看到general_msg_list和can_msg_continue字段说明链路已经通了。为了快速确认可以临时跑一下fetchHomePage().then(data { console.log(data.can_msg_continue); console.log(data.general_msg_list.slice(0, 300)); });general_msg_list是一个 JSON 字符串不是对象直接console.log会看到一大串带转义的字符串。不要慌下一步就要解析它。3.2 翻页逻辑根据 can_msg_continue 控制循环第一页拿到之后后续翻页要走actiongetmsg并且要带上当前页返回的offset和key。如果继续沿用首页的key翻到中间大概率会被识别成无效请求。// 根据 offset 和 key 抓取后续页 async function fetchListByOffset(offset, key) { const url https://mp.weixin.qq.com/mp/profile_ext; const params { action: getmsg, __biz: config.__biz, f: json, offset: offset, count: 10, is_ok: 1, scene: 124, uin: config.uin, key: key, pass_ticket: , wxtoken: , appmsg_token: config.appmsg_token, x5: 1 }; const response await axios.get(url, { params, headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Cookie: config.cookie } }); return response.data; }主循环里每一页都需要判断是否还有下一页同时更新offset和keyconst fs require(fs); function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); } async function main() { const allArticles []; let offset 0; let key config.key; for (let page 0; page config.maxPages; page) { const data await fetchListByOffset(offset, key); const msgList JSON.parse(data.general_msg_list); const canContinue Number(data.can_msg_continue) 1; const nextOffset data.next_offset; const nextKey data.next_key; allArticles.push(...msgList.list); console.log(第 ${page 1} 页累计 ${allArticles.length} 篇); if (!canContinue) break; offset nextOffset; key nextKey; await sleep(config.delay Math.floor(Math.random() * 2000)); } return allArticles; }每次循环至少要更新两样东西offset和key。can_msg_continue1时表示还有下一页等于 0 就说明已经到历史消息的尽头。count10是单页条数可以调但调大不一定所有公众号都会给满。翻页之间加了sleep把固定延迟和随机抖动叠一起避免请求规律太明显。这里我不建议把 delay 改成 1000 以下后文会专门讲原因。注意JSON.parse(data.general_msg_list)这一步很关键接口返回的字段是字符串化的 JSON 数组。如果解析报错优先确认fjson是否生效。3.3 字段提取与格式化列表项里能拿到的信息比想象中多但存到 JSON 里不需要全部保留。我会提取标题、原文链接、封面、摘要、作者、文章类型和发布时间其中发布时间是秒级时间戳顺手转成 ISO 字符串。// 把列表项转成统一结构 function normalizeArticle(item) { const appmsg item.appmsg || {}; const comm item.comm_msg_info || {}; return { title: appmsg.title, url: appmsg.content_url ? appmsg.content_url.replace(/amp;/g, ) : , cover: appmsg.cover, digest: appmsg.digest, author: appmsg.author, type: appmsg.type, publishTime: comm.datetime ? new Date(comm.datetime * 1000).toISOString() : , publishTimestamp: comm.datetime || 0 }; }content_url里大概率会出现amp;这是 HTML 转义存到 JSON 里要还原成否则文章链接拿去访问时会打不开。type字段建议保留图文消息一般是 9视频或小程序卡片是其他值分析时可以通过它区分内容形态。digest可能为空发布时间有时也没有这种情况就填空字符串等后续处理时再看。3.4 写入 JSON 文件最后一步就是把格式化出来的数组写到磁盘。文件名带上__biz避免抓多个公众号时互相覆盖。function saveJson(articles) { const payload { biz: config.__biz, crawledAt: new Date().toISOString(), total: articles.length, articles: articles.map(normalizeArticle) }; // 第二个参数是 null第三个参数 2 表示缩进两个空格 fs.writeFileSync(wechat_${config.__biz}.json, JSON.stringify(payload, null, 2), utf8); }这一步结束后wechat_xxx.json就是最终结果。文件大小取决于文章数量和摘要长度几十篇到几千篇的公众号都会生成一个可读的 JSON。如果后续要做海量分析可以改成JSON.stringify(payload)去掉缩进体积能小 40% 左右但可读性差一些自行取舍。4. 抓取避坑与排查五个反复翻车的点4.1 返回“freq control”或没有 general_msg_list现象脚本跑到二十页以后接口不再返回正常列表而是返回{ret: -3}或空对象更隐蔽的是general_msg_list字段消失但 HTTP 状态码还是 200。第一次遇到这种情况会以为脚本写错了其实不是。原因微信对历史列表接口有频率控制短时间密集请求会触发会话级风控。尤其是同一个 key 连续翻页前十几页没事后面突然就断了。这个不是 IP 被封是账号维度的临时限制。解决把翻页间隔从 1000ms 提到 3000~5000ms并用随机抖动打散请求规律。我现在的做法是每页config.delay Math.random() * 2000毫秒抓大号时手动把 delay 调到 6000。每抓完一个公众号休息五分钟再抓下一个。// 随机延时基础间隔 0~2000ms 抖动 await sleep(config.delay Math.floor(Math.random() * 2000));之前的血泪经验是延时写死成 2000ms结果每次都在一百篇左右断掉。后来改成随机延时同一个号能一次性抓完三百多篇。遇到ret:-3时不要反复重试等几分钟再继续重试越频繁限制越久。4.2 key 和 appmsg_token 失效导致中断现象抓了十分钟后日志开始出现key error或invalid key之后所有请求都拿不到数据。如果没做断点续传前面抓的文章全丢。原因微信的会话令牌有效期不长而且当你在客户端重新打开公众号页面旧的 key 会被主动作废。长时间抓取时不能指望一个 key 从头用到尾。解决标准做法是断点续传。把offset和当前key写入progress.json每成功翻一页更新一次脚本启动时先读这个文件续传而非从头开始。遇到 key 失效时重新在微信里打开一次公众号主页抓包拿新 key 填回配置再执行脚本就会从断点继续。// 保存翻页进度 function saveProgress(offset, key) { fs.writeFileSync(progress.json, JSON.stringify({ offset, key, updatedAt: new Date().toISOString() })); } // 读取上次进度没有则从头开始 function loadProgress() { try { const saved JSON.parse(fs.readFileSync(progress.json, utf8)); return { offset: saved.offset, key: saved.key }; } catch (error) { return { offset: 0, key: config.key }; } }把进度落到文件里比存在内存里稳。我在实际抓一个两千篇的号时key 断了三次靠这个进度文件最终把全量拿完。注意updatedAt只作诊断用真正恢复时不会按时间做判断。4.3 抓到文章数量比公众号里少现象跑完统计只有 200 篇但公众号主页里看着有 300 多篇对不上。原因最常见两个。一是公众号删除或违规下架的文章不会出现在历史列表接口二是列表接口里的消息不只图文还有视频、小程序卡片、分享语代码里如果按type9过滤非图文会被滤掉数量自然变少。解决既然目标是全量备份先不要过滤类型把type原样存下来。跑完以后对账如果总数还差很多手动滑到公众号历史列表最底部看看是否有分组或连载折叠。对账时用发布时间分布排序能直观看到缺口在哪。4.4 抓包工具看不到微信流量现象按网上教程配好抓包环境后手机上打开公众号主页抓包软件里刷不到mp.weixin.qq.com的请求脚本缺少关键参数没法跑。原因微信客户端对证书有校验部分流量不经过用户安装的证书另外抓包工具如果没解析 HTTP/2会出现“有连接、没请求”的假象。这不是代码问题是抓包这层没打通。解决优先用 PC 端微信抓包PC 端证书信任问题相对少在抓包工具里加域名过滤只看mp.weixin.qq.com不要全量解密。手机端一定要用的话检查证书是否被系统信任然后重新打开公众号主页触发一次请求。我后来基本固定用 PC 端抓包效率更高。4.5 Node 版本和依赖安装问题现象npm install报node-gyp错误或者运行时报Cannot find module axios。同一份代码在自己机器没问题换台服务器就装不上。原因项目里某些依赖编译时依赖 Node 的 ABI 版本Node 22 和 Node 16 不兼容另外没有 lock 文件时 npm 会解析出不同版本造成依赖漂移。解决用 nvm 把 Node 切到 16 或 18 再安装别用最新版。离线 Linux 服务器上最快的方式是本地装好后把整个项目目录和node_modules一起打包传上去而不是在目标机器上跑 npm install。如果切版本后仍报错删掉node_modules和package-lock.json再重新安装。拿到这套资源包时第一件事也是先看package.json里的 engines 字段多数情况下 Node 18 是安全选择。5. 数据落地与周边处理JSON 导出后怎么用5.1 用 jq 快速验证 JSON 完整性抓取完成以后先别急着写复杂分析先用 jq 验证一下 JSON 文件是不是完整。jq 是命令行下的 JSON 查询函数比直接用编辑器打开大文件省内存。jq .total wechat_Mzkz...json jq .articles[0] wechat_Mzkz...json第一条命令看总条数第二条看第一篇文章字段是否齐全。如果要筛图文消息可以用jq [.articles[] | select(.type 9)] | length wechat_Mzkz...json这个命令把articles数组里所有type9的元素筛出来再统计数量能快速知道图文占比。建议拿到 JSON 先跑一遍jq .total如果数量和预期差太远先回第 4 章排查而不是继续往下做数据分析。5.2 转成 CSV 供 Excel 和知识库使用有些人习惯用 Excel 打开 JSON但 Excel 不能直接识别 JSON 结构更常见的是转成 CSV。Node 里可以这样写const fs require(fs); const data JSON.parse(fs.readFileSync(wechat_Mzkz...json, utf8)); // 转成 CSV标题里的逗号/引号做转义 const rows data.articles.map(a [ a.title, a.url, a.publishTime ].map(v ${String(v).replace(//g, )}).join(,)); // \uFEFF 是 BOM防止 Excel 打开中文乱码 const csv \uFEFF [title,url,publishTime, ...rows].join(\n); fs.writeFileSync(wechat_articles.csv, csv, utf8);Excel 打开 CSV 时如果标题里有逗号不用双引号包裹会错列。这个脚本用双引号包了所有字段并把内部引号转成两个引号基本能覆盖绝大多数情况。如果不想转 CSV也可以把 JSON 原样导入到知识库工具Notion、Obsidian 这类工具通常都支持 JSON 或 CSV 导入。5.3 用 Python 继续做内容分析Node 负责抓取Python 负责分析这是很顺的组合。抓下来的 JSON 可以直接给 pandas 读import json import pandas as pd with open(wechat_Mzkz...json, r, encodingutf-8) as f: data json.load(f) df pd.json_normalize(data[articles]) print(df.head()) print(df.groupby(type).size())拿到 DataFrame 之后可以统计发布时间分布、按作者分组、做标题关键词分析。比如按月份聚合发布数量df[month] df[publishTime].str.slice(0, 7) print(df.groupby(month).size())这一步不需要重新写爬虫也不需要有 Node 经验只要能把 JSON 读进来就能继续做文本挖掘、知识库入库或者内容聚合。JSON 作为中间格式比 CSV 更适合保留多级字段。6. 进阶技巧断点续抓、增量更新与定时跑批6.1 增量更新用文章链接做去重第一次全量抓完以后后续只需要增量补抓新发布的内容。用content_url作为去重键启动时读旧 JSON抓取时过滤掉已经存在的链接。const history JSON.parse(fs.readFileSync(history.json, utf8)); const seen new Set(history.articles.map(a a.url)); const fresh articles.filter(a !seen.has(a.url)); history.articles history.articles.concat(fresh); fs.writeFileSync(history.json, JSON.stringify(history, null, 2), utf8);history.json是长期维护的备份文件articles是本次抓取结果。这样重复跑不会产生重复数据适合放进定时任务。6.2 定时跑批PM2 按 cron 重启pm2 start index.js --name wechat-history-crawler --cron-restart 0 3 * * * pm2 savecron-restart的意思是每天凌晨 3 点重启脚本配合增量逻辑就变成了每日增量备份。没有 pm2 的话在 crontab 里写0 3 * * * node /path/index.js也是一样的效果。6.3 边界这套抓取工具的适用场景这套工具只适用你自己微信里有权限访问的公众号历史消息被作者删除、付费阅读、被平台下架的接口里不会出现。抓下来的 JSON建议只用来做内容备份、知识库归档和统计分析不要做公开转载或商业用途。从那以后我每抓一个新公众号都会先用fetchHomePage()跑一页确认general_msg_list能正常返回再开全量循环。这个习惯帮我避开了大半翻车。如果你也需要一套能直接改配置跑的 Node 工程资源包里已经把这里的脚本整理成完整目录补齐 config.json 后执行node index.js就行。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询