微信爬虫实战:公众号历史文章与阅读点赞评论全量抓取
发布时间:2026/10/18 20:11:08 锦皓数字建站

简介这是一份基于 Node.js 的微信爬虫项目源码面向需要批量采集公众号数据的开发者与数据分析人员通过 AnyProxy 中间人代理机制解析微信 HTTPS 请求可获取文章正文、阅读量、点赞量、在看数及评论等数据并支持抓取公众号历史文章链接。项目已适配 AnyProxy 4可运行于个人电脑或服务器并提供 Docker 部署方案使用前需准备 Node 8.8.1 以上、MongoDB 3.4.6 以上及 Redis 环境。压缩包共 80 个文件约 1.13MB以 40 个 js 与 9 个 jsx 业务代码为核心辅以 json 配置、html 模板、png/svg 图标及字体文件另含 Dockerfile、docker-compose.yml、证书与说明文档目录按 client、server、models、rule、utils 等模块划分结构清晰。目前已有 2627 人学习下载适合研究代理抓包、数据持久化与爬虫工程化的读者参考复用。1. 微信爬虫项目拆解从公众号历史文章到阅读点赞评论的全量抓取做内容分析的朋友大概率遇到过这种需求手里有一批公众号想批量拿到它们的历史文章链接、正文、阅读量、点赞量和评论数据用来做选题分析或者竞品监测。手动一篇篇复制显然不现实浏览器插件又受限于登录态和翻页效率。这个wechat_spider项目就是冲着这个场景来的——它用 JavaScript 写的一套抓取流程能顺着公众号的历史消息入口把文章列表翻出来再逐篇解析正文和互动数据。适合做数据采集、内容运营分析或者想学爬虫工程结构的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲一遍。2. 技术选型与目录结构为什么用 Node.js 而不是 Python2.1 选型逻辑登录态复用是核心约束微信公众平台的接口几乎全部依赖登录后的 Cookie 和 token纯 HTTP 请求很难绕过。这个项目选择 Node.js一个关键原因是它天然适合做「中间人」——用puppeteer这类无头浏览器先完成扫码登录拿到有效的 Cookie 和 token再交给后续的请求模块复用。Python 当然也能做但 Node.js 在浏览器自动化和异步请求编排上的生态更顺手尤其是需要同时维护多个公众号会话时。另一个原因是数据落盘。项目把抓到的文章元信息、正文 HTML、阅读点赞数分别存成 JSON 和本地文件Node.js 的fs模块配合async/await写起来很直接不需要额外引入 ORM。常见做法是每抓完一个公众号就落一次盘避免中途断掉导致前面白跑。2.2 目录结构按职责分层源码的目录大致是这样组织的wechat_spider/ ├── config/ │ └── index.js # 全局配置并发数、延迟、存储路径 ├── core/ │ ├── login.js # 扫码登录获取并缓存 Cookie/token │ ├── articleList.js # 拉取公众号历史文章列表 │ ├── articleDetail.js # 解析单篇文章正文与互动数据 │ └── comment.js # 抓取评论区 ├── store/ │ └── fileStore.js # JSON/HTML 落盘 ├── utils/ │ ├── request.js # 带重试和延迟的请求封装 │ └── parser.js # HTML 解析工具 └── index.js # 入口串联整个流程这个分层的好处是登录、列表、详情、评论各自独立改哪一块不影响其他模块。比如你只想抓阅读量不想抓评论直接把comment.js的调用去掉就行。2.3 关键依赖与配置项package.json里核心依赖通常包括puppeteer浏览器自动化、axiosHTTP 请求、cheerioHTML 解析和p-limit并发控制。配置项集中在config/index.js// config/index.js module.exports { concurrency: 3, // 同时抓取的文章数建议不超过 5 delayMin: 2000, // 请求间最小延迟毫秒 delayMax: 5000, // 请求间最大延迟 outputDir: ./output, // 数据落盘根目录 maxRetry: 3, // 单请求失败重试次数 cookieFile: ./.cookie.json // 登录态缓存文件 };concurrency控制并发设太高容易触发风控delayMin/delayMax是随机延迟区间模拟人工翻页节奏。这两个参数是后面调优的重点先记住它们的位置。3. 核心模块拆解登录、列表、详情、评论的完整链路3.1 登录模块扫码后缓存 Cookie 和 token登录是整个流程的入口。项目用puppeteer打开公众平台登录页等用户扫码然后从页面上下文里把 Cookie 和 token 抠出来存到本地。// core/login.js const puppeteer require(puppeteer); const fs require(fs); const { cookieFile } require(../config); async function login() { const browser await puppeteer.launch({ headless: false }); const page await browser.newPage(); await page.goto(https://mp.weixin.qq.com/); // 等待用户扫码直到页面出现登录后的元素 await page.waitForSelector(.weui-desktop-account__nickname, { timeout: 120000 }); // 从页面全局变量里取 token const token await page.evaluate(() window.wx.commonData.token); const cookies await page.cookies(); // 落盘缓存下次直接复用 fs.writeFileSync(cookieFile, JSON.stringify({ token, cookies }, null, 2)); await browser.close(); return { token, cookies }; } module.exports { login };逻辑说明headless: false是必须的因为要人工扫码。waitForSelector等的是登录后才会出现的账号昵称元素超时设了 120 秒。window.wx.commonData.token是公众平台页面里挂的全局 token不同版本可能路径不同如果取不到就换成从 URL 参数里解析。Cookie 和 token 一起缓存后续请求直接读文件不用每次重新登录。参数方面timeout可以根据网络情况调大cookieFile路径在配置里改。注意 Cookie 有有效期一般几小时到一天过期后请求会返回登录页这时候要删掉缓存文件重新扫码。3.2 文章列表模块翻页拉取历史文章链接拿到登录态后列表模块负责按公众号拉历史文章。公众平台的历史消息接口是分页的每页返回一批文章需要循环请求直到没有更多。// core/articleList.js const axios require(axios); const { delayMin, delayMax, maxRetry } require(../config); const { randomDelay } require(../utils/request); async function fetchArticleList(biz, token, cookies, begin 0) { const url https://mp.weixin.qq.com/mp/profile_ext; const params { action: getmsg, __biz: biz, // 公众号唯一标识 offset: begin, // 分页偏移 count: 10, // 每页条数 token: token }; for (let i 0; i maxRetry; i) { try { const res await axios.get(url, { params, headers: { Cookie: cookies.map(c ${c.name}${c.value}).join(; ) } }); await randomDelay(delayMin, delayMax); return res.data; } catch (e) { if (i maxRetry - 1) throw e; await randomDelay(delayMin * 2, delayMax * 2); } } } module.exports { fetchArticleList };逻辑说明__biz是每个公众号的唯一标识可以从任意一篇该号文章的链接里提取。offset是分页偏移每次加count往下翻。请求头里带上缓存的 Cookie。重试逻辑是失败后延迟加倍再试最多maxRetry次。参数说明count一般设 10设太大容易被限制begin初始为 0循环时累加。返回的数据里包含文章标题、链接、发布时间等需要解析出msgList字段再逐条处理。3.3 详情模块解析正文、阅读量和点赞量列表拿到的是文章链接详情模块逐篇打开解析。正文在 HTML 里阅读量和点赞量通常在页面脚本的变量里。// core/articleDetail.js const axios require(axios); const cheerio require(cheerio); const { randomDelay, delayMin, delayMax } require(../utils/request); async function fetchArticleDetail(url) { const res await axios.get(url, { headers: { User-Agent: Mozilla/5.0 ... } }); const html res.data; const $ cheerio.load(html); // 正文 const content $(#js_content).text().trim(); // 阅读量和点赞量藏在脚本变量里 const readNum (html.match(/var read_num (\d)/) || [])[1] || 0; const likeNum (html.match(/var like_num (\d)/) || [])[1] || 0; await randomDelay(delayMin, delayMax); return { url, content, readNum, likeNum }; } module.exports { fetchArticleDetail };逻辑说明cheerio用来选正文容器#js_content。阅读量和点赞量不在 DOM 里而是页面内联脚本的变量用正则从原始 HTML 里抠。这两个字段有时是read_num有时是old_real_cnt不同时期页面结构有差异需要做兼容。参数说明User-Agent要伪装成正常浏览器否则可能返回验证页。正则里的(\d)只匹配数字遇到「10万」这种文本要额外处理常见做法是判断是否包含「万」再乘以 10000。3.4 评论模块翻页抓取评论区评论区是单独接口需要文章 ID 和评论分页参数。// core/comment.js const axios require(axios); async function fetchComments(articleId, token, cookies, page 0) { const url https://mp.weixin.qq.com/mp/appmsg_comment; const params { action: getcomment, __biz: articleId, comment_id: articleId, offset: page * 10, count: 10, token: token }; const res await axios.get(url, { params, headers: { Cookie: cookies.map(c ${c.name}${c.value}).join(; ) } }); return res.data; } module.exports { fetchComments };逻辑说明评论接口按offset翻页每页 10 条。返回的 JSON 里有评论内容、点赞数、回复列表。注意评论接口对频率更敏感延迟要比文章详情更大。参数说明articleId从文章链接或详情页里提取page从 0 开始递增直到返回空列表为止。4. 跑起来环境准备、登录、抓取、落盘的完整步骤4.1 环境准备与依赖安装先确认 Node.js 版本建议 14 以上。然后进项目目录装依赖# 安装依赖 npm install # 如果 puppeteer 下载 Chromium 慢可以指定国内镜像 npm config set puppeteer_download_host https://npmmirror.com/mirrors npm install puppeteerpuppeteer会下载一个 Chromium体积较大网络不好时容易卡住。用镜像源能快不少。装完后检查node_modules里有没有puppeteer和cheerio。4.2 首次登录扫码并生成缓存node core/login.js运行后会弹出一个浏览器窗口用微信扫码登录公众平台。登录成功后窗口自动关闭.cookie.json生成。这一步只需要做一次之后抓取都复用这个缓存。如果后续请求返回登录页说明缓存过期删掉文件重新跑这条命令。4.3 配置目标公众号并启动抓取在入口文件里填入目标公众号的__biz然后运行node index.jsindex.js会依次调用列表、详情、评论模块把每个公众号的数据写到output/下。每个公众号一个文件夹里面是文章列表 JSON 和每篇文章的正文 HTML。4.4 数据落盘格式与字段说明落盘结构大致如下文件内容关键字段list.json文章列表title, url, publishTimedetail/{id}.json单篇详情content, readNum, likeNumcomments/{id}.json评论数据content, likeNum, replieslist.json是索引详情和评论按文章 ID 分开存。这样设计的好处是单篇失败不影响整体重跑时可以根据 ID 跳过已完成的。5. 避坑与常见问题登录态、风控、字段缺失的排查5.1 请求返回登录页Cookie 失效现象抓取时返回的 HTML 是登录页或者接口返回ret: -3。原因是 Cookie 或 token 过期公众平台的登录态一般只维持几小时。解决方法是删掉.cookie.json重新跑login.js扫码。如果频繁失效检查是不是同一账号在多处登录导致互踢。5.2 抓取中途被限制返回空数据现象翻了几页后接口返回空列表或错误码。原因是请求频率过高触发风控。解决方法是调大config里的delayMin和delayMax把concurrency降到 1 或 2。血泪经验是别贪快稳定比速度重要。5.3 阅读量字段取不到正则匹配为空现象readNum一直是 0。原因是页面结构变了变量名不再是read_num。解决方法是打开一篇抓到的 HTML搜read_num或old_real_cnt看实际变量名改articleDetail.js里的正则。常见做法是同时匹配多个变量名取第一个非空的。5.4 评论接口返回空但文章明明有评论现象评论 JSON 是空的。原因是评论接口需要额外的comment_id这个 ID 和文章 ID 不一定相同要从详情页的脚本里单独提取。解决方法是先在详情页 HTML 里搜comment_id确认取值路径再传给评论模块。5.5 落盘文件乱码或截断现象正文 HTML 打开是乱码或者只写了一半。原因是写入时没等异步完成或者编码没指定。解决方法是落盘用fs.writeFileSync(path, data, utf8)并确保在await之后再写。如果文件很大考虑流式写入。6. 二次开发扩展字段、增量抓取和调度改造6.1 扩展抓取字段从正文里提取封面和作者默认只抓了正文和互动数如果想加封面图和作者名在articleDetail.js里补两行// 封面图 const cover $(#js_content img).first().attr(data-src) || ; // 作者 const author $(#js_name).text().trim();>const existing new Set(JSON.parse(fs.readFileSync(output/list.json)).map(a a.url)); const newArticles fetched.filter(a !existing.has(a.url));这样每次只抓新增文章。注意公众号文章链接里可能带临时参数去重前先归一化去掉scene之类的尾巴。6.3 调度改造定时任务与失败重试队列想让它每天自动跑可以用node-cron挂个定时任务const cron require(node-cron); cron.schedule(0 3 * * *, () { runAll(); });凌晨 3 点跑避开白天风控严的时段。失败的文章单独记到一个failed.json下次启动时优先重试。我一般会把重试队列和主流程分开避免一个坏链接拖垮整批。6.4 验证改造是否生效改完代码后别直接全量跑。先拿一个公众号、限制 5 篇文章做小批量验证MAX_ARTICLES5 node index.js在入口里读这个环境变量控制上限。确认字段完整、落盘正常、没有报错再放开全量。从那以后我每次改抓取逻辑都强制走一遍小批量验证省得跑了几小时才发现字段全空。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。