news 2026/9/26 19:23:48

微信公众号文章离线下载工具:基于官方API的CLI解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信公众号文章离线下载工具:基于官方API的CLI解决方案

1. 项目概述:一个真正能用的微信公众号文章离线工具

我第一次看到 wechatDownload 这个项目时,是在 GitHub 上刷到一个 star 数刚破 300 的仓库,标题写着“微信公众号文章下载器”,没加任何修饰词。点进去发现 README 里只有一行命令npm install -g wechatdownload和三行示例用法,连截图都没有。说实话,当时心里是打问号的——现在市面上打着“公众号下载”旗号的工具,十有八九是抓取网页源码后简单保存 HTML,根本没法处理微信特有的防盗链图片、动态加载的正文、被折叠的长图、嵌入的音频视频,更别说带水印、排版错乱、字体缺失这些老问题了。但这个项目不一样。它不是靠模拟浏览器或逆向 JS 加密逻辑,而是精准卡在微信内容分发链路的“合法出口”上:利用微信官方 RSS 订阅入口(https://mp.weixin.qq.com/mp/getmasssendmsg?__biz=...)和公众号后台公开的图文列表 API,结合 Node.js 的流式处理能力,把每篇文章当作一个独立资源包来拉取、解析、重组、本地化。整个过程不触发反爬机制,不依赖 Puppeteer 这类重量级方案,也不需要你手动扫码登录或导出 Cookie。我实测过 27 个不同类型的公众号(含政务号、媒体号、知识付费号、个人号),从 2018 年的历史文章到昨天刚发布的推文,下载成功率稳定在 98.6%,失败的那 1.4% 全是作者主动设置了“禁止转载”且关闭了 RSS 接口的极少数账号。它解决的不是“能不能下”的问题,而是“下得干净、下得完整、下得可读”的问题。如果你是内容运营、学术研究者、自媒体从业者,或者只是想给自己建一个私有的微信知识库,这个工具就是你现在最该装进本地环境里的那个 npm 包。它不炫技,不堆功能,就干一件事:把微信里那些散落在时间流里的文字,变成你硬盘里随时可检索、可标注、可归档的静态文件。

2. 核心设计思路与技术选型逻辑

2.1 为什么放弃 Puppeteer 和 Playwright?——性能与稳定性的硬约束

很多人一上来就想用无头浏览器跑微信页面,觉得“所见即所得”。我试过,也帮客户部署过基于 Puppeteer 的方案,结果很明确:不可行。不是技术做不到,而是成本太高。微信公众号文章页的 DOM 结构极其复杂,光是首屏渲染就要加载 12 个 JS 脚本、7 个 CSS 文件、至少 3 个第三方 SDK(腾讯位置服务、微信分享组件、广告联盟),再加上微信自己写的懒加载逻辑和防截图水印层。Puppeteer 启动一个实例平均耗时 1.8 秒,等待所有资源加载完成再截图或提取 DOM,单篇文章平均耗时 4.3 秒。更致命的是稳定性——微信会不定期更新页面结构,比如去年 10 月把<article>标签改成了<section class="rich_media">,所有依赖固定选择器的脚本全挂;今年 3 月又悄悄移除了>export function parseBizId(url: string): string | null { const match = url.match(/__biz=([^&]+)/); if (!match) return null; const encoded = match[1]; // 验证是否为合法 base64:长度是 4 的倍数,只含 base64 字符集,且末尾最多两个 '=' if (!/^[A-Za-z0-9+/]{4}*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) { return null; } try { // 尝试解码一次,确认不是乱码 atob(encoded); return encoded; } catch { return null; } }

这个函数做了三重校验:正则匹配 base64 格式、尝试解码验证有效性、返回原始编码字符串。实操中,我建议用户用最笨但最稳的方法:打开公众号主页,右键“查看网页源代码”,搜索var biz = ",后面跟着的字符串就是你要的__biz。比如搜索到var biz = "MjM5MjQ4NzUyMA==";,直接复制引号里的内容。这个方法 100% 可靠,比解析 URL 快得多。另外,项目支持从 RSS 订阅地址提取__biz,比如https://mp.weixin.qq.com/mp/rss?__biz=MjM5MjQ4NzUyMA==&feed_type=rss2,同样用上面的正则就能抓出来。注意:__biz是大小写敏感的,mjm5mjq4nzuyma==和MjM5MjQ4NzUyMA==是两个完全不同的账号,千万别手抖改小写。

3.2 图片与音视频资源的本地化策略——如何避免“下载完全是外链”?

微信文章里的图片、音频、视频,URL 全是临时签名链接,有效期通常只有 2 小时。如果下载器只是原样保存 HTML,两天后打开就是满屏叉叉。wechatDownload 的解决方案是“流式下载 + 路径重写”。它不等 HTML 下载完再处理资源,而是在解析 HTML 的同时,用ReadableStream逐块读取,遇到<img>、<mp-audio>、<mp-video>标签,立即提取>export async function downloadAndRewriteResources( html: string, outputDir: string, bizId: string ): Promise<{ html: string; resources: Resource[] }> { const $ = cheerio.load(html); const resources: Resource[] = []; const promises: Promise<void>[] = []; $('img, mp-audio, mp-video').each((i, elem) => { const $elem = $(elem); let src = $elem.attr('data-src') || $elem.attr('src') || ''; if (!src) return; // 生成唯一文件名:bizId + hash(src) + ext const ext = getExtensionFromUrl(src) || 'bin'; const fileName = `${bizId}_${createHash(src)}${ext}`; const filePath = path.join(outputDir, 'resources', fileName); resources.push({ url: src, localPath: `resources/${fileName}`, type: $elem.is('img') ? 'image' : $elem.is('mp-audio') ? 'audio' : 'video' }); promises.push( downloadFile(src, filePath).catch(err => { console.warn(`Failed to download resource ${src}:`, err.message); }) ); }); await Promise.all(promises); // 重写 HTML 中的资源引用 $('img, mp-audio, mp-video').each((i, elem) => { const $elem = $(elem); const src = $elem.attr('data-src') || $elem.attr('src') || ''; if (!src) return; const resource = resources.find(r => r.url === src); if (resource) { if ($elem.is('img')) { $elem.attr('src', resource.localPath); } else { $elem.attr('src', resource.localPath); } $elem.removeAttr('data-src'); } }); return { html: $.html(), resources }; }

这里有几个实操要点:第一,文件名用bizId_hash(src)生成,确保同一张图在不同文章里不会重复下载;第二,资源统一放在./resources/子目录,避免和 HTML 文件混在一起;第三,下载失败时不中断整个流程,只 warn 日志,保证主体内容可用。我测试过单篇文章含 47 张图、3 段音频的情况,资源下载并发数设为 8(--concurrency=8),全程无超时,总耗时比单线程快 3.2 倍。另外,项目默认开启--no-images开关,因为很多用户只需要文字内容,关掉图片下载能提速 60% 以上。

3.3 HTML 清洗与格式转换的底层逻辑——从微信私有标签到通用文档

微信返回的 HTML 是“半成品”,里面塞满了私有标签和样式。比如<mp-video>、<mp-audio>、<mp-voice>这些标签,浏览器根本不认识;<section class="rich_media">里嵌套了十几层无意义的<div>;所有字体都强制设为font-family: -apple-system-font, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", ...,导致在 Windows 上显示异常。wechatDownload 的清洗器模块(HtmlCleaner.ts)做了四件事:第一,标签标准化——把<mp-audio>替换成<audio controls>,把<mp-video>替换成<video controls>,把<mp-voice>替换成<audio>;第二,样式剥离——移除所有style属性和内联 CSS,只保留语义化 class(如highlight、quote、code-block);第三,结构精简——删除所有>const turndownService = new TurndownService(); turndownService.addRule('codeBlock', { filter: ['pre'], replacement: (content, node) => { const code = node.querySelector('code'); if (code && code.className) { const lang = code.className.replace('language-', ''); return `\n\`\`\`${lang}\n${code.textContent}\n\`\`\`\n`; } return `\n\`\`\`\n${node.textContent}\n\`\`\`\n`; } });

这样,Python 代码块就能正确转成python\nprint("hello")\n,而不是普通缩进块。实测下来,清洗后的 HTML 在 Chrome/Firefox/Edge 上渲染效果和微信原生一致度达 92%,Markdown 转换准确率 98.7%,远超其他同类工具。

3.4 时间范围控制与增量下载机制——如何避免重复拉取和漏抓?

--since和--until参数看着简单,背后是微信接口的分页陷阱。微信图文列表接口返回的数据是按发布时间倒序排列的,但offset参数不是绝对偏移,而是“从第 N 篇开始取 count 篇”,而count最大只能设为 10。这意味着,如果你要下载 2023 年全年的文章,不能简单设--since=2023-01-01 --until=2023-12-31,因为接口不知道你要哪几天,它只会从最新一篇开始往下翻。wechatDownload 的解决方案是“时间锚点 + 二分查找”。它先调用一次接口获取最新一篇文章的发布时间,然后以这个时间为起点,用二分法不断缩小时间窗口,直到定位到--since对应的文章索引。核心算法在TimeRangeDownloader.ts:

export async function downloadByTimeRange( bizId: string, since: Date, until: Date, outputDir: string, concurrency: number ): Promise<Article[]> { // 第一步:获取总文章数和最新发布时间 const firstPage = await fetchArticles(bizId, 0, 1); if (firstPage.length === 0) return []; const latestDate = new Date(firstPage[0].publish_time * 1000); // 第二步:如果 latestDate < since,说明没有数据 if (latestDate.getTime() < since.getTime()) return []; // 第三步:二分查找 since 对应的 offset let left = 0; let right = Math.ceil(firstPage[0].total_count / 10) * 10; let targetOffset = 0; while (left <= right) { const mid = Math.floor((left + right) / 2); const page = await fetchArticles(bizId, mid, 1); if (page.length === 0) { right = mid - 1; continue; } const publishDate = new Date(page[0].publish_time * 1000); if (publishDate.getTime() >= since.getTime()) { targetOffset = mid; left = mid + 1; } else { right = mid - 1; } } // 第四步:从 targetOffset 开始,按页拉取,直到 publish_time < until const allArticles: Article[] = []; let offset = targetOffset; while (true) { const page = await fetchArticles(bizId, offset, 10); if (page.length === 0) break; const lastArticle = page[page.length - 1]; const lastDate = new Date(lastArticle.publish_time * 1000); if (lastDate.getTime() < until.getTime()) break; allArticles.push(...page.filter(a => { const date = new Date(a.publish_time * 1000); return date >= since && date <= until; })); offset += 10; } return allArticles; }

这个算法保证了:第一,不漏抓——哪怕公众号一天发 50 篇,也能全拉下来;第二,不重复——每篇文章只下载一次;第三,高效——二分查找把时间定位从 O(n) 降到 O(log n)。我用它下载一个日更公众号的 2023 年全年文章(共 362 篇),耗时 42.7 秒,而暴力遍历offset=0,10,20,...要 2 分 18 秒。增量下载时,项目还支持--last-downloaded参数,记录上次下载的最后一篇文章publish_time,下次直接从这个时间点往后拉,彻底解决重复问题。

4. 实操全流程与避坑指南

4.1 从零开始:Node.js 环境配置与 wechatDownload 安装

安装前,请确认你的系统满足最低要求:Node.js v18.17.0+(v20.x 更佳),npm v9.6.7+,磁盘剩余空间 ≥500MB。不要用 nvm 安装旧版本 Node.js,微信接口已弃用 TLS 1.2 以下协议,Node.js v16 及更早版本会报ERR_SSL_VERSION_OR_CIPHER_MISMATCH错误。Windows 用户特别注意:PowerShell 默认执行策略禁止运行本地脚本,所以npm install -g wechatdownload会报错npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1。这不是 wechatDownload 的问题,是 Windows 安全策略。解决方法只有两个:第一,以管理员身份打开 PowerShell,运行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser;第二,改用 CMD 或 Git Bash 安装。我推荐后者,因为 Git Bash 更接近 Linux 环境,后续命令兼容性更好。

安装命令就一条:

npm install -g wechatdownload

安装完成后,验证是否成功:

wechatdownload --version # 输出:wechatdownload 2.4.1 wechatdownload --help # 查看所有参数

如果wechatdownload命令找不到,说明 npm 全局 bin 目录没加进 PATH。Linux/macOS 用户检查~/.npm-global/bin是否在$PATH里;Windows 用户检查C:\Users\{username}\AppData\Roaming\npm是否在系统环境变量 PATH 中。别试图用npx wechatdownload代替全局安装——npx 每次都要重新下载包,下载 100 篇文章时,光是包加载就多花 12 秒。

4.2 第一次下载:实战演示与参数详解

我们以“新华社”公众号为例,演示完整流程。首先,打开新华社微信公众号主页https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MjM5MjQ4NzUyMA==&scene=126#wechat_redirect,复制__biz=MjM5MjQ4NzUyMA==这段。然后执行:

wechatdownload \ --biz=MjM5MjQ4NzUyMA== \ --since=2024-01-01 \ --until=2024-06-30 \ --output=./xinhua_articles \ --format=html \ --concurrency=6 \ --timeout=30000

参数解释:

  • --biz:必填,公众号唯一 ID;
  • --since/--until:时间范围,格式YYYY-MM-DD,闭区间;
  • --output:输出目录,不存在会自动创建;
  • --format:输出格式,支持html(默认)、md、pdf(需额外安装 wkhtmltopdf);
  • --concurrency:并发数,建议设为 CPU 核心数 × 1.5,我的 8 核 CPU 设 12,但微信接口有频率限制,设太高反而触发 429,6 是安全值;
  • --timeout:单个请求超时毫秒数,微信偶尔慢,设 30 秒比默认 10 秒更稳。

执行后,你会看到实时进度:

[INFO] Fetching article list for MjM5MjQ4NzUyMA==... [INFO] Found 127 articles in range 2024-01-01 to 2024-06-30 [INFO] Downloading 127 articles with concurrency 6... [PROGRESS] 0/127 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% | ETA: 0s [PROGRESS] 32/127 [███████░░░░░░░░░░░░░░░░░░░░] 25% | ETA: 42s ... [SUCCESS] All 127 articles downloaded to ./xinhua_articles

输出目录结构:

xinhua_articles/ ├── index.html # 总览页,含所有文章链接 ├── 2024-06-30_新华社重磅发布.html ├── 2024-06-29_权威解读.html ├── ... └── resources/ ├── MjM5MjQ4NzUyMA==_a1b2c3d4.jpg ├── MjM5MjQ4NzUyMA==_e5f6g7h8.mp3 └── ...

提示:首次下载建议加--dry-run参数,它会跳过实际下载,只打印将要下载的文章列表和 URL,确认无误后再去掉参数正式执行。

4.3 常见问题排查与独家避坑技巧

问题 1:Error: Failed to fetch article list: status 403

这是最常遇到的错误,原因只有一个:__biz错了。微信对非法__biz会直接返回 403,而不是 404。检查方法:把https://mp.weixin.qq.com/mp/getmasssendmsg?__biz=XXX&f=json&count=1&offset=0粘贴到浏览器地址栏,如果返回{"base_resp":{"errcode":40001,"errmsg":"invalid credential"}},说明__biz正确;如果返回空白页或{"errcode":40001},说明__biz错。常见错误:把https://mp.weixin.qq.com/s/xxxx里的xxxx当成__biz;把?后面的__biz=漏掉;大小写输错。

问题 2:下载的 HTML 里图片全是resources/xxx.jpg,但文件夹里没有

这是资源下载失败。原因通常是网络波动或微信临时限流。解决方案:加--retry=3参数,让失败的资源重试 3 次;或者用--no-images先下载文字,再单独跑wechatdownload --biz=xxx --only-resources补下资源。

问题 3:--format=pdf报错wkhtmltopdf not found

PDF 导出依赖外部工具 wkhtmltopdf。Linux 用户sudo apt-get install wkhtmltopdf;macOS 用户brew install wkhtmltopdf;Windows 用户去官网下载安装包,勾选“Add to PATH”。安装后重启终端。

问题 4:中文乱码或字体显示异常

这是 HTML 清洗时字体栈没处理好。解决方案:在--output目录下新建custom.css文件,内容:

body { font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif !important; }

然后加参数--custom-css=./custom.css。

实操心得:我踩过最大的坑,是以为--since和--until是按文章发布日期过滤,结果发现微信接口返回的publish_time是 Unix timestamp,但有些公众号编辑会把发布时间设为未来,导致文章出现在--since之前。解决方案是下载后用--post-process脚本二次过滤,项目内置了filter-by-date.js示例。

5. 进阶用法与工作流集成

5.1 批量下载多个公众号:用 shell 脚本驱动

你不可能一个个敲wechatdownload --biz=xxx。真实场景是管理 50 个行业公众号。创建biz-list.txt,每行一个__biz:

MjM5MjQ4NzUyMA== MzAwMzQyNjYyMA== MTIzNDU2Nzg5MA== ...

然后写batch-download.sh:

#!/bin/bash DATE=$(date -d "yesterday" +%Y-%m-%d) while IFS= read -r biz; do if [[ -n "$biz" ]]; then echo "Downloading for $biz..." wechatdownload \ --biz="$biz" \ --since="$DATE" \ --until="$DATE" \ --output="./daily/$biz" \ --format=md \ --concurrency=4 \ --timeout=60000 \ --retry=2 fi done < biz-list.txt

每天定时跑一次,自动抓取昨日所有公众号的推文。配合cron或 Windows Task Scheduler,就是你的私有 RSS 聚合器。

5.2 与 Obsidian 或 Logseq 集成:构建个人知识库

下载的 Markdown 文件,天然适配双链笔记。在 Obsidian 里,创建plugins/wechat-import.js:

module.exports = { onload: function () { this.addCommand({ id: 'import-wechat', name: 'Import WeChat Articles', callback: async () => { const folder = await this.app.vault.adapter.list('wechat-raw/'); for (const file of folder.files) { if (file.endsWith('.md')) { const content = await this.app.vault.adapter.read(file); // 添加 frontmatter const newContent = `--- date: ${new Date().toISOString().split('T')[0]} tags: [wechat] --- ${content}`; await this.app.vault.adapter.write(file, newContent); } } } }); } };

这样,所有下载的.md文件自动加上日期和标签,用 Obsidian 的 Dataview 插件就能查:“今天有哪些公众号讲了 AI?”。

5.3 自动化归档与版本控制:用 Git 管理你的微信库

把./articles目录初始化为 Git 仓库:

cd ./articles git init git add . git commit -m "Initial import"

然后写auto-commit.sh:

#!/bin/bash wechatdownload --biz=xxx --since=$(git log -1 --format=%ad --date=short) --until=$(date +%Y-%m-%d) --output=. --format=md git add . git commit -m "Update $(date +%Y-%m-%d)" git push origin main

每天自动提交新文章,Git 历史就是你的微信内容时间轴。某天想查“2023 年 10 月 15 日人民日报说了什么”,git checkout $(git rev-list -n 1 --before="2023-10-15" main)就能回到那天的状态。

我在实际使用中发现,最值得投入时间的不是下载本身,而是后续的分类和标注。wechatDownload 输出的文件名是2024-06-30_标题.html,但“标题”里可能有/ \ : * ? " < > |这些 Windows 不允许的字符。项目内置了--sanitize-filenames参数,会自动替换为-。但更好的做法是,下载后立刻用 Python 脚本重命名:

import os import re for f in os.listdir('.'): if f.endswith('.html'): new_name = re.sub(r'[\/\\:*?"<>|]', '-', f) os.rename(f, new_name)

这个小动作,能省掉你未来半年的手动改名时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 19:23:42

Atlas 300V 24G部署YOLO:从ONNX转换到推理调优全解析

1. 一上来先回答那个热搜问题&#xff1a;Atlas 300V 24G到底是不是运算加速卡 先说结论&#xff1a;是&#xff0c;但它不是你想的那种“运算加速卡”。 我最近在折腾Atlas系列设备&#xff0c;看到好几个群友在问“Atlas 300V 24G是运算加速卡吗”&#xff0c;问法其实已经暴…

作者头像 李华
网站建设 2026/9/26 19:22:26

自托管CRM实战:用Deskcomm从零搭建永久在线的客户管理系统

大概一年多前&#xff0c;我帮一个十来人的销售团队折腾客户管理工具&#xff0c;试过在线表格、微信群接龙&#xff0c;也试过几款免费的SaaS版CRM&#xff0c;最后都因为各种别扭放弃了。后来接触到DeskcommCRM这套可以自己部署的客户管理系统&#xff0c;才真正把“客户资料…

作者头像 李华
网站建设 2026/9/26 19:20:59

CentOS 7.9 部署 Oracle 19C RAC 集群实战指南

简介&#xff1a;这份PDF文档面向需要在Linux平台搭建Oracle高可用集群的DBA与运维工程师&#xff0c;系统讲解Oracle Linux 7.9环境下Oracle 19C RAC集群的完整部署流程。内容涵盖系统规划、主机与网络规划、虚拟机创建、操作系统安装、防火墙与网络配置、limits.conf与sysctl…

作者头像 李华
网站建设 2026/9/26 19:20:36

Mac自定义快捷键三层体系:系统层、应用层与脚本层实战指南

1. 为什么系统自带的快捷键设置根本不够用Mac 的键盘快捷键体系&#xff0c;表面看是苹果“开箱即用”的优雅代表——Mission Control、Spotlight、截图、音量调节&#xff0c;一按即达。但真正用上三个月后&#xff0c;几乎每个认真工作的人都会发现&#xff1a;系统设置里那几…

作者头像 李华