微信公众号历史文章全量抓取:Node.js解析PC端微信缓存文件

发布时间:2026/10/7 20:48:32
微信公众号历史文章全量抓取:Node.js解析PC端微信缓存文件 简介面向需要批量获取公众号历史内容的内容运营、数据分析与备份归档人员这套爬虫工具通过微信PC端与手机端配合完成历史消息抓取可自动遍历指定公众号全部历史文章并以JSON格式落地便于后续内容聚合与二次分析。资源包共12个文件约39KB主体为6个JavaScript脚本及配套的Node配置覆盖抓取入口、请求头处理与核心爬取逻辑另有README和Markdown文档说明使用方式附赠docx资料与json配置目录结构精简。目前已有112人学习下载。借助该工具可省去人工逐篇复制翻页的繁琐流程快速建立公众号文章本地库同时保留标题、时间、链接等结构化的数据字段适合熟悉Node环境、想自主扩展采集需求的初中级开发者直接参考改造。1. 微信历史文章全量爬取与其死磕接口不如去读 PC 端微信留下的缓存做过公众号数据备份或内容分析的同学应该都有体会微信公众号历史消息的抓取一直是爬虫里的老大难直接写爬虫去调接口不是签名过期就是滑块验证。而这个标题给了一条反直觉的路——利用微信 PC 端和手机端配合加载历史消息时在本地生成的缓存文件再用 Node.js 扫描解析把指定公众号的文章列表按结构化 JSON 保存下来。它能解决内容分析、数据备份、内容聚合三类诉求适合手里有 Node.js 基础、愿意手动配合微信端滚动 5 到 10 分钟的内容运营、数据分析师和独立开发者。接下来我按「原理 → 最小实现 → 参数调优 → 踩坑」的顺序把整套方案讲透。2. 为什么缓存里真的能挖出全量历史文章先搞懂微信的渲染与落盘机制2.1 PC 端微信怎样渲染历史消息页页面数据最后去了哪微信 PC 端WeChat/Weixin.exe在打开公众号的「查看历史消息」页面时本质上是用内置的 Chromium 内核加载了一个网页应用。这个页面会异步请求mp.weixin.qq.com/mp/profile_ext?actiongetmsg系列接口每请求一次返回一批文章列表。做过微信公众号开发的同学应该熟悉这类 JSON 响应的结构ret、errmsg核心字段是general_msg_list。关键点来了这些接口响应不只是用来渲染页面还会以临时文件的形式写进本地缓存目录。常见路径是C:\Users\{用户名}\Documents\WeChat Files\{wxid}\FileStorage\Msg\FileStorage\{yyyy-MM}\{随机文件名}.json新版微信4.x 之后部分版本把根目录改成了xwechat_files后缀和内部结构大同小异。扫描时最好把两个根目录都覆盖到。这意味着什么意味着你想要的公众号历史文章列表微信已经帮你「下载」到硬盘上了只是散落在一堆随机命名的文件里没有整理成一份可读的 JSON。这套工具做的事情其实只有三步找到缓存文件、解析general_msg_list、去重后落盘。getmsg响应的general_msg_list字段内部是一个序列化后的 JSON 字符串展开后是这样的结构{ list: [ { title: 文章标题, digest: 文章摘要, url: https://mp.weixin.qq.com/s?__bizxxxmidxxxidx1snxxx, cover: 封面图链接, publish_time: 1735689600 } ] }这个结构里url同时带mid、idx、sn对应的三要素在去重和后续抓正文时都要用。单次 getmsg 默认返回 20 条左右翻页靠传入offset参数而翻页加载出来的数据同样会在缓存里追加。2.2 手机端在整套链路里到底扮演什么角色很多人会问既然 PC 端能加载历史消息为什么还要手机端配合我实际跑下来的经验是手机端不是用来产生缓存文件的它的作用是「解锁资格」和「提高加载上限」。第一层是登录态。PC 端微信登录需要手机扫码确认部分公众号列表页在 PC 端首次打开时还会弹出「请在手机上确认」需要你在手机上点一下允许。没有手机端的确认动作PC 端的历史消息页甚至不会发起 getmsg 请求。第二层是数据完整性。我观察到的现象是同一个公众号如果手机端近期没有打开过它的历史消息PC 端往下翻页时经常翻到某一 offset 就转圈圈加载不出来。先让手机端进入公众号历史消息页往下滑几屏再回到 PC 端操作能翻到的页码明显变多。合理的解释是历史列表的深分页需要账号侧的活跃度与设备信任状态参与校验这里不展开但你按「先手机热一遍再 PC 拉全量」的顺序操作成功率会高很多。2.3 与其拼 getmsg 接口不如解析本地文件三个硬门槛既然 getmsg 接口能用为什么不直接写个 Node.js 脚本循环调接口我试过这里有三个绕不过去的门槛。第一接口需要动态凭证。getmsg 请求得带appmsg_token和pass_ticket这两个值是从公众号历史消息页面的 HTML 里脚本算出来的有有效期短则几小时长则一天过期后接口返回ret: 200003要求重新加载页面并换新 token。自动化脚本去刷新 token 不是不行但每来一次都要处理一轮页面脚本链路脆弱。第二翻页参数有签名。getmsg 翻页不是简单加个offset请求里还有基于_biz token 时间戳生成的校验字段参数顺序或编码差一点就会判定非法请求。第三也是最现实的一条频率限制。即使凭证齐全连续翻页到几十页以后大概率触发滑块验证或登录态失效。一旦触发整个微信账号在 PC 端的操作都会被卡住恢复成本很高。解析本地缓存文件则完全没有这些负担。文件是你打开页面之后微信自己生成的工具只是读文件不发任何网络请求不存在 token 过期也不存在频率风控。这就是标题里“PC 端和手机端配合”的真正价值用人的操作代替自动化请求让微信自己把数据喂给你。2.4 缓存方案的能力边界能跑通什么跑不通什么这套方案适合个人或小团队对自有关注列表做内容备份也适合内容运营把几个竞品号的历史文章拉下来做选题复盘。数据延迟在分钟级可接受。它不适合大规模批量监控——你想同时抓几百个公众号每个都要人工滚动加载操作量太大也不适合秒级实时性要求高的场景。还有一点要认账缓存文件是有生命周期的。微信会做缓存清理LRU 淘汰旧文件切换登录账号后缓存目录会切换用户在微信设置里点「清理缓存」也会让之前抓到的列表文件消失。所以正确的用法是「需要数据时当场用 PC 端重新加载一遍」不要指望缓存文件永久驻留。跑通一次后把这批 JSON 妥善备份数据就真正归你了。我把缓存扫描和直接调接口做个对比方便你在方案选型时判断对比项缓存扫描方案直接调 getmsg 接口数据来源PC 微信本地缓存文件在线接口登录凭证不需要appmsg_token / pass_ticket翻页方式人工/模拟滚动速度慢但可控循环 offset效率高但易触发校验失败恢复重新滚动加载一遍即可需要重新扫码、重新过滑块适合规模几十个号以内的备份与分析小批量验证长期跑会被限制3. 跑通最小抓取链路从 Node.js 环境到第一份 JSON3.1 装 Node.jsWindows LTS 与 Ubuntu 20 两条路径这个工具基于 Node.js先把运行环境备好。Windows 上直接去 nodejs.org 下载 LTS 版本的.msi安装包一路下一步PATH 会自动配好。Linux 服务器上部署时我一般用 NodeSource 的源装 20.x比发行版自带的版本新而且后续支持周期长curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs node --version装完确认node --version和npm --version都能输出版本号即可。整个工具的核心逻辑只用 Node 原生模块fs、path最多加一个child_process不需要重型依赖所以你不用纠结npm install装什么把环境跑通是第一优先级。3.2 扫描缓存目录定位包含历史消息列表的大文件微信的缓存根目录很容易找到但文件数量很大直接全文扫描会非常慢。我一般先用一个扫描脚本按体积过滤把可疑的大文件收集起来。下面这个scan.js就是我常用的版本// scan.js —— 扫描微信缓存根目录输出体积超过阈值的候选文件 const fs require(fs); const path require(path); const ROOTS [ path.join(process.env.USERPROFILE || , Documents, WeChat Files), path.join(process.env.USERPROFILE || , Documents, xwechat_files), ]; const MIN_SIZE 512 * 1024; // 只关心 512KB 以上的文件 const MAX_DEPTH 8; // 微信缓存目录层级有限8 层足够 const EXT /\.(html|json|js|txt)$/i; function walk(dir, depth, acc) { if (depth MAX_DEPTH) return acc; let entries; try { entries fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return acc; // 无权限或目录不存在直接跳过 } for (const ent of entries) { if (ent.name.startsWith(.)) continue; const full path.join(dir, ent.name); if (ent.isDirectory()) { walk(full, depth 1, acc); } else if (ent.isFile() EXT.test(ent.name)) { const st fs.statSync(full); if (st.size MIN_SIZE) acc.push(full); } } return acc; } const candidates []; for (const root of ROOTS) { if (fs.existsSync(root)) { walk(root, 0, candidates); } } fs.writeFileSync(candidates.txt, candidates.join(\n), utf8); console.log(候选文件数:, candidates.length);这段逻辑很简单但有两个参数值得说明。MIN_SIZE设成 512KB是因为包含完整历史列表的文件通常体量不小低于这个值的文件大概率是零碎的单篇文章页或图片信息文件过滤掉能省下大量 IO。调试阶段如果什么都扫不到可以临时改成 200KB 再跑一轮。MAX_DEPTH给 8 层微信缓存目录实际深度一般在 5 到 6 层给到 8 既不会漏也不会把无关的系统目录卷进来。3.3 解析 general_msg_list 并落盘 JSONL拿到candidates.txt之后解析工作就集中在一个点上了从文件内容里找出general_msg_list字段把它展开成文章列表。这里有个坑我在第 5 章会专门讲先直接上正确写法// parse.js —— 从候选缓存文件中提取公众号文章列表输出 JSONL const fs require(fs); const files fs.readFileSync(candidates.txt, utf8) .split(\n) .filter(Boolean); const out fs.createWriteStream(articles.jsonl, { flags: a }); const seen new Set(); let saved 0; // getmsg 响应里 general_msg_list 的值本身是转义后的 JSON 字符串 // 正则必须匹配到转义引号不能简单用 [^] const LIST_RE /general_msg_list\s*:\s*((?:[^\\]|\\.)*)/; function extractList(raw) { const m raw.match(LIST_RE); if (!m) return []; try { const parsed JSON.parse(m[1].replace(/\\\//g, /)); return Array.isArray(parsed.list) ? parsed.list : []; } catch (e) { return []; } } for (const file of files) { if (!file.includes(FileStorage)) continue; const raw fs.readFileSync(file, utf8); for (const item of extractList(raw)) { const url item.url || ; const sn (url.match(/sn([A-Za-z0-9_-])/) || [])[1] || (url.match(/\/s\/([A-Za-z0-9_-])/) || [])[1] || ; const key sn : (item.idx || 0); if (!key || seen.has(key)) continue; seen.add(key); saved; out.write(JSON.stringify({ title: item.title, url: url, sn: sn, biz: item.biz || , idx: item.idx || 0, publishTime: item.publish_time || 0, digest: item.digest || , cover: item.cover || }) \n); } } out.end(() console.log(保存, saved, 条));运行方式很简单node scan.js node parse.jsscan.js先把候选文件路径写进candidates.txtparse.js逐行读取并解析。注意replace(/\\\//g, /)这一行缓存文件里的 URL 带转义斜杠https:\/\/mp.weixin.qq.com\/s\/...不替换的话存进 JSON 里的链接虽然能解析但后续拿去请求时会多一层麻烦。输出用 JSONL每行一个 JSON 对象而不是单个大 JSON 数组是为了增量写入和断点续跑——下次抓到更多缓存直接往同一个文件里追加即可不需要整文件重写。3.4 用 jq 验证输出查询 JSONL 最顺手的命令行工具跑完 parse 之后先别急着写更多功能用 jq 快速验证一下数据长什么样。jq 是处理 JSON 的命令行工具对标热搜词里的「json查询函数」可以在终端直接做筛选和格式化jq -c {title, url, publishTime} articles.jsonl | head -20这条命令从articles.jsonl里逐行读取 JSON 对象只输出title、url、publishTime三个字段head -20限制只显示前 20 条。如果能看到正常的中文标题和publish_time时间戳说明链路已经通了如果输出为空回头检查candidates.txt里有没有文件以及微信 PC 端是不是真的加载过历史消息页。jq 的另一个常用姿势是把 JSONL 合并成数组方便交给其他分析工具jq -s . articles.jsonl articles.json-s表示把所有输入行读入数组输出一个标准的 JSON 数组文件。中间态用 JSONL交付物用标准 JSON是我在这个项目里固定的工作习惯。4. 把抓取配置调到适合真实公众号五个必调参数与微信端操作节奏4.1 config 结构与五个必调参数工具跑通之后就该按真实公众号的体量调参数了。这个项目我建议把配置集中在一个config.json里别散落在脚本各处。常见结构是{ scanRoots: [ C:\\Users\\me\\Documents\\WeChat Files, C:\\Users\\me\\Documents\\xwechat_files ], wxid: , minFileKB: 512, scrollTimes: 80, scrollIntervalMs: 900, outputFile: articles.jsonl, filterBiz: }五个必调参数都写在表里参数默认值作用调参建议scanRoots两个根目录指定微信缓存根路径3.x 用WeChat Files4.x 部分版本是xwechat_files两个都留着wxid空限定某个微信号的缓存子目录切换过账号必须指定否则会扫到旧账号残留minFileKB512最小缓存文件体积调试期降到 200跑通后调回 512scrollTimes80单轮滚动次数500 篇历史文章大概需要 60 到 100 次滚动scrollIntervalMs900两次滚动之间的停顿低于 400ms 容易触发加载保护别贪快这里单独说filterBiz。微信缓存目录里可能混着多个公众号的列表文件你不一定全都要。filterBiz填上目标公众号的__biz参数值解析时就只保留该公众号的文章。__biz可以从公众号主页 URL 里提取也可以从已经解析出的 JSON 里看url字段抄下来。4.2 滚动翻页的两种姿势手动滚轮与 PowerShell 模拟历史消息页面是滚动加载的不滚到底缓存里就永远只有前面一两页。滚动这个动作我试过两种方式。第一种是纯手动。微信窗口保持前台人拿鼠标在历史消息列表区域持续滚动滚到没有新内容为止。手动的好处是灵活看到加载卡住了可以停下来等缺点是费手一个几千篇的号滚下来胳膊酸。第二种是用 PowerShell 调 Windows API 模拟滚轮。下面这个脚本我一直在用# scroll-wechat.ps1 —— 模拟鼠标滚轮向下滚动需保持微信窗口在前台 Add-Type -TypeDefinition using System; using System.Runtime.InteropServices; public class MouseUtil { [DllImport(user32.dll)] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo); } $times [Math]::Max(1, [int]$args[0]) for ($i 0; $i -lt $times; $i) { [MouseUtil]::mouse_event(0x0800, 0, 0, 900, 0) Start-Sleep -Milliseconds 900 } Write-Host 已向下滚动 $times 次调用方式powershell -ExecutionPolicy Bypass -File scroll-wechat.ps1 800x0800是滚轮事件的标志位最后一个参数900表示向下滚动 900 个单位这个值大约相当于三行内容。脚本每滚一次停 900ms给页面留出请求和渲染的时间。注意一个关键前提脚本执行期间鼠标指针必须悬停在微信窗口的历史消息区域内如果焦点跑到别的窗口滚轮消息会发给那个窗口等于白滚。4.3 微信 PC 端 手机端的标准操作流我把每次全量抓取的标准流程固定成了下面六步按顺序执行基本不会翻车手机微信打开目标公众号进入历史消息页手动往下翻三四屏然后退出。这一步是在给账号「热数据」让后续 PC 端深分页加载更顺畅。电脑登录微信手机扫一扫并确认登录。注意 PC 端弹出的「请在手机上确认」提示必须点允许否则页面加载会中断。在 PC 端微信里找到该公众号进入主页点击「查看历史消息」。等首屏列表渲染完成。保持微信窗口在前台运行 PowerShell 滚动脚本或者手动滚动。滚动过程中观察列表底部是否持续出现新内容。滚动到列表底部不再加载新数据后等三到五秒让最后一批数据写完缓存。执行node parse.js生成articles.jsonl再用 jq 抽查结果。这套流程里最容易出问题的是第 4 步。scrollTimes 设得太大滚动到底以后脚本还在继续发滚轮消息虽然无害但浪费时间设得太小则加载不全。我的习惯是先滚到没内容记下实际滚动次数再把这个次数乘以 1.2 写进 config留出余量。5. 全量抓取避坑清单从缓存失效到 JSON 字段缺失的五个现象5.1 只抓到最近二十条缓存只存了已渲染的列表现象。跑完 parse 之后articles.jsonl里始终只有 20 条左右怎么调整滚动次数都上不去。原因。微信 PC 端的历史消息页是按需加载的缓存文件里只有「已经被渲染进列表区域」的文章数据。如果滚动动作没有真正发生在微信窗口内比如脚本执行时鼠标焦点停在别的应用上实际只加载了首屏那 20 条后续分页根本没触发。解决。先把微信窗口置顶鼠标悬停在列表区域手动滚两下确认有加载动作再跑滚动脚本。滚动的节奏也要控制一次滚动两到三屏停顿 900ms 左右让网络请求和渲染跟上。如果一个号的历史文章超过 1000 篇把scrollTimes提到 120 以上再试。5.2 general_msg_list 解析出来是字符串而不是数组现象。自己写正则去匹配general_msg_list字段拿到之后JSON.parse直接抛异常或者好不容易 parse 成功发现结果是一个字符串而不是对象。原因。getmsg 响应里general_msg_list的值本身就是一段转义后的 JSON 字符串内部的所有引号都写成了\。如果你用general_msg_list:([^])这种正则去匹配到第一个内部引号就截断了后面全是残缺内容。解决。用能匹配转义引号的正则general_msg_list\s*:\s*((?:[^\\]|\\.)*)。我在 3.3 节的parse.js里用的就是这条抄过去直接能用。另外解析前记得把 URL 里的\/替换成/否则存进 JSON 的链接带反斜杠后续请求或者拼接 markdown 都会出问题。5.3 JSON 里的 emoji 变成 \ud83d别急着改现象。带 emoji 的标题在输出的 JSON 文件里显示成\ud83d\ude00这类转义序列看着像乱码。原因。JavaScript 的JSON.stringify对代理对字符默认输出为\udXXX转义形式这是 JSON 规范允许的合法表示不是数据损坏。很多不熟悉这个机制的人会以为是编码错了然后拿文本编辑器来回转码越转越乱。解决。不用改存储格式下游任何语言的 JSON 解析器都能还原。想人眼核对时用jq -r输出原始字符。真正要避免的是把 JSON 文件用记事本打开并「另存为」——那才会破坏文件编码把整个文件弄成乱码。如果交付给同事告诉对方「直接按 JSON 读不要用文本编辑器转码」。5.4 换微信账号后全部扫不到缓存目录与 wxid 绑定现象。昨天还能扫到目标公众号的文章今天换了个微信号登录 PC 微信重新扫描结果为空candidates.txt里只有几个无关文件。原因。微信的缓存目录是按微信号隔离的每个账号对应一个随机命名的 wxid 目录。切换账号后新账号的缓存写进新的目录旧账号目录可能已经失去权限或者被清理。你的扫描脚本如果没跟上目录变化自然什么都扫不到。解决。在 config 里显式指定wxid为当前登录账号对应的目录名别靠脚本自动扫全部。微信启动后去WeChat Files根目录下看一眼有哪些文件夹把目标账号的文件夹路径填进scanRoots。切换账号前先把上一轮抓到的 JSON 备份走。5.5 图片 url 抓得到但下载失败Referer 与防盗链现象。文章列表和正文链接都正常解析出来了但脚本去下载封面图或正文图片时全部返回 403。原因。公众号图片走的是微信自己的 CDNmmbiz.qpic.cn会校验请求里的 Referer 头。直接发请求不带 Referer或者带的是脚本所属域名都会被判定为盗链直接拒绝。解决。下载图片时把 Referer 固定为https://mp.weixin.qq.com/同时把 User-Agent 设置成微信 PC 端常见的那串 UA。如果只是做内容分析我建议干脆不下载图片保留cover字段里的原图链接等要生成报告或归档时再按需拉取。另外有一类图片是内容违规被微信替换成了占位图这种无论 Referer 怎么设置都下载不到不是技术问题不用花时间追。6. 从「能跑」到「可维护」增量备份、正文保存与知识库对接全量列表拿到手之后接下来要解决的是「下一次怎么跑得更省事」。我在这个项目上固化下来的做法有三件事。第一件事是幂等增量。articles.jsonl用sn idx作为去重主键重复执行parse.js不会产生重复数据。每次抓完我习惯用sort -u再排一遍全量文件确认没有重复行。最终交付给分析系统的标准 JSON用jq -s . articles.jsonl articles.json合并生成。这样中间态始终是追加写不会因为某次中断丢掉历史数据这个设计我建议你保留。第二件事是把 JSON 喂给知识库或内容分析系统。很多同事问过我「如何把微信公众号看到文章保存到知识库」我的经验是三步走先保留articles.jsonl作为原始数据层再写一个小脚本把每篇文章转成 Markdown文件名格式用发布日期_标题.md正文来源优先取缓存里的 HTML没有缓存时再用url链接补齐标题与摘要。这样做出来的文件仓库能被绝大多数知识库系统直接导入也能被静态博客生成器消费。第三件事是正文抓取要克制。缓存里通常只有列表数据单篇文章正文不一定都落盘过。如果你需要全文分析不要急着写并发抓取脚本——先统计articles.jsonl里有多少条再用单线程、间隔 800ms 以上的节奏去抓正文 HTML抓到后抽og:title和js_content节点。我看过太多人一上来就上并发结果一个号还没抓完微信登录态先没了。我之前踩过一次教训头一回全量抓取只存了标题、链接和发布时间没存正文当时想着「以后需要正文再抓」。一个月后业务方真的来要全文做词频分析我不得不回到 PC 端重新加载历史消息又滚了一遍全量才补上。从那以后我的固定流程是「列表 JSON 与正文 HTML 同批次落盘」滚动完成后先跑 parse紧接着就跑正文抓取不把数据需求分两次做。列表是骨架正文是血肉一次抓全后面省心。希望这套基于 Node.js 的缓存扫描思路能帮到你让你在公众号内容备份与聚合这件事上少走几趟弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询