☰
微信爬虫实战:公众号历史文章与阅读点赞评论全量抓取
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 而不是 Python

2.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(浏览器自动化)、axios(HTTP 请求)、cheerio(HTML 解析)和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 puppeteer

puppeteer会下载一个 Chromium,体积较大,网络不好时容易卡住。用镜像源能快不少。装完后检查node_modules里有没有puppeteer和cheerio。

4.2 首次登录:扫码并生成缓存

node core/login.js

运行后会弹出一个浏览器窗口,用微信扫码登录公众平台。登录成功后窗口自动关闭,.cookie.json生成。这一步只需要做一次,之后抓取都复用这个缓存。如果后续请求返回登录页,说明缓存过期,删掉文件重新跑这条命令。

4.3 配置目标公众号并启动抓取

在入口文件里填入目标公众号的__biz,然后运行:

node index.js

index.js会依次调用列表、详情、评论模块,把每个公众号的数据写到output/下。每个公众号一个文件夹,里面是文章列表 JSON 和每篇文章的正文 HTML。

4.4 数据落盘格式与字段说明

落盘结构大致如下:

文件内容关键字段
list.json文章列表title, url, publishTime
detail/{id}.json单篇详情content, readNum, likeNum
comments/{id}.json评论数据content, likeNum, replies

list.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_ARTICLES=5 node index.js

在入口里读这个环境变量控制上限。确认字段完整、落盘正常、没有报错,再放开全量。从那以后我每次改抓取逻辑都强制走一遍小批量验证,省得跑了几小时才发现字段全空。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询