ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Node.js的微信公众号全量文章爬虫:PC与手机端配合抓取实战

基于Node.js的微信公众号全量文章爬虫:PC与手机端配合抓取实战 简介网络爬虫作为数据采集的核心技术其原理是通过模拟浏览器行为或直接调用API接口自动抓取并解析网页数据。在数据驱动决策的背景下爬虫技术对于市场分析、竞品研究和内容聚合具有重要价值广泛应用于舆情监控、价格追踪和知识库构建等场景。本文聚焦于微信公众号这一特定数据源针对其反爬机制强、历史文章获取难的问题深入探讨了如何利用PC端与手机端微信配合的抓包技术结合Node.js的异步高效特性实现稳定可控的全量文章抓取方案。该方案通过分析真实用户操作触发的网络请求逆向解析关键参数并构建自动化脚本有效解决了常规爬虫框架在数据完整性和可控性上的不足为内容分析和数据备份提供了可靠的技术路径。1. 项目概述与核心价值最近在做一个内容分析的项目需要把几个目标公众号的历史文章都扒下来做语料库。一开始想着用现成的爬虫框架但试了几个发现要么被封得厉害要么就是只能抓最近几十篇对于动辄上千篇的深度号根本不够用。市面上那些号称全量爬取的工具要么收费不菲要么就是黑盒操作数据安全性和可控性都存疑。于是我决定自己动手基于 Node.js 撸一个工具核心思路是利用微信 PC 端和手机端的配合模拟真实用户操作来获取数据最终将文章列表和详情都规整地保存为 JSON 格式。这个工具的核心价值在于“全量”和“可控”。所谓全量是指它能突破常规接口或网页抓取的数量限制理论上只要公众号存在且可访问就能拿到所有历史文章。可控则意味着整个流程——从登录认证、列表翻页到详情抓取——你都能看得见、摸得着可以根据目标公众号的特性更新频率、文章数量灵活调整策略比如设置抓取间隔防封或者只抓取特定时间段的文章。最终生成的 JSON 文件结构清晰包含了标题、发布时间、原文链接、摘要、正文内容可选等字段非常适合直接导入数据库进行分析或者用于内容备份、聚合展示。它特别适合这几类人一是像我一样的内容分析师或运营人员需要批量分析竞品或行业标杆公众号的内容策略二是开发者需要构建自己的公众号文章搜索引擎或内容聚合平台三是个人用户想要备份自己关注的优质公众号内容防止文章被删除。整个过程不需要你懂太多高深的逆向工程关键在于对微信客户端通信逻辑的耐心梳理和模拟。2. 工具设计思路与核心原理拆解2.1 为什么选择 PC 端与手机端配合单纯从技术实现难度来看直接抓取微信公众号的网页端如 mp.weixin.qq.com或者利用公开的搜索接口似乎更简单。但实际操作过你就会发现网页端需要处理复杂的登录态Cookie、Token且列表页有很强的反爬机制比如动态加载、请求参数加密。而公开的搜索接口如搜狗微信不仅数据不全、有延迟而且严格限制了访问频率和数量。因此我的思路转向了客户端。微信 PC 版作为一个成熟的桌面应用它与服务器之间的通信协议相对稳定。更重要的是我们可以通过抓包工具如 Fiddler、Charles拦截和分析这些通信过程。然而PC 端获取公众号历史文章列表的接口往往需要依赖手机微信进行授权或触发。这就是“配合”的由来手机端负责进行关键的“点击”或“授权”动作触发 PC 端与服务器交换数据PC 端则负责实际的数据请求和接收因为其网络流量更容易被我们捕获和解析。这种模式模拟了一个真实用户的操作你在手机微信上打开某个公众号点击“查看历史消息”这个动作会同步到登录同一账号的 PC 微信上PC 端随之向服务器请求该公众号的历史文章列表数据。我们的工具就扮演了这个“中间人”的角色自动化了手机端的触发动作并监听和解析 PC 端发出的网络请求。2.2 核心流程与技术栈选型整个工具的流程可以分解为以下几个核心步骤环境准备与抓包配置在 PC 上安装微信客户端和抓包工具并配置代理使得微信的所有网络流量都经过抓包工具。登录与认证在 PC 和手机上登录同一个微信账号。这是后续所有操作的基础。目标公众号定位在手机微信上找到目标公众号。这里可以通过搜索公众号名称或从“订阅号消息”列表进入。触发历史列表请求在手机端点击公众号的“查看历史消息”或类似入口。这是最关键的一步它会让服务器生成一个针对该公众号的、带有特定令牌Token的列表请求地址。拦截与解析列表接口抓包工具会捕获到 PC 端发出的请求历史文章列表的 HTTPS 请求。我们需要从中提取出关键的请求 URL、参数如uin、key、pass_ticket、__biz等以及响应数据格式。自动化模拟请求使用 Node.js 编写脚本模拟步骤5中捕获到的请求向微信服务器请求文章列表数据。通常需要处理分页逻辑因为一次请求只返回少量文章如10-20篇。解析列表与获取详情从列表接口的响应中解析出每篇文章的标题、链接、发布时间等信息。然后根据文章链接再次模拟请求抓取文章详情页的 HTML并从中提取正文内容。数据清洗与持久化将提取到的文章信息列表信息和详情内容进行清洗如去除 HTML 标签、空白字符然后按照预设的结构化格式JSON保存到本地文件。技术栈选择 Node.js 的原因异步高效处理大量网络 I/O请求文章列表和详情是 Node.js 的强项其非阻塞 I/O 模型非常适合这种高并发的爬虫场景。生态丰富有axios或node-fetch用于发送 HTTP/HTTPS 请求cheerio用于解析 HTML类似 jQuerypuppeteer或playwright可用于更复杂的、需要执行 JavaScript 的自动化操作虽然本项目主要靠模拟接口但可作为备选。易于部署与集成生成的脚本可以轻松地在服务器或本地持续运行也方便与其他数据处理管道如 Python 数据分析脚本集成。注意整个流程高度依赖对微信客户端通信协议的逆向分析。微信的接口参数和加密方式可能会更新这意味着工具可能需要定期维护和调整。这不是一个“一劳永逸”的解决方案而是一个需要你理解其原理并能随之调整的“半自动化”工具。3. 环境准备与关键配置详解3.1 抓包工具配置以 Fiddler Everywhere 为例抓包是逆向分析的基石。这里我推荐使用 Fiddler Everywhere因为它对 HTTPS 流量解密支持较好界面也比经典的 Fiddler 更现代。安装与信任根证书安装 Fiddler Everywhere 后首次启动它会提示你安装根证书。务必在系统和/或浏览器中信任此证书。这是解密 HTTPS 流量的前提。在 Fiddler 的Settings HTTPS中确保 “Capture HTTPS traffic” 和 “Decrypt HTTPS traffic” 选项是开启的。配置代理与微信客户端Fiddler 默认监听127.0.0.1:8866。你需要确保微信 PC 版的网络流量经过这个代理。方法一推荐在 Fiddler 的Settings Connections中开启 “Allow remote computers to connect”。然后在系统的网络设置中手动配置全局 HTTP/HTTPS 代理为127.0.0.1:8866。这样所有应用包括微信的流量都会经过 Fiddler。方法二如果微信客户端支持独立配置代理可以在其设置中指定。但微信 PC 版通常不提供此选项所以方法一更通用。重要步骤配置好代理后完全关闭并重新启动微信 PC 版以确保新的代理设置生效。手机端配置让手机和 PC 处于同一局域网Wi-Fi。在手机 Wi-Fi 设置中配置代理为“手动”主机名填写你 PC 的局域网 IP 地址如192.168.1.100端口填写 Fiddler 的监听端口如8866。在手机浏览器中访问http://你的PC_IP:8866例如http://192.168.1.100:8866下载并安装 Fiddler 的根证书到手机并信任它iOS 需要在“设置 通用 关于本机 证书信任设置”中完全信任Android 路径因系统而异。3.2 Node.js 环境与依赖安装确保你的系统已安装 Node.js建议 LTS 版本如 v18.x。可以通过node -v和npm -v检查。创建一个新的项目目录初始化并安装核心依赖mkdir wechat-article-crawler cd wechat-article-crawler npm init -y npm install axios cheerio fs-extra progressaxios: 用于发送 HTTP/HTTPS 请求比内置的http/https模块更友好。cheerio: 用于在服务器端解析和操作 HTML提取文章正文。fs-extra: 提供了比原生fs模块更强大的文件操作功能如确保目录存在、读写 JSON 文件。progress: 用于在命令行显示抓取进度条提升体验。此外你可能还需要puppeteer作为备选方案用于应对那些严重依赖前端 JavaScript 渲染的页面虽然公众号文章页大多是静态 HTML。可以先不安装必要时再加。4. 核心环节实现拦截、模拟与解析4.1 捕获关键的历史列表请求完成环境配置后开始实操抓包确保 Fiddler 正在运行并捕获流量。在已登录的 PC 微信上不要进行任何操作。在已配置代理的手机微信上找到目标公众号点击进入其主页然后点击“查看历史消息”在公众号介绍页下方。此时观察 Fiddler 的流量列表。你会看到瞬间出现大量请求。我们需要找到那个携带了历史文章数据的请求。识别特征这个目标请求通常是GET方法域名可能包含mp.weixin.qq.com或微信的某个内部域名URL 路径中常含有/mp/profile_ext、/mp/getappmsgext或类似的字符串。请求参数会非常长包含__biz公众号的唯一标识、uin、key、pass_ticket、appmsg_token等关键字段。查看响应点击这个请求在 Fiddler 右侧的 Inspectors 面板中查看 “TextView” 或 “WebView”。如果响应体是一大段 JSON 数据里面包含了app_msg_list、general_msg_list这样的字段并且字段值里有文章标题、链接等信息那么恭喜你找到了核心接口。实操心得第一次可能很难一眼认出。一个技巧是在手机点击“历史消息”前先清空 Fiddler 的会话列表这样新产生的请求会非常集中。另外响应内容如果是 JSON可以尝试用 Fiddler 的 “JSON” 视图格式化便于阅读。务必完整地记录下这个请求的URL和Headers特别是Cookie、User-Agent和那些自定义的头部。4.2 构建 Node.js 爬虫脚本假设我们捕获到的列表接口 URL 模板如下已脱敏https://mp.weixin.qq.com/mp/profile_ext?actiongetmsg__bizMzUxODkzNTg0Nwfjsonoffset0count10is_ok1scene124uin777key888pass_ticket...appmsg_token......可以看到offset参数控制翻页从0开始count控制每页数量。下面开始编写脚本的核心部分。1. 基础请求模块 (request.js):const axios require(axios); const fs require(fs-extra); // 从抓包数据中复制的请求头 const HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/4.0.1326.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x6307062c), Cookie: 你的抓包获取的完整Cookie字符串, // 这是关键身份凭证 // ... 其他必要的头部如 Referer, Accept 等 }; // 创建一个配置好的 axios 实例 const apiClient axios.create({ headers: HEADERS, timeout: 30000, // 30秒超时 }); /** * 发送请求并处理响应 * param {string} url - 请求地址 * param {object} params - 查询参数 * returns {Promiseobject} 响应数据 */ async function makeRequest(url, params {}) { try { const response await apiClient.get(url, { params }); return response.data; } catch (error) { console.error(请求失败: ${url}, error.message); // 可以在这里加入重试逻辑 throw error; } } module.exports { makeRequest };2. 列表爬取与分页逻辑 (crawlList.js):const { makeRequest } require(./request); const fs require(fs-extra); const path require(path); const ProgressBar require(progress); // 基础URL和参数从抓包数据中提取 const BASE_URL https://mp.weixin.qq.com/mp/profile_ext; const BASE_PARAMS { action: getmsg, __biz: MzUxODkzNTg0Nw, // 替换为目标公众号的biz f: json, count: 10, // 每页数量可调整但不宜过大 is_ok: 1, scene: 124, uin: 777, // 替换为你的uin key: 888, // 替换为你的key pass_ticket: ..., // 替换 appmsg_token: ..., // 替换 // ... 其他固定参数 }; async function crawlAllArticles(outputDir ./data) { await fs.ensureDir(outputDir); const allArticles []; let offset 0; let hasMore true; let retryCount 0; const maxRetry 3; console.log(开始爬取公众号历史文章列表...); while (hasMore) { const params { ...BASE_PARAMS, offset }; console.log(正在抓取 offset${offset}...); try { const data await makeRequest(BASE_URL, params); // 解析响应这里需要根据实际接口返回结构调整 if (data data.general_msg_list) { const msgList JSON.parse(data.general_msg_list).list; if (!msgList || msgList.length 0) { hasMore false; console.log(没有更多文章了。); break; } for (const msg of msgList) { // 每条消息可能包含多篇文章主文次文 const appMsg msg.app_msg_ext_info; if (appMsg) { // 主文章 allArticles.push({ title: appMsg.title, link: appMsg.content_url, digest: appMsg.digest, publish_time: appMsg.datetime, cover: appMsg.cover, author: appMsg.author, copyright_stat: appMsg.copyright_stat, }); // 次文章如果有 if (appMsg.multi_app_msg_item_list) { for (const sub of appMsg.multi_app_msg_item_list) { allArticles.push({ title: sub.title, link: sub.content_url, digest: sub.digest, publish_time: sub.datetime || appMsg.datetime, // 次文可能无独立时间 cover: sub.cover, author: sub.author, copyright_stat: sub.copyright_stat, }); } } } } offset msgList.length; retryCount 0; // 成功则重置重试计数 console.log(已获取 ${allArticles.length} 篇文章。); // 礼貌性延迟避免请求过快 await sleep(2000 Math.random() * 1000); } else { console.warn(响应数据格式异常 offset${offset}:, JSON.stringify(data).substring(0, 200)); hasMore false; } } catch (error) { retryCount; console.error(第 ${retryCount} 次尝试失败 offset${offset}:, error.message); if (retryCount maxRetry) { console.error(偏移量 ${offset} 处重试 ${maxRetry} 次均失败停止爬取。); break; } await sleep(5000 * retryCount); // 失败后等待更长时间再重试 } } // 去重根据链接 const uniqueArticles Array.from(new Map(allArticles.map(item [item.link, item])).values()); const outputPath path.join(outputDir, article_list_${Date.now()}.json); await fs.writeJson(outputPath, uniqueArticles, { spaces: 2 }); console.log(列表爬取完成共 ${uniqueArticles.length} 篇文章已保存至: ${outputPath}); return uniqueArticles; } function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); } module.exports { crawlAllArticles };3. 文章详情抓取 (crawlDetail.js): 有了文章链接列表下一步是抓取每篇文章的详细内容。公众号文章页是 HTML我们需要用cheerio解析。const { makeRequest } require(./request); const cheerio require(cheerio); const fs require(fs-extra); const path require(path); async function crawlArticleDetail(articleUrl) { try { // 注意文章详情页的请求头可能需要调整特别是Referer const html await makeRequest(articleUrl, {}, { headers: { Referer: https://mp.weixin.qq.com/, // ... 其他可能需要的头部 } }); const $ cheerio.load(html); // 找到文章正文的容器通常是 id 为 js_content 的 div const contentHtml $(#js_content).html(); if (!contentHtml) { console.warn(未找到正文内容: ${articleUrl}); return null; } // 简单的清理移除脚本、样式等标签 const cleanedHtml contentHtml .replace(/script\b[^]*(?:(?!\/script)[^]*)*\/script/gi, ) .replace(/style\b[^]*(?:(?!\/style)[^]*)*\/style/gi, ) .replace(/\s/g, ) // 合并多余空白 .trim(); // 你也可以选择提取纯文本 const plainText $(#js_content).text().replace(/\s/g, ).trim(); return { url: articleUrl, content_html: cleanedHtml, content_text: plainText, crawl_time: new Date().toISOString(), }; } catch (error) { console.error(抓取文章详情失败: ${articleUrl}, error.message); return null; } } async function crawlAllDetails(articleList, outputDir ./data, concurrency 3) { const details []; const total articleList.length; console.log(开始抓取 ${total} 篇文章的详情...); // 简单的并发控制 for (let i 0; i total; i concurrency) { const batch articleList.slice(i, i concurrency); const promises batch.map(article crawlArticleDetail(article.link)); const results await Promise.allSettled(promises); // 使用 allSettled 防止单个失败导致全部中断 for (const result of results) { if (result.status fulfilled result.value) { details.push(result.value); } } console.log(进度: ${details.length}/${total}); await sleep(1500 Math.random() * 1000); // 控制请求频率 } const outputPath path.join(outputDir, article_details_${Date.now()}.json); await fs.writeJson(outputPath, details, { spaces: 2 }); console.log(详情抓取完成共 ${details.length} 篇已保存至: ${outputPath}); return details; } module.exports { crawlArticleDetail, crawlAllDetails };4. 主入口脚本 (index.js):const { crawlAllArticles } require(./crawlList); const { crawlAllDetails } require(./crawlDetail); (async () { try { // 第一步抓取文章列表 const articleList await crawlAllArticles(./output); console.log(成功获取 ${articleList.length} 篇文章列表。); // 第二步根据列表抓取文章详情 if (articleList.length 0) { // 可以只抓取前N篇进行测试 // const testList articleList.slice(0, 5); await crawlAllDetails(articleList, ./output, 2); // 并发数设为2更保守 } console.log(全部任务完成); } catch (error) { console.error(主流程执行失败:, error); } })();5. 常见问题、排查技巧与优化策略5.1 请求失败与参数失效这是最常见的问题。表现通常是请求返回错误代码如-1或者返回的数据为空。Cookie/Uin/Key/Token 过期这些参数是登录态的体现都有有效期。解决方案重新进行抓包流程从最新的 Fiddler 会话中提取这些参数。特别是Cookie它是最关键的凭证。可以将获取最新Cookie的步骤脚本化但这涉及到模拟登录复杂度极高。目前最实用的方法还是手动更新。__biz参数错误确保你提取的__biz参数与目标公众号对应。不同公众号的__biz不同。请求频率过高即使参数有效短时间内发送大量请求也会被微信服务器限制。解决方案在代码中合理设置延迟 (sleep)。列表请求间隔建议 2-5 秒详情请求间隔建议 1-3 秒并加入随机抖动。使用Promise.all控制并发数如上述代码中的concurrency参数。IP 被暂时限制如果同一 IP 在短时间内行为异常可能会被限制访问。解决方案暂停爬取一段时间如几小时或隔天或者考虑使用代理 IP 池但这会大大增加复杂度。5.2 数据解析异常JSON 解析错误接口返回的数据可能不是标准 JSON或者结构有变化。解决方案在JSON.parse前先用try...catch包裹并打印出原始响应片段进行比对。确保你解析的是正确的字段如data.general_msg_list。HTML 结构变化公众号文章页的 HTML 结构可能改版导致cheerio选择器#js_content找不到内容。解决方案手动打开一篇公众号文章检查元素确认正文容器的选择器是否变化。也可能是文章内容被懒加载此时需要考虑使用puppeteer等无头浏览器来获取渲染后的 HTML。5.3 性能与稳定性优化增量爬取首次全量爬取后后续可以只爬取新文章。实现思路记录已爬取文章的最新发布时间下次爬取时从列表的offset0开始直到遇到发布时间早于记录时间的文章就停止。断点续传将爬取进度当前offset、已成功抓取的文章 ID 列表持久化到文件或数据库。当脚本因网络或错误中断后重启时可以从中断点继续。更优雅的错误处理与重试为网络请求实现指数退避的重试机制。对于因频率限制导致的错误可以动态增加等待时间。使用数据库当文章数量很大时JSON 文件会变得难以管理。可以考虑将数据存入 SQLite 或 MongoDB便于查询和去重。部署到服务器在本地电脑上长时间运行爬虫不现实。可以将脚本部署到云服务器上设置定时任务在凌晨等低峰期运行。5.4 法律与道德风险规避遵守robots.txt虽然微信没有公开的robots.txt但应尊重其服务条款。本工具仅用于个人学习、研究和合法的内容备份严禁用于商业爬取、数据倒卖、恶意攻击等用途。控制爬取速度主动限制请求频率避免对微信服务器造成不必要的负担这既是道德要求也能降低被封禁的风险。尊重版权抓取的内容版权仍归原作者所有。在展示、使用这些数据时应注明出处并遵守相关著作权法规。6. 数据存储与应用示例爬取到的 JSON 数据是结构化的宝藏。以下是一个最终合并后的数据示例片段[ { title: 如何高效学习 Node.js, link: https://mp.weixin.qq.com/s/abc123..., digest: 本文分享了五个提升 Node.js 学习效率的实用技巧..., publish_time: 1640995200, publish_date: 2022-01-01, cover: http://mmbiz.qpic.cn/..., author: 技术老张, copyright_stat: 11, detail: { content_html: div...p正文HTML内容.../p.../div, content_text: 正文纯文本内容..., crawl_time: 2023-10-27T08:00:00.000Z } } ]有了这样的数据你可以轻松地进行内容分析使用 Python 的pandas、jieba、sklearn等库对文章标题和正文进行词频统计、主题建模、情感分析。构建本地搜索引擎使用Elasticsearch或MiniSearch索引文章实现快速全文检索。生成内容摘要或周刊定期将爬取的新文章自动汇总生成摘要邮件或文档。数据备份为你珍视的公众号内容提供一个本地备份防止原文被删除。最后我想强调的是这个工具的生命周期与微信客户端的接口紧密绑定。今天有效的方法明天可能就会失效。因此理解整个工具的原理——如何抓包、如何分析请求、如何模拟——比单纯运行脚本更重要。当接口变化时你能够自己动手去调整代码这才是真正的“可控”。在整个开发过程中耐心和细致的观察是你最好的伙伴。希望这份详细的拆解能帮你顺利搭建起自己的公众号内容库。如果在实际操作中遇到新的问题不妨回到抓包这一步重新审视数据流往往就能找到答案。本文还有配套的精品资源点击获取
返回列表