简介这是一份基于 Node.js 的微信爬虫项目源码面向需要批量采集公众号数据的开发者与数据分析人员通过 AnyProxy 中间人代理原理抓取微信文章正文、阅读量、点赞量、在看数及评论等信息并支持获取公众号历史文章链接。项目已适配 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项目就是冲着这个痛点来的——它把「获取公众号所有历史文章链接」和「抓取文章内容、阅读量、点赞量、评论」这两件事串成了一条自动化链路。技术栈是 JavaScriptNode.js 侧适合做内容运营分析、竞品监控、数据归档的开发者。它不是一个开箱即用的 SaaS而是一套需要你理解请求链路、自己配好抓取入口的工程代码下面我按「能干什么 → 怎么跑 → 怎么改」的顺序拆一遍。2. 项目定位与技术选型为什么用 Node.js 做抓取层2.1 这个项目到底解决什么问题公众号的数据有两个入口一个是文章列表历史文章链接一个是单篇文章的详情正文、阅读量、点赞、评论。前者靠的是公众号后台的「超链接」素材接口后者靠的是文章页面的 HTML 和接口返回。wechat_spider把这两段拆成了两个模块列表抓取负责翻页拿到biz、mid、idx、sn这几个关键参数拼出永久链接详情抓取负责拿着链接去解析页面和接口数据。它适合的场景很明确你需要批量拿到某个号的历史文章清单并且想顺带把每篇的互动数据落库。不适合的场景也说清楚——它不负责绕过登录态你得自己提供有效的抓取凭证比如后台登录后的 cookie 或 token这是所有公众号抓取类项目的共同前提。2.2 技术栈拆解Node.js 请求库 解析库项目主体是 JavaScript跑在 Node.js 上。常见的依赖组合是这样// package.json 依赖片段常见做法 { dependencies: { axios: ^1.6.0, // 发 HTTP 请求拿列表和详情 cheerio: ^1.0.0, // 服务端 jQuery解析文章 HTML puppeteer: ^21.0.0, // 需要渲染时兜底抓动态加载的评论 mysql2: ^3.6.0, // 落库存文章和互动数据 dayjs: ^1.11.0 // 时间格式化处理发布时间 } }axios负责纯接口请求速度快、资源占用低适合列表翻页这种高频调用。cheerio用来从文章页 HTML 里抠正文和初始的阅读量字段。puppeteer是兜底方案——评论区和部分互动数据是异步加载的纯请求拿不到这时候起一个无头浏览器去等 DOM 渲染完再取。mysql2负责持久化把文章链接、标题、发布时间、阅读量、点赞数、评论内容分别落到不同的表里。提示如果你的抓取量不大可以把mysql2换成better-sqlite3省去装数据库的麻烦单文件就能存。2.3 目录结构模块怎么分一个典型的工程目录长这样wechat_spider/ ├── config/ │ └── index.js # cookie、数据库连接、并发数配置 ├── src/ │ ├── list/ │ │ ├── fetchList.js # 拉取历史文章列表翻页逻辑 │ │ └── parseLink.js # 从列表响应里提取文章参数 │ ├── detail/ │ │ ├── fetchDetail.js # 请求单篇文章页面 │ │ ├── parseContent.js # cheerio 解析正文和阅读量 │ │ └── fetchComment.js # 抓评论走 puppeteer │ ├── db/ │ │ ├── articleDao.js # 文章表读写 │ │ └── commentDao.js # 评论表读写 │ └── utils/ │ ├── request.js # 统一请求封装带重试和延时 │ └── logger.js # 日志 ├── index.js # 入口串起列表→详情→落库 └── package.json这个分法的好处是列表和详情解耦你可以只跑列表拿链接也可以拿着已有链接单独跑详情。utils/request.js是全局的请求出口重试、延时、UA 伪装都收在这里改一处全链路生效。3. 核心模块拆解列表翻页与详情解析的完整链路3.1 历史文章列表抓取翻页参数怎么传列表抓取的核心是模拟公众号后台的素材列表请求。关键参数是begin偏移量和count每页条数配合登录态 cookie 一起发。// src/list/fetchList.js const axios require(axios); const config require(../../config); async function fetchArticleList(begin 0, count 20) { const url https://mp.weixin.qq.com/cgi-bin/appmsg; const params { action: list_ex, begin: begin, // 偏移量翻页靠它递增 count: count, // 每页条数一般 20 type: 9, // 9 表示图文消息 token: config.token, // 后台登录 token lang: zh_CN, f: json, ajax: 1 }; const res await axios.get(url, { params, headers: { Cookie: config.cookie, // 登录态必须 User-Agent: config.ua }, timeout: 10000 }); // 返回结构里 app_msg_list 是文章数组 return res.data.app_msg_list || []; } module.exports { fetchArticleList };逻辑说明begin每次加count就是下一页循环调用直到返回空数组说明翻到底了。token和cookie是登录凭证从浏览器开发者工具里复制配到config/index.js。type: 9是图文消息的类型标识改成别的值能拿其他素材类型。参数说明count不建议调太大常见做法是 20调大容易被限流begin从 0 开始每次递增count。返回的每条数据里有aid文章 id、title、create_time但永久链接需要另外拼。3.2 永久链接拼接biz、mid、idx、sn 四个参数列表接口返回的aid不能直接当链接用真正的永久链接需要biz、mid、idx、sn四个参数。biz是公众号的唯一标识mid是消息 ididx是消息里的第几篇sn是短链标识。// src/list/parseLink.js function buildArticleUrl(item, biz) { // item 来自列表接口biz 是公众号标识 const { aid, mid, idx, sn } item; // 常见拼接格式 return https://mp.weixin.qq.com/s?__biz${biz}mid${mid}idx${idx}sn${sn}; } module.exports { buildArticleUrl };逻辑说明biz一般从列表接口的响应头或第一条数据的biz字段拿整个号固定不变。mid、idx、sn每条文章不同。拼出来的链接就是可以在浏览器直接打开的永久链接。参数说明如果列表返回里没有sn有些版本需要用aid去换常见做法是先请求一次详情页拿重定向后的真实链接。这一步是踩坑高发区后面避坑章节细说。3.3 文章详情解析正文、阅读量、点赞量拿到链接后请求文章页用cheerio解析。正文在#js_content里阅读量和点赞量在页面的 JS 变量里。// src/detail/parseContent.js const cheerio require(cheerio); function parseArticle(html) { const $ cheerio.load(html); // 正文 const content $(#js_content).text().trim(); // 阅读量和点赞量藏在页面脚本里用正则抠 const readNumMatch html.match(/var read_num (\d)/); const likeNumMatch html.match(/var like_num (\d)/); return { content, readNum: readNumMatch ? parseInt(readNumMatch[1]) : 0, likeNum: likeNumMatch ? parseInt(likeNumMatch[1]) : 0 }; } module.exports { parseArticle };逻辑说明#js_content是正文容器的固定 idcheerio加载后直接取文本。阅读量和点赞量不在 DOM 里而是页面内联脚本的变量用正则从原始 HTML 里抠最稳。read_num和like_num是常见变量名不同版本可能叫read_num_new之类需要按实际页面调整。参数说明parseInt兜底成 0避免变量不存在时报错。正文如果要做富文本保留把.text()换成.html()但要注意清理内联样式。3.4 评论抓取为什么必须上 puppeteer评论区是异步加载的纯axios拿到的 HTML 里没有评论内容。这时候需要puppeteer起一个无头浏览器等评论 DOM 渲染出来再取。// src/detail/fetchComment.js const puppeteer require(puppeteer); async function fetchComments(url) { const browser await puppeteer.launch({ headless: new }); const page await browser.newPage(); await page.goto(url, { waitUntil: networkidle2 }); // 等评论容器出现 await page.waitForSelector(.comment_item, { timeout: 10000 }).catch(() {}); const comments await page.evaluate(() { const items document.querySelectorAll(.comment_item); return Array.from(items).map(el ({ user: el.querySelector(.nickname)?.innerText || , content: el.querySelector(.content)?.innerText || })); }); await browser.close(); return comments; } module.exports { fetchComments };逻辑说明waitUntil: networkidle2等网络基本空闲waitForSelector等评论容器出现超时用catch兜住避免卡死。page.evaluate在浏览器上下文里跑直接操作 DOM 取数据。参数说明headless: new是新版无头模式资源占用比旧版低。评论多的时候要滚动加载常见做法是循环page.evaluate触发滚动再等新节点。4. 跑起来环境准备、配置与执行步骤4.1 环境准备与依赖安装先把 Node.js 装好建议 18 以上。然后拉代码装依赖# 克隆项目后进入目录 cd wechat_spider # 安装依赖 npm install # 如果 puppeteer 下载慢可以跳过 chromium 下载 # PUPPETEER_SKIP_DOWNLOADtrue npm install逻辑说明npm install会按package.json装齐依赖。puppeteer默认会下载一个 Chromium体积大、国内网络容易卡用环境变量跳过运行时指向系统已装的 Chrome。参数说明PUPPETEER_SKIP_DOWNLOADtrue是官方支持的环境变量跳过下载后需要在launch时指定executablePath。4.2 配置文件cookie、token、数据库config/index.js是唯一的配置入口// config/index.js module.exports { cookie: 你的登录 cookie, // 从浏览器开发者工具复制 token: 你的后台 token, // 同上 ua: Mozilla/5.0 ..., // 建议用真实浏览器 UA db: { host: 127.0.0.1, user: root, password: your_password, database: wechat_spider }, concurrency: 3, // 详情抓取并发数 delay: 2000 // 每次请求间隔毫秒 };逻辑说明cookie和token是抓取的前提没有它们列表接口直接返回登录页。concurrency控制并发delay控制间隔两个一起决定抓取速度和对目标站的压力。参数说明concurrency建议 3 以内delay建议 2000 毫秒以上。调太快会触发限流返回空数据或验证码。4.3 建表与执行先建库建表再跑入口-- 文章表 CREATE TABLE article ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255), url VARCHAR(512), content TEXT, read_num INT DEFAULT 0, like_num INT DEFAULT 0, publish_time DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 评论表 CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, article_id INT, user VARCHAR(128), content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );# 跑入口先抓列表再抓详情 node index.js逻辑说明article表存文章主体和互动数据comment表通过article_id关联。index.js里先调fetchArticleList翻页拿全量链接再对每条链接调详情和评论抓取最后落库。参数说明url字段留 512 长度永久链接比较长。content用TEXT存正文长文够用。5. 避坑与常见问题抓取链路上的五个翻车点5.1 列表翻页返回空cookie 过期或 begin 越界现象翻到某一页后app_msg_list返回空数组但明明还有文章。原因通常是 cookie 过期或者begin超过了实际文章总数。解决先手动在浏览器打开后台确认登录态还在重新复制 cookiebegin递增时判断返回数组长度小于count就停止不要死循环。5.2 永久链接打不开sn 参数缺失现象拼出来的链接打开是「参数错误」。原因是列表接口某些版本不返回sn只有aid。解决拿aid先请求一次https://mp.weixin.qq.com/s?__bizxxxaidxxx跟随重定向拿到最终 URL从最终 URL 里提取sn。这一步多一次请求但能保证链接有效。5.3 阅读量抓不到变量名变了现象正则匹配read_num返回 null阅读量全是 0。原因是页面改版变量名可能变成read_num_new或藏在 JSON 里。解决把文章页 HTML 存下来搜read关键字看实际变量名调整正则。常见做法是同时匹配多个候选变量名取第一个命中的。5.4 评论抓取超时无头浏览器被识别现象puppeteer打开页面后评论区一直不加载waitForSelector超时。原因是无头模式被识别或者需要登录态。解决launch时加args: [--no-sandbox]设置真实 UA必要时传 cookie 到page.setCookie。还不行就开headless: false看实际卡在哪。5.5 请求被限流并发和延时没控好现象跑一会儿后接口返回验证码或直接拒绝。原因是并发太高、间隔太短。解决concurrency降到 1 到 2delay提到 3000 毫秒以上加随机抖动。utils/request.js里做统一重试遇到限流退避等待再试。6. 二次开发从单号抓取到多号调度的改造思路项目跑通之后最自然的改造方向是从「抓一个号」变成「抓一批号」。这里我分享一个自己踩过的改造路径。第一步是把biz和对应的 cookie 抽成配置数组而不是写死在代码里。常见做法是建一张account表存biz、name、cookie、last_sync_time调度时遍历这张表。这样加号不用改代码插一条记录就行。// 多号调度的核心循环 async function syncAllAccounts() { const accounts await accountDao.getAll(); for (const acc of accounts) { // 每个号独立跑列表和详情互不影响 const list await fetchArticleList(0, 20, acc.cookie, acc.token); for (const item of list) { const url buildArticleUrl(item, acc.biz); const detail await fetchDetail(url); await articleDao.save({ ...detail, accountId: acc.id }); } // 更新同步时间下次增量抓 await accountDao.updateSyncTime(acc.id, Date.now()); } }逻辑说明外层遍历账号内层遍历文章每个账号用自己的 cookie。last_sync_time用来做增量——下次只抓发布时间晚于上次同步的文章避免全量重跑。第二步是加增量判断。列表接口返回的create_time是发布时间和last_sync_time比较遇到旧文章就break因为列表是按时间倒序的。这一步能把重复抓取量砍掉一大半。第三步是失败重试和断点续传。详情抓取失败的文章记到一张failed_task表下次启动先补跑失败任务。我一般会在utils/request.js里做三层重试网络错误立即重试限流退避重试解析失败记录待人工确认。验证改造是否成功看两个指标一是article表的去重率同一url不应该有两条二是单号全量抓取耗时增量模式下应该比首次全量快一个数量级。我习惯在每次改完调度逻辑后先拿一个小号跑全量确认数据完整再上多号。从那以后我每次改抓取链路都强制先用单号小批量验证一遍再放开并发这个习惯帮我省了不少返工。希望帮到你。本文还有配套的精品资源点击获取