简介:这是一份基于 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 图标、woff2/ttf 等字体资源,以及 Dockerfile、docker-compose.yml、证书与说明文档,前后端与代理规则模块划分清晰。目前已有 2627 人学习下载,适合想研究代理抓包、数据落库与爬虫工程化的读者参考复用。
1. 公众号历史文章抓取:从阅读量到评论,一套 JavaScript 爬虫能拿到什么
做公众号内容分析的人多半遇到过这种局面:想复盘某个号三年的选题曲线,后台只能一页页翻;想统计阅读量中位数,手动复制到表格里眼睛都花了。这个wechat_spider项目就是冲着这个痛点来的——用 JavaScript 写的一套抓取工具,目标是把公众号的历史文章链接、正文内容、阅读量、点赞量、评论一并拉下来。它适合做内容运营分析、竞品号监测、选题库搭建的从业者,也适合想拿真实数据练手的数据方向同学。技术栈是 Node.js 加 JavaScript,核心思路是模拟客户端请求、解析返回结构、分页遍历,最后落成结构化数据。下面按「它怎么跑起来、参数怎么调、哪里会翻车」的顺序拆一遍。
2. 环境搭建与请求链路:Node.js 侧要准备什么
2.1 运行环境与依赖清单
这套工具跑在 Node.js 上,建议用 LTS 版本,太老的版本对async/await和fetch支持不完整,容易在并发请求那一步报奇怪的错。依赖方面,常见做法是axios负责 HTTP 请求、cheerio负责 HTML 解析、fs-extra负责文件写入、p-limit控制并发数。这几个包在 npm 上都能直接装,不需要额外编译工具链。
先把项目目录初始化好,装依赖:
# 初始化项目,生成 package.json npm init -y # 安装核心依赖 npm install axios cheerio fs-extra p-limit # 如果要做定时任务,再加一个 npm install node-cron装完之后检查node_modules里有没有axios和cheerio这两个目录,缺了说明网络或镜像源有问题,换源重装即可。这一步看着简单,但后面所有请求和解析都依赖它们,装不干净后面全是玄学报错。
2.2 请求链路:从公众号入口到文章列表
抓取的核心链路分三段:第一段是拿到目标公众号的文章列表入口,第二段是翻页遍历历史文章拿到每篇的链接,第三段是逐篇请求详情页解析阅读量、点赞和评论。第一段通常需要一个能定位到该公众号的标识参数,这个参数在公众号主页的请求里能找到,具体字段名各版本可能不同,常见做法是从主页 URL 或返回的 JSON 里提取。
下面是一个请求封装的骨架,把公共头、超时、重试都收在一起:
const axios = require('axios'); // 创建带默认配置的请求实例 const client = axios.create({ timeout: 10000, // 单次请求超时 10 秒,太短容易误判失败 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json, text/plain, */*' } }); // 带重试的请求函数,失败后隔 1 秒重试,最多 3 次 async function requestWithRetry(url, params = {}, retries = 3) { for (let i = 0; i < retries; i++) { try { const res = await client.get(url, { params }); return res.data; } catch (err) { if (i === retries - 1) throw err; await new Promise(r => setTimeout(r, 1000)); } } } module.exports = { requestWithRetry };这段代码里timeout设 10 秒是个经验值,公众号接口偶尔会慢,设 3 秒会大量误报失败。重试间隔 1 秒是为了避开瞬时抖动,重试次数 3 次足够,再多说明目标接口本身有问题,硬重试没意义。User-Agent必须带,不带的话部分接口直接返回空数据,这是最常见的翻车点之一。
2.3 分页遍历与去重
历史文章列表是分页返回的,每页条数有限,需要循环请求直到没有下一页。这里要处理两个问题:一是翻页游标或偏移量怎么传,二是重复文章怎么去重。常见做法是用一个Set存已抓到的文章 ID,每次解析后先查重再入库。
const seen = new Set(); // 已抓取的文章 ID 集合 async function fetchArticleList(accountId, maxPages = 50) { const all = []; let offset = 0; const pageSize = 20; for (let page = 0; page < maxPages; page++) { const data = await requestWithRetry('/api/article/list', { account_id: accountId, offset: offset, count: pageSize }); const list = data?.articles || []; if (list.length === 0) break; // 没有数据了,提前结束 for (const item of list) { if (seen.has(item.id)) continue; // 去重 seen.add(item.id); all.push(item); } offset += pageSize; await new Promise(r => setTimeout(r, 500)); // 每页之间歇一下 } return all; }maxPages是保护性上限,防止接口异常时无限循环。每页之间停 500 毫秒是降低请求频率,太快容易触发限流。offset的递增方式取决于接口设计,有的用游标cursor,有的用页码page,拿到实际返回后按字段调整。去重这步不能省,翻页过程中接口偶尔会返回重复项,不去重最后统计阅读量会偏高。
3. 正文、阅读量、点赞与评论的解析:字段怎么取、结构怎么落
3.1 详情页解析:正文与互动数据
拿到文章链接后,逐篇请求详情页。正文一般在 HTML 的某个容器里,用cheerio按选择器提取;阅读量和点赞量有的在页面内嵌的 JS 变量里,有的在单独的接口返回中。常见做法是先用cheerio抓正文,再用正则从页面源码里匹配阅读量字段。
const cheerio = require('cheerio'); function parseArticleDetail(html) { const $ = cheerio.load(html); // 正文:取内容容器,去掉脚本和样式 const content = $('#js_content').text().trim(); // 阅读量、点赞量常藏在页面脚本里,用正则匹配 const readMatch = html.match(/read_num["']?\s*[:=]\s*["']?(\d+)/); const likeMatch = html.match(/like_num["']?\s*[:=]\s*["']?(\d+)/); return { content: content, readNum: readMatch ? parseInt(readMatch[1], 10) : 0, likeNum: likeMatch ? parseInt(likeMatch[1], 10) : 0 }; }#js_content是正文容器的常见 ID,不同模板可能不同,抓不到时先打印 HTML 看结构再改选择器。正则里的read_num和like_num是字段名,实际字段名以页面源码为准,匹配不到就返回 0,避免整条记录失败。正文用.text()会丢掉图片和链接,如果需要保留富文本,改用.html()再单独清洗。
3.2 评论抓取:分页与嵌套结构
评论通常是单独接口,按文章 ID 分页拉取,每条评论可能带子评论。抓的时候要处理两层分页:主评论翻页和子评论展开。常见做法是先拉主评论,再对每条有子评论的项发二次请求。
async function fetchComments(articleId, maxPages = 20) { const comments = []; let page = 1; while (page <= maxPages) { const data = await requestWithRetry('/api/comment/list', { article_id: articleId, page: page, limit: 20 }); const list = data?.comments || []; if (list.length === 0) break; for (const c of list) { comments.push({ user: c.nickname, text: c.content, like: c.like_num || 0, time: c.create_time }); } page++; await new Promise(r => setTimeout(r, 300)); } return comments; }maxPages同样做上限保护。评论接口对频率更敏感,间隔设 300 毫秒以上比较稳。子评论如果也要,就在主评论循环里判断c.reply_count > 0再发一次请求,但要注意嵌套层级别无限展开,设个深度上限。
3.3 数据落盘:JSON 与 CSV 两种格式
抓完的数据建议同时落 JSON 和 CSV。JSON 保留完整结构方便后续程序处理,CSV 方便直接丢进表格软件看。用fs-extra写入:
const fs = require('fs-extra'); async function saveData(articles, outputDir = './output') { await fs.ensureDir(outputDir); // 目录不存在就创建 // 完整数据存 JSON await fs.writeJson(`${outputDir}/articles.json`, articles, { spaces: 2 }); // 摘要字段存 CSV const header = 'title,readNum,likeNum,url\n'; const rows = articles.map(a => `"${a.title}",${a.readNum},${a.likeNum},"${a.url}"` ).join('\n'); await fs.writeFile(`${outputDir}/summary.csv`, header + rows, 'utf8'); }ensureDir保证目录存在,避免首次运行报错。CSV 里标题和 URL 用双引号包住,防止内容里的逗号破坏列结构。JSON 用spaces: 2格式化,方便人工查看。数据量大时 CSV 拼接可能占内存,可以改成流式写入,但一般几千篇以内直接拼没问题。
4. 避坑与排查:抓取过程中最容易翻车的五件事
4.1 请求返回空数据或 403
现象:请求发出后返回空数组或 403 状态码。原因:缺少必要的请求头,或请求频率过高被临时限制。解决:补全User-Agent、Referer等头,降低并发数,每页之间加延时。如果持续 403,说明该入口暂时不可用,换时间段再试。
4.2 阅读量字段匹配不到
现象:正文能抓到,但阅读量和点赞量全是 0。原因:字段名和正则不匹配,或数据在异步加载的接口里而非页面源码。解决:先打印完整 HTML 搜索read关键字定位真实字段名;如果页面源码里没有,改用抓包工具找到对应接口单独请求。
4.3 翻页死循环或漏抓
现象:程序一直翻页不结束,或中间漏掉几页。原因:翻页游标逻辑写错,或接口在某一页返回异常导致break提前触发。解决:打印每页返回的条数和游标值,确认递增逻辑;对异常返回做判断,区分「真的没有下一页」和「请求失败」。
4.4 评论抓取被限流
现象:评论抓到前几页后开始返回空或报错。原因:评论接口频率限制比文章列表更严。解决:把评论请求的间隔调到 500 毫秒以上,并发降到 1,必要时分批分时段抓。
4.5 数据重复或统计偏差
现象:同一篇文章出现多次,阅读量统计偏高。原因:翻页过程中接口返回重复项,去重没做或做得不彻底。解决:用文章 ID 做Set去重,入库前再查一次;统计前先按 ID 去重再算中位数和均值。
5. 进阶技巧:把抓取结果变成可复用的选题库
抓到数据只是第一步,真正省时间的是把结果整理成能直接用的选题库。我一般会做三件事:按阅读量分档、按时间看趋势、按关键词聚类。
分档用简单的分位数就行,不用上复杂模型:
function analyzeArticles(articles) { // 按阅读量排序后取分位数 const sorted = [...articles].sort((a, b) => a.readNum - b.readNum); const q = (p) => sorted[Math.floor(sorted.length * p)]?.readNum || 0; const low = q(0.25); // 25 分位 const mid = q(0.5); // 中位数 const high = q(0.75); // 75 分位 // 分档统计 const buckets = { low: 0, mid: 0, high: 0 }; for (const a of articles) { if (a.readNum <= low) buckets.low++; else if (a.readNum <= high) buckets.mid++; else buckets.high++; } return { low, mid, high, buckets }; }这段代码把文章按阅读量分成三档,q(0.25)是 25 分位值,低于它的算低阅读,高于 75 分位的算高阅读。分档之后就能看出这个号的内容基本盘在哪,哪些选题属于爆款区间。趋势分析按月份分组算均值,关键词聚类用简单的词频统计就够,不用上 NLP 模型。
验证抓取结果是否可靠,我习惯抽三篇手动核对阅读量和评论数,对不上就回头查解析逻辑。这个习惯是从一次翻车来的——当时正则匹配到了页面里另一个同名字段,导致所有阅读量都偏大,抽检才发现。从那以后我每次抓完都强制走一遍抽检,三篇对不上就全量重跑。希望帮到你。
本文还有配套的精品资源,点击获取