wechat-article-exporter Documentation

一款在线的 微信公众号文章批量下载 工具,支持导出阅读量与评论数据,无需搭建任何环境,可通过 在线网站 使用,支持 docker 私有化部署和 Cloudflare 部署。 支持下载各种文件格式,其中 HTML 格式可100%还原文章排版与样式。

13,082 stars TypeScript TypeScript 23 files · ~8,798 tokens #download#wechat#wechat-article#wechat-download
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy wechat-article-exporter on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name wechat-article-exporter -p 3000:3000 --restart always wechat-article/wechat-article-exporter:latest

Technical Documentation: wechat-article/wechat-article-exporter

ℹ️ Provenance: Hybrid Fusion: wechat-article/wechat-article-exporter + wechat-article/docs · CodeWiki Reference · Recency: Active (< 180 days)

1. Project Overview & Quickstart (wechat-article/wechat-article-exporter)

wechat-article-exporter

GitHub stars
GitHub forks
GitHub License
Package Version

[!WARNING]
本项目已停止维护(2026-07-30)。

项目所依赖的微信上游核心接口已被官方关闭,且大概率不会再开放。这是整个项目赖以工作的地基,无法通过修改代码绕过 —— 开箱即用的一键同步公众号文章已不复存在。完整说明见 关于本项目停止维护的说明 #200。

  • 仓库不会删除,将转为只读归档。代码、文档与 issue 讨论均留在原处,任何人都可以继续阅读、fork、学习和二次开发。
  • 不再接受新的 issue 和 PR,也不会再有新版本,请不要等待「明天可能修好」。
  • 在线网站的域名将于 2026-10-30 到期,届时将无法访问,请在此之前导出你需要保留的数据。
  • 你已经同步过的文章都还是你的,它们存放在你自己的设备上,不受任何接口影响。

一款在线的 微信公众号文章批量下载 工具,支持导出阅读量与评论数据,无需搭建任何环境,可通过 在线网站 使用(域名 2026-10-30 到期,届时将无法访问),同时也支持 docker 私有化部署和 Cloudflare 部署。

支持下载各种文件格式,其中 HTML 格式可100%还原文章排版与样式。

:books: 如何使用?

该工具的使用教程已移至 文档站点。

:dart: 特性

  • 搜索公众号,支持关键字搜索
  • 支持导出 html/json/excel/txt/md/docx 格式(html 格式打包了图片和样式文件,能够保证100%还原文章样式)
  • 缓存文章列表数据,减少接口请求次数
  • 支持文章过滤,包括作者、标题、发布时间、原创标识、所属合集等
  • 支持合集下载
  • 支持图片分享消息
  • 支持视频分享消息
  • 支持导出评论、评论回复、阅读量、转发量等数据 (需要抓包获取 credentials 信息,查看操作步骤)
  • 支持 Docker 部署
  • 支持 Cloudflare 部署
  • 开放 API 接口(公开 API 已下线,不再对外提供服务)

「取阅」

同一作者的另一个项目 —— 取阅。

实时监听并推送公众号群发文章。

:heart: 感谢

:star: 支持

如果你觉得本项目帮助到了你,请给作者一个免费的 Star,感谢你的支持!

:bulb: 原理

在公众号后台写文章时支持搜索其他公众号的文章功能,以此来实现抓取指定公众号所有文章的目的。

:memo: 许可

MIT

:red_circle: 声明

本程序承诺,不会利用您扫码登录的公众号进行任何形式的私有爬虫,也就是说不存在把你的账号作为公共账号为别人爬取文章的行为,也不存在类似账号池的东西。

您的公众号只会服务于您自己的抓取文章的目的。

通过本程序获取的公众号文章内容,版权归文章原作者所有,请合理使用。若发现侵权行为,请联系我们处理。

:chart_with_upwards_trend: Star 历史

Star History Chart

2. Official Technical Reference & Guides (wechat-article/docs)

File: README.md

docs

微信公众号文章系列工具文档


File: CLAUDE.md

CLAUDE.md

项目概述

这是 微信公众号文章下载工具 的文档站点,基于 VitePress 构建。
仓库地址:wechat-article/docs,主工具仓库:wechat-article/wechat-article-exporter。

该工具是一款开源免费的在线工具,支持批量下载微信公众号文章,导出为 Excel/JSON/HTML/TXT/Markdown/Word 等格式。

技术栈

  • 静态站点生成器:VitePress (v1.6.3+)
  • 前端框架:Vue 3
  • 语言:中文 (zh-CN)
  • 插件:
    • vitepress-plugin-lightbox - 图片灯箱效果
    • vitepress-plugin-llms - LLM 友好的 llms.txt 生成
    • medium-zoom - 图片缩放
    • giscus - 基于 GitHub Discussions 的评论系统

项目结构

text
docs/
├── .vitepress/
│   ├── config.ts          # VitePress 配置文件(导航、侧边栏、主题等)
│   ├── theme/
│   │   ├── Layout.vue     # 自定义布局(暗色模式切换动画 + medium-zoom)
│   │   ├── Comments.vue   # Giscus 评论组件
│   │   └── style.css      # 自定义主题样式
│   └── dist/              # 构建产物(已 gitignore)
├── src/                   # 文档源文件目录(VitePress srcDir)
│   ├── index.md           # 首页
│   ├── faq.md             # 常见问题
│   ├── get-started/       # 入门指南
│   ├── advanced/          # 进阶使用
│   ├── tutorials/         # 教程
│   ├── misc/              # 杂项(架构、域名、Features 等)
│   ├── others/            # 其他技术文档
│   ├── assets/            # 图片等静态资源
│   └── public/            # 公共静态文件(favicon、logo)
├── app/                   # Docker 部署相关文件(已 gitignore)
├── package.json
└── CLAUDE.md

常用命令

bash
# 启动开发服务器
npm run dev

# 构建生产版本
npm run build

# 预览构建产物
npm run preview

代码规范

  • TypeScript 零容忍:遇到任何 TS 错误必须立即修复,无论是否由当前改动引入。不允许使用 @ts-ignore 或 @ts-expect-error 来绕过,除非第三方库确实缺少类型声明
  • 所有 .ts 和 .vue 文件必须通过 TypeScript 类型检查

编写规范

  • 所有文档内容使用 中文 撰写
  • Markdown 图片使用 `` 相对路径引用
  • 图片支持灯箱效果(通过 lightbox 插件自动处理)
  • 代码块启用行号显示
  • 图片启用懒加载
  • 页面默认启用 Giscus 评论,可通过 frontmatter comments: false 关闭
  • VitePress 的 srcDir 设置为 src,所有文档页面在 src/ 目录下

Git 规范

  • 主分支:master
  • 提交信息使用简短描述(参考历史提交风格如 "update")

File: package.json

{
"name": "docs",
"version": "1.0.1",
"repository": "[email protected]:wechat-article/docs.git",
"author": "jock",
"license": "UNLICENSED",
"packageManager": "[email protected]",
"type": "module",
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview"
},
"devDependencies": {
"medium-zoom": "^1.1.0",
"vitepress": "^1.6.4",
"vitepress-plugin-lightbox": "^1.0.3",
"vitepress-plugin-llms": "^1.12.0",
"vue": "^3.5.32"
}
}


File: tsconfig.json

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/": ["./src/"]
}
},
"include": [
"src//*.ts",
"src/
/.vue",
".vitepress/**/
.ts",
".vitepress/**/*.vue"
],
"exclude": [
"node_modules",
".vitepress/dist",
".vitepress/cache"
]
}


File: src/faq.md

常见问题 (FAQ)

[[toc]]

出现 `200002:invalid args` 如何解决?

该错误通常由以下原因导致,请逐一排查:

1. 登录账号类型不正确

扫码登录时必须选择 公众号 或 服务号,不能使用 小程序 登录:

2. 目标公众号未开启搜索

本工具基于公众号后台的文章搜索接口获取数据,目标公众号必须开启「允许通过名称搜索」选项,否则无法搜索到该公众号:

3. 登录已过期

登录凭证(auth-key)有效期为 4 天,过期后需要重新扫码登录。

登录页面为什么不显示二维码?

这通常出现在私有部署的网站上。微信返回的登录 Cookie 带有 Secure 属性,要求网站必须通过 HTTPS 访问才能正常携带 Cookie。

解决方案:

如何获取评论和阅读量数据?

阅读量、点赞数、转发量和评论数据需要通过微信用户身份才能获取,本工具将这些身份信息统称为 Credentials。

获取流程:

  1. 使用 wxdown-service 程序启动本地代理
  2. 在微信客户端中打开目标公众号的一篇文章
  3. 程序自动抓取 Credentials 并同步到网站

::: warning 注意
Credentials 有效期约 25 分钟,过期后需要在微信中重新打开文章页面以刷新。
:::

详细操作请查看 抓取 Credentials。

已加载消息数和消息总数为什么不一致?

首先需要理解 消息 和 文章 的区别:

如上图所示,红框中的是 2 条消息,但每条消息可能包含多篇文章(类似于一期推送中的多篇图文)。

  • 已加载消息数:实际从微信接口拉取到的消息数量,该数值是准确的
  • 消息总数:微信接口返回的 total_count 字段,作为参考值,不一定完全准确

文章下载页面的文章数量为什么不一样?

这是因为默认启用了「隐藏已删除文章」选项。已删除的文章在同步时仍会被获取到,但在文章列表中默认隐藏。

关闭该选项后,两个数字会保持一致:

请求出现 `429` 状态码

429 表示请求过于频繁,通常是公共代理节点的当日额度用完了。

解决方案:

  • 搭建自己的 私有代理节点(推荐)
  • 等待次日北京时间 8:00 额度自动刷新

参考 GitHub Issue #119

目标公众号 Credential 未设置

该提示出现在抓取阅读量或评论数据时,表示对应公众号的 Credential 尚未获取或已过期。

请参考 抓取 Credential 教程获取 Credential 后再进行抓取。

无法打开此文件夹,因为其中含有系统文件

导出时不能选择系统保护的目录(如「下载」、「文档」等系统目录)。请手动创建一个新目录(如桌面上新建一个「导出文件」文件夹),然后选择该目录进行导出。

如何清理数据

工具中的文章数据(除单篇文章下载外)都存储在浏览器本地的 IndexedDB 数据库中,有以下几种清理方式:

方法 1:删除指定公众号数据

在【公众号管理】页面,选择对应的公众号,点击【删除】按钮即可清除该公众号的所有缓存数据。

方法 2:通过开发者工具清除全部数据

适用于需要清除所有公众号数据的情况:

  1. 按 F12 或右键页面选择「检查」,打开浏览器开发者工具
  2. 切换到「Application」(应用)标签页
  3. 在左侧展开「IndexedDB」
  4. 找到网站对应的数据库,点击【删除数据库】

方法 3:通过浏览器设置清除站点数据

此方法会清除该网站的所有本地数据(包括 IndexedDB、Cookie、登录状态等):

  1. 在 Chrome 地址栏输入 chrome://settings/content/all?searchSubpage=mptext.top 并回车
  2. 找到 down.mptext.top 域名
  3. 点击右侧的垃圾桶图标删除该站点的所有数据

如何修改 Chrome 浏览器 IndexedDB 数据库的存储位置?

由于本工具的所有公众号数据都存储在浏览器的 IndexedDB 中,当数据量较大时,可能会占用系统盘较多空间。可以通过以下两种方式修改 Chrome 的存储位置,将数据迁移到其他磁盘。

::: warning 注意
修改前请先关闭所有 Chrome 进程,并对原有数据做好备份,避免数据丢失。
:::

方法 1:使用 `--user-data-dir` 启动参数(推荐)

通过命令行参数指定一个新的用户数据目录,Chrome 会将所有用户数据(包括 IndexedDB)保存到该目录。

Windows:

右键 Chrome 快捷方式 → 属性 → 在「目标」末尾追加参数:

text
"C:\Program Files\Google\Chrome\Application\chrome.exe" --user-data-dir="D:\ChromeData"

macOS:

通过终端启动:

bash
open -a "Google Chrome" --args --user-data-dir="/Volumes/Data/ChromeData"

Linux:

bash
google-chrome --user-data-dir="/home/user/ChromeData"

::: tip 提示
该方法相当于创建了一个全新的 Chrome 配置文件,原有的书签、扩展、登录信息等都不会同步过来。如需保留原数据,可将原 User Data 目录的内容复制到新目录。
:::

方法 2:使用符号链接(保留原配置)

将 IndexedDB 文件夹移动到其他磁盘,再通过符号链接指回原路径,这样 Chrome 仍然按原路径访问,但实际数据存储在新位置。

Chrome 的 IndexedDB 默认路径:

  • Windows:C:\Users\<用户名>\AppData\Local\Google\Chrome\User Data\Default\IndexedDB
  • macOS:~/Library/Application Support/Google/Chrome/Default/IndexedDB
  • Linux:~/.config/google-chrome/Default/IndexedDB

Windows(管理员身份运行 CMD):

cmd
:: 1. 将原目录移动到新位置
move "C:\Users\<用户名>\AppData\Local\Google\Chrome\User Data\Default\IndexedDB" "D:\ChromeIndexedDB"

:: 2. 创建符号链接
mklink /D "C:\Users\<用户名>\AppData\Local\Google\Chrome\User Data\Default\IndexedDB" "D:\ChromeIndexedDB"

macOS / Linux:

bash
# 1. 将原目录移动到新位置
mv ~/Library/Application\ Support/Google/Chrome/Default/IndexedDB /Volumes/Data/ChromeIndexedDB

# 2. 创建符号链接
ln -s /Volumes/Data/ChromeIndexedDB ~/Library/Application\ Support/Google/Chrome/Default/IndexedDB

::: warning 注意

  • 如果使用了多个 Chrome 用户配置(Profile),需要分别处理对应的 Profile 1、Profile 2 等目录
  • 确保新的存储位置所在磁盘始终可用,否则 Chrome 将无法读取数据(外接磁盘需常驻连接)
    :::

如何检查请求失败原因?

当遇到数据获取失败时,可以通过浏览器开发者工具查看具体的网络请求和错误信息:

第一步:打开开发者工具

通过菜单打开:

或者在网页上右键,选择「检查」(Inspect):

第二步:捕获网络请求

  1. 切换到「Network」(网络)面板
  2. 点击清空按钮清除已有记录
  3. 在页面上进行你的操作(如抓取评论)
  4. 操作完成后,查看网络面板中的请求和响应内容

如果不确定如何分析,可以将请求详情截图发到 QQ 群中寻求帮助。

抓取或导出失败

大部分抓取和导出失败都是因为当天的公共代理额度用完了,可以自己检查下网站上面的【公共代理】页面,如果发现某个节点已经到了100%,就需要自己手动在【设置】页面的「代理节点」中删掉相关节点的域名。

可参考 https://github.com/wechat-article/wechat-article-exporter/issues/157


File: src/index.md

[Binary file]


File: src/advanced/api.md

API 使用说明

本工具提供 RESTful API 接口,方便第三方程序集成和自动化处理。

认证方式

所有 API 请求需要在 Header 中携带 X-Auth-Key 进行认证:

text
X-Auth-Key: 你的API密钥

API 密钥在登录成功后自动生成,可在网站的 API 页面查看。密钥有效期与登录会话一致(4 天)。

API 端点

基础地址:https://down.mptext.top(或你的私有部署地址)

搜索公众号

text
GET /api/public/v1/account?keyword=关键词
参数 类型 必填 说明
keyword string 是 公众号名称关键词

获取文章列表

text
GET /api/public/v1/article?fakeid=xxx&begin=0&size=20
参数 类型 必填 说明
fakeid string 是 公众号的 fakeid
begin number 否 起始位置,默认 0
size number 否 每页数量,默认 20(最大 20)
keyword string 否 文章标题搜索关键词

下载文章内容

text
GET /api/public/v1/download?url=文章URL&format=html
参数 类型 必填 说明
url string 是 微信文章 URL
format string 否 输出格式:html(默认)/ markdown / text / json

通过文章 URL 获取公众号信息

text
GET /api/public/v1/accountbyurl?url=文章URL
参数 类型 必填 说明
url string 是 微信文章 URL

验证 API 密钥

text
GET /api/public/v1/authkey

返回 code: 0 表示密钥有效,code: -1 表示已过期。

Postman 在线文档

更详细的 API 文档和示例代码可查看:

https://documenter.getpostman.com/view/1276582/2sB3QDuXUa

在文档页面可选择你的目标编程语言,右侧会自动生成对应的请求代码示例:


File: src/advanced/private-deploy.md

私有部署

::: warning 注意
公共代理节点仅限于以下域名使用:

  • https://down.mptext.top
  • http://localhost
  • http://127.0.0.1

如果私有部署后使用其他域名访问,需要 搭建自己的代理节点。
:::

本地运行

适合开发者调试或临时使用,需要 Node.js >= 22 环境。

  1. 拉取代码
shell
git clone [email protected]:wechat-article/wechat-article-exporter.git
cd wechat-article-exporter
  1. 安装依赖
shell
yarn
  1. 启动开发服务器
shell
yarn dev

启动后打开 http://localhost:3000 即可使用。

Docker 运行(推荐)

适合长期使用,无需搭建开发环境。

拉取镜像

shell
docker pull ghcr.io/wechat-article/wechat-article-exporter:latest

启动容器

容器暴露端口 3000,内部数据目录为 /app/.data。

::: code-group

shell [linux]
docker run -d \
  --restart always \
  --name wechat-article-exporter \
  -e NODE_TLS_REJECT_UNAUTHORIZED=0 \
  -p 3000:3000 \
  -v ./.data:/app/.data \
  ghcr.io/wechat-article/wechat-article-exporter:latest
shell [macos / windows]
docker run -d \
  --restart always \
  --name wechat-article-exporter \
  -e NODE_TLS_REJECT_UNAUTHORIZED=0 \
  -p 3000:3000 \
  -v ./.data:/app/.data \
  ghcr.io/wechat-article/wechat-article-exporter:latest

:::

::: tip 提示
-v ./.data:/app/.data 会将容器内的数据映射到当前目录的 .data 文件夹,确保容器重启后数据不丢失。
:::

启动后打开 http://localhost:3000 即可使用。

docker compose + mkcert 自签名证书

使用`mkcert`生成自签名证书

mkcert的安装及使用可以查看官方文档

shell
# 在系统中安装根证书(只需执行一次)
mkcert -install

执行以下代码生成本地 IP 对应的证书文件:

shell
# 这里可以列出你最终访问的ip
mkcert localhost 127.0.0.1 ::1

该命令会生成2个pem文件,分别对应证书和密钥文件,如下图所示:

将这两个文件重命名为cert.pem和key.pem,并放在certs目录中。

拷贝`nginx.conf`和`docker-compose.yaml`文件

::: code-group

nginx [nginx.conf]
server {
    listen 80;
    server_name localhost;

    # HTTP 自动重定向到 HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name localhost;

    # SSL 证书路径(根据你实际文件名调整)
    ssl_certificate /etc/nginx/certs/cert.pem;
    ssl_certificate_key /etc/nginx/certs/key.pem;

    # 推荐的 SSL 配置(增强安全性)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://app:3000;  # 代理到应用容器
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}
yaml [docker-compose.yml]
services:
  app:
    image: ghcr.io/wechat-article/wechat-article-exporter:latest
    restart: always
    environment:
      - NODE_TLS_REJECT_UNAUTHORIZED=0
    volumes:
      # 持久化 KV 数据(防止容器重启丢失)
      - .data:/app/.data

  nginx:
    image: nginx:alpine
    container_name: wechat-article-nginx
    restart: always
    ports:
      - "80:80"    # HTTP(会自动重定向到 HTTPS)
      - "443:443"  # HTTPS
    volumes:
      - ./certs:/etc/nginx/certs:ro          # 挂载证书(只读)
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro  # 挂载配置
    depends_on:
      - app

:::

将以上所有文件放在一个目录中,比如app目录,最终的目录结构如下:

text
app
├── certs
│   ├── cert.pem
│   └── key.pem
├── docker-compose.yml
└── nginx.conf

在该目录中执行docker compose up -d启动,然后就可以通过https://localhost(或者你的本地ip)访问 https 网站程序了。

Vercel 快速部署

一键部署

点击下方按钮一键部署到 Vercel:

手动部署

  1. Fork 本项目到你的 GitHub 账号

  2. 在 Vercel 中导入项目

    • 访问 Vercel
    • 点击 "Add New..." → "Project"
    • 选择你 Fork 的项目仓库
  3. 配置项目

    • Framework Preset: 选择 Nuxt.js
    • Root Directory: 保持默认 ./
    • Build Command: 保持默认或使用 yarn build
    • Output Directory: 保持默认 .output/public
  4. 配置环境变量(可选)

    • 在项目设置中添加所需的环境变量(见下方环境变量说明)
  5. 点击 "Deploy" 完成部署

环境变量

以下环境变量均为可选项。本地运行时在项目根目录创建 .env 文件即可;Docker 运行时通过 --env-file .env 传入。

数据存储(KV)

登录会话等数据通过 KV 存储持久化,不同部署方式配置不同:

dotenv
# 本地 / Docker 部署(基于文件系统)
NITRO_KV_DRIVER=fs
NITRO_KV_BASE=.data/kv

# Cloudflare 部署(使用 Cloudflare KV)
# NITRO_KV_DRIVER=cloudflare-kv-binding

调试模式

dotenv
# 调试微信代理请求(仅开发环境生效)
NUXT_DEBUG_MP_REQUEST=true

AG-Grid 企业版授权

dotenv
# 文章列表使用 AG-Grid 展示,企业版授权可解锁更多功能
NUXT_AGGRID_LICENSE=

网站统计(umami)

dotenv
# https://umami.nuxt.dev/api/configuration
NUXT_UMAMI_ID=
NUXT_UMAMI_HOST=

错误监控(Sentry)

dotenv
# https://docs.sentry.io/platforms/javascript/guides/nuxt/manual-setup/
NUXT_SENTRY_DSN=
NUXT_SENTRY_ORG=
NUXT_SENTRY_PROJECT=
NUXT_SENTRY_AUTH_TOKEN=

File: src/advanced/wxdown-service.md

抓取 Credential

Credential 是微信用户的一些 cookie 相关信息,同一个微信对不用公众号有不一样的 Credential,所以每个公众号都需要先抓取 Credential 才能抓取阅读量和评论数据。

wxdown-service 是一个独立运行的控制台程序,用于自动抓取微信公众号的 Credential 数据(阅读量、评论等接口所需的认证信息)。该程序内置了 mitmproxy,无需额外安装依赖。

::: tip 提示
本程序代码开源,项目地址:https://github.com/wechat-article/wxdown-service
:::

工作原理

wxdown-service 通过在本地启动一个 HTTP 代理(mitmproxy),拦截微信客户端访问公众号文章时的请求,从中提取 Credential 参数(biz、uin、key、pass_ticket 等),并自动同步到网站。

下载软件

根据你的操作系统下载对应的版本:

下载地址:https://github.com/wechat-article/wxdown-service/releases

平台 文件 说明
Windows wxdown-service.exe 可直接运行,无需安装
macOS 源码 需要 Python 环境运行

Windows 运行

下载并解压后程序如下图所示:

1. 启动软件

双击 wxdown-service.exe 启动,启动过程中可能需要管理员权限和网络访问权限,请全部允许。

启动成功后如下所示:

2. 安装证书

首次运行需要安装 mitmproxy 的 CA 证书,以便解密 HTTPS 请求。

方式一:命令行安装(推荐)

以管理员身份打开 cmd,执行:

cmd
certutil -addstore root %userprofile%\.mitmproxy\mitmproxy-ca-cert.cer

执行结果如下:

方式二:手动安装

如果不熟悉命令行,可以在文件管理器中打开 %userprofile%\.mitmproxy\ 目录,找到 mitmproxy-ca-cert.cer 文件:

双击证书文件,按照向导安装:

选择「本地计算机」:

证书存储位置选择「受信任的根证书颁发机构」:

证书安装完成后,软件界面如下:

3. 设置操作系统代理

将系统代理设置为 127.0.0.1:65000:

设置成功后软件界面如下:

macOS 运行

macOS 暂无打包版本,需要从源码运行。

1. 安装依赖并启动

确保已安装 Python 3,然后执行:

shell
# 克隆项目
git clone https://github.com/wechat-article/wxdown-service.git
cd wxdown-service

# 安装依赖
pip install -r requirements.txt

# 启动程序
python main.py

2. 安装证书

shell
sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem

3. 设置系统代理

打开「系统设置」→「网络」→「Wi-Fi」→「代理」,将 HTTP 和 HTTPS 代理都设置为 127.0.0.1:65000:

设置成功后界面如下:

抓取 Credentials

wxdown-service 启动成功后,网站右上角的图标会变成绿色,表示已连接:

点击图标查看面板:

现在,在 电脑端微信客户端 中打开目标公众号的任意一篇文章:

::: warning 注意
必须在微信内置浏览器中打开文章,不能使用系统浏览器。
:::

如果网站上没有显示该公众号信息,点击微信浏览器左上角的 刷新按钮 重新加载文章页面即可。

抓取成功后,就可以在网站上抓取该公众号的阅读量和评论数据了。

注意事项

事项 说明
有效期 Credentials 有效期约 25 分钟,过期后无法继续抓取阅读量和评论
数据缓存 抓取成功后数据会缓存在浏览器中,此时可以关闭 wxdown-service 程序
刷新方式 过期后只需在微信中重新打开/刷新一篇文章即可自动获取新的 Credentials
持续抓取 如需长时间持续抓取,请保持 wxdown-service 运行且系统代理不要关闭

常见问题

Q: 证书安装后仍然无法抓取?

  • 确认系统代理已正确设置为 127.0.0.1:65000
  • 重启微信客户端后再试
  • Windows 用户确认证书安装到了「受信任的根证书颁发机构」

Q: macOS 提示权限不足?

  • 证书安装命令需要 sudo 权限
  • 如遇安全提示,在「系统设置」→「隐私与安全性」中允许运行

Q: 批量下载过程中 Credentials 过期了怎么办?

  • 已下载的数据不受影响
  • 在微信中重新打开一篇该公众号的文章刷新 Credentials,然后继续抓取即可

File: src/get-started/introduction.md

[Binary file]


File: src/get-started/private-proxy.md

搭建私有代理节点

私有代理节点部署在各种 serverless 环境(都是国外的平台,所以在使用时最好配合翻墙软件),可以加速资源的下载进度。但由于目前节点代码并没有进行身份验证,所以请避免部署后的节点地址遭到泄露。如若发现节点流量异常,请销毁节点重新进行搭建(即更换节点地址)。

如果使用的 Cloudflare 部署的节点,也可以使用自定义规则限制可访问的域名,详情见下面的安全设置。

::: details 点我查看节点代码

js
const UA =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";

const PRESETS = {
    mp: {
        Referer: "https://mp.weixin.qq.com",
    },
};


function error(msg, status = 400) {
    return new Response(msg, {
        status: status,
    });
}


/**
 * 解析请求
 */
async function parseRequest(req) {
    const origin = req.headers.get("origin") || '*';

    // 代理目标的请求参数
    let targetURL = '';
    let targetMethod = "GET";
    let targetBody = '';
    let targetHeaders = {};
    let preset = '';

    const method = req.method.toLowerCase();
    if (method === "get") {
        // GET
        // ?url=${encodeURIComponent(https://example.com?a=b)}&method=GET&headers=${encodeURIComponent(JSON.stringify(headers))}
        const {searchParams} = new URL(req.url);
        if (searchParams.has("url")) {
            targetURL = decodeURIComponent(searchParams.get("url"));
        }
        if (searchParams.has("method")) {
            targetMethod = searchParams.get("method");
        }
        if (searchParams.has("body")) {
            targetBody = decodeURIComponent(searchParams.get("body"));
        }
        if (searchParams.has("headers")) {
            try {
                targetHeaders = JSON.parse(
                    decodeURIComponent(searchParams.get("headers")),
                );
            } catch (_) {
                throw new Error("headers not valid");
            }
        }
        if (searchParams.has("preset")) {
            preset = decodeURIComponent(searchParams.get("preset"));
        }
    } else if (method === "post") {
        // POST
        /**
         * payload(json):
         * {
         *   url: 'https://example.com',
         *   method: 'PUT',
         *   body: 'a=1&b=2',
         *   headers: {
         *     Cookie: 'name=root'
         *   },
         *   preset: '',
         * }
         */
        const payload = await req.json();
        if (payload.url) {
            targetURL = payload.url;
        }
        if (payload.method) {
            targetMethod = payload.method;
        }
        if (payload.body) {
            targetBody = payload.body;
        }
        if (payload.headers) {
            targetHeaders = payload.headers;
        }
        if (payload.preset) {
            preset = payload.preset;
        }
    } else {
        throw new Error("Method not implemented");
    }

    if (!targetURL) {
        throw new Error("URL not found");
    }
    if (!/^https?:\/\//.test(targetURL)) {
        throw new Error("URL not valid");
    }
    if (targetMethod === "GET" && targetBody) {
        throw new Error("GET method can't has body");
    }
    if (Object.prototype.toString.call(targetHeaders) !== "[object Object]") {
        throw new Error("Headers not valid");
    }
    if (!targetHeaders["User-Agent"]) {
        targetHeaders["User-Agent"] = UA;
    }

    // 增加预设
    if (preset in PRESETS) {
        Object.assign(targetHeaders, PRESETS[preset]);
    }

    return {
        origin,
        targetURL,
        targetMethod,
        targetBody,
        targetHeaders,
    };
}

/**
 * 代理请求
 */
function wfetch(url, method, body, headers = {}) {
    return fetch(url, {
        method: method,
        body: body || undefined,
        headers: {
            ...headers,
        },
    });
}

export default {
    async fetch(request) {
        try {
            const {
                origin,
                targetURL,
                targetMethod,
                targetBody,
                targetHeaders,
            } = await parseRequest(request);

            // 代理请求
            const response = await wfetch(
                targetURL,
                targetMethod,
                targetBody,
                targetHeaders,
            );

            return new Response(response.body, {
                headers: {
                    "Access-Control-Allow-Origin": origin,
                    "Access-Control-Max-Age": "86400",
                    "Content-Type": response.headers.get("Content-Type"),
                },
            });
        } catch (err) {
            return error(err.message);
        }
    }
}

:::

目前该节点代码可部署在以下平台:

注意:
Deno Deploy 改版后要求 绑定信用卡 才可使用之前的免费额度,所以 推荐使用 Cloudflare 平台搭建节点。

部署到 Cloudflare Workers

打开控制台的左侧【计算和AI】下面的【Workers 和 Pages】菜单,点击【创建应用程序】,创建一个新的 worker,如下所示:

选择【从 Hello World! 开始】

部署之后,点击【编辑代码】,将节点的代码替换为我们自己的代码(节点代码从本文档上面拷贝):

返回【URL not found】就表示节点部署成功了,我们的节点地址(url)就可以配置到网站中进行使用了。

绑定自定义域名

worker 自动生成的域名(比如上面例子中的https://gentle-firefly-af03.markortese3.workers.dev)通常需要翻墙才能访问,通过配置自定义域名,可缓解 cloudflare 域名被墙的问题。

实测下来,虽然用自定义域名不需要翻墙即可访问,但速度会有一些下降。如果有条件的话,还是开启翻墙软件进行使用。

1. 购买域名

若手里还没有自己的域名,则推荐从 SpaceShip 处购买,支持支付宝支付。
可以选择.site或者.online等后缀的域名,首年价格较低。

2. 将域名添加到 Cloudflare

在【账户主页】点击【加入域】,如下所示:

将域名填入,其他保留默认设置,点【继续】:

选择免费计划:

最后,需要修改 SpaceShip 上面的设置,如下:

打开 SpaceShip 上面你刚购买的那个域名的高级DNS设置页面,如下:

这两处设置对应 Cloudflare 上面要求修改的两处。

这两处设置完之后,需要等待一段时间,等 Cloudflare 更新就可以了。

之后如果想重新启用域名的 DNSSEC,可以在 Cloudflare 中点下面这里,如下所示:

会得到一些配置值,如下:

回到 SpaceShip,在【高级DNS】中添加一条DS记录,如下所示:

【关键标签】对应【密钥标记】

设置完之后等待就可以了。

到此,我们已成功的将我们的域名托管给 Cloudflare 进行管理了。

3. 给 worker 添加自定义域

在 worker 的设置里,添加【自定义域】,如下:

这里可以输入任意的二级域名,比如1235566.space是我刚注册的域名,我可以在前面添加00.表示一条访问节点地址,如下所示:

添加完之后,我就可以通过00.1235566.space访问这个代理节点了,如下:

你可以在同一个 worker 上面添加多个这样的【自定义域】,这样就会得到多个节点地址。
比如,公共代理节点的配置如下:

配置通配符路由(可选)

这样就不需要手动添加二级域名了。

安全设置

针对绑定了自定义域名的账户

如果想限制你的私有节点只能被特定域名使用,可以在 Cloudflare 控制台添加过滤规则,如下所示:

通过限制【引用方】,也就是 HTTP 请求中的referer必须为特定域名才可访问。

上面这个示例配置表示,只有https://www.example.com网站可以使用该节点,其他网站使用时会自动被阻止,一般会返回下面这样的错误:

部署到 Deno Deploy

视频版

文字版

在 Deno Deploy 控制台点击New Playground,创建一个项目,如下:

将上面的节点代码拷贝到左侧代码编辑区,并点击Save & Deploy按钮:

保存成功后,右侧会出现URL not found提示,表示代理搭建完成。

复制右侧地址栏中的地址( https://deep-boa-76.deno.dev ),配置进页面中即可使用。


File: src/get-started/proxy.md

关于代理节点

代理节点的作用

微信公众号文章的图片和内容存在跨域限制和防盗链机制,浏览器无法直接下载这些资源。代理节点充当中间转发层,帮助你绕过这些限制,实现文章内容和图片的正常下载。

系统采用 代理池 机制管理多个代理节点:

  • 自动轮询:按最少失败次数 + 最久未使用的策略选择最优节点
  • 故障转移:单个节点连续失败 5 次后自动进入 10 分钟冷却期
  • 负载均衡:节点越多,并发下载越快

公共代理节点

目前公共代理节点全部部署在 Cloudflare Workers 上,共部署了 6 个账号:

根据实际部署情况,可能会有增删,具体可查看网站后台的【公共代理】页面

text
https://*.worker-proxy.asia
https://*.net-proxy.asia
https://*.1235566.space
https://*.worker-proxy.shop
https://*.worker-proxys.cyou
https://*.worker-proxy.cyou

::: warning 公共代理使用限制

  • 每个节点基于 Cloudflare Workers 免费计划,每天共 10 万次请求(所有用户共享)
  • 额度用完后会返回 429 状态码,需等待次日 UTC 0:00(北京时间 8:00)刷新
  • 公共节点仅限官网 (down.mptext.top) 使用,私有部署请搭建自己的节点
    :::

搭建私有代理

如果公共代理不够用,或者你需要私有部署,推荐搭建自己的代理节点。每个 Cloudflare 免费账号可创建多个 Worker,操作简单且完全免费。

查看 搭建私有代理节点 教程。


File: src/get-started/usage.md

[Binary file]


File: src/misc/architecture.md

整体架构

技术栈

层级 技术 说明
前端框架 Nuxt 3 + Vue 3 服务端渲染 + 单页应用
UI 样式 Tailwind CSS 原子化 CSS
数据表格 AG-Grid 文章列表展示
本地存储 IndexedDB (Dexie) 文章数据、HTML 内容、元数据缓存
后端服务 Nitro Nuxt 的服务端引擎
数据持久化 KV Storage 登录会话等服务端数据

系统架构

text
┌─────────────────────────────────────────────────┐
│                   浏览器端                        │
│                                                   │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐  │
│  │ Vue 组件  │  │ Composables│  │  IndexedDB    │  │
│  │ (页面/UI) │→ │ (业务逻辑) │→ │ (本地数据缓存) │  │
│  └──────────┘  └─────┬────┘  └───────────────┘  │
│                      │                            │
│          ┌───────────┴───────────┐               │
│          │    Downloader/Exporter │               │
│          │    (下载与导出引擎)     │               │
│          └───────────┬───────────┘               │
└──────────────────────┼───────────────────────────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
   ┌────────────┐ ┌─────────┐ ┌──────────┐
   │ Nitro 服务端│ │代理节点池│ │微信公众号API│
   │ (登录/搜索) │ │(CF Workers)│ │          │
   └────────────┘ └─────────┘ └──────────┘

核心模块

登录认证

用户通过微信扫码登录公众号后台,服务端获取并存储 Cookie 和 Token,生成 auth-key 返回客户端。后续所有对微信接口的请求都通过服务端代理,自动注入认证信息。

文章获取

通过公众号后台的文章搜索接口分页拉取文章列表(每页最多 20 条),文章链接和基本信息存储在浏览器的 IndexedDB 中。

内容下载

文章 HTML 内容通过代理节点池下载。ProxyManager 管理多个代理节点,采用最少失败 + 最久未用的策略选择最优节点,单节点连续失败 5 次后自动冷却 10 分钟。

数据导出

根据用户选择的格式,从 IndexedDB 中读取缓存数据进行转换和打包:

  • Excel/JSON:直接从缓存数据生成
  • HTML:解析文章中的图片和样式表,通过代理下载后替换 URL,打包为 ZIP
  • Markdown/TXT/Word:将 HTML 通过 TurndownService 转换后输出

Credentials 系统

阅读量和评论数据需要微信用户身份才能获取。通过 wxdown-service 程序在本地拦截微信客户端的请求,提取认证参数并同步到网站。Credentials 有效期约 25 分钟。

数据流向

text
微信公众号后台 ──→ Nitro 服务端 ──→ 浏览器(文章列表)
                                         │
微信文章页面 ──→ 代理节点 ──→ 浏览器(文章内容)──→ IndexedDB
                                                      │
                                              导出引擎 ──→ 本地文件

部署架构

部署方式 服务端 代理节点 数据存储
公共网站 Cloudflare Pages Cloudflare Workers 浏览器 IndexedDB + Cloudflare KV
Docker 本地容器 自建 Workers 浏览器 IndexedDB + 文件系统 KV
Vercel Vercel Functions 自建 Workers 浏览器 IndexedDB + Vercel KV

File: src/misc/domain.md

关于更换域名的一些事

本文记录了项目域名变化的历史,帮助用户了解各个地址之间的关系。

域名变更历史

时间 域名 平台 状态
早期 wechat-article-exporter.deno.dev Deno Deploy 已重定向
中期 exporter.wxdown.online Cloudflare 已停用
现在 down.mptext.top Cloudflare 当前使用

文档站点:

时间 域名 状态
早期 wechat-article-docs.deno.dev 已停用
中期 docs.wxdown.online 已停用
现在 docs.mptext.top 当前使用

变更原因

  • Deno Deploy → Cloudflare:deno.dev 域名在国内被屏蔽,迁移至 Cloudflare 以提升国内访问体验
  • wxdown.online → mptext.top:旧 Cloudflare 账号无法登录,域名无法绑定新账号,因此启用新域名

旧域名状态

wxdown.online 域名已于 2026 年 4 月过期,相关网站已不可用。

::: warning 数据迁移提醒
由于工具数据存储在浏览器的 IndexedDB 中,且与域名绑定,旧域名下缓存的数据已无法访问。如有重要数据,建议使用 私有部署 方案以确保域名一致性。
:::

当前地址


File: src/misc/features.md

[Binary file]


File: src/misc/qq-group.md

交流群

目前在 QQ 上开通了交流群,有需要的小伙伴可以进来和大家交流使用体验。

  • 494723496

File: src/others/database.md

缓存数据库设计

数据库名: wechat-article-exporter

version: 1

`article` store

用于存储文章列表接口的数据,减少接口请求次数。

对象数据结构如下:

ts
import {AppMsgEx} from "../types";

declare const article: AppMsgEx


// key 采用【公众号id】与【文章id】组合的形式
const key = `${fakeid}:${article.aid}`

// value 除了文章相关字段外,增加了 fakeid 字段
type StoreObjectValue = AppMsgEx & {
    fakeid: string
}

需要的索引:

  • fakeid
  • fakeid_create_time (复合索引)

`asset` store

用于存储 css 文件,因为大部分文章的样式文件都相同,所以缓存该文件对于减少下载速度有重要意义。

对象数据结构如下:

ts
interface Asset {
    /**
     * css文件路径 (keyPath)
     */
    url: string

    /**
     * 文件对象
     */
    file: File
}

`info` store

用于统计公众号已缓存信息。

对象数据结构如下:

ts
interface Info {
    /**
     * 公众号id (keyPath)
     */
    fakeid: string
    
    /**
     * 文章是否已全部加载
     * 
     * 公众号文章的加载逻辑是从最新的文章开始往前加载,越早的文章越靠后
     */
    completed: boolean

    /**
     * 缓存的消息数
     * 
     * 一条消息可能会包含多篇文章
     * 分页查询采用的是消息条数,而不是文章条数
     */
    count: number

    /**
     * 缓存的文章数
     */
    articles: number
}

version: 2

`api` store

用于统计接口调用情况,帮助分析微信接口频率限制规则。

该项目涉及到的可能会被微信限制调用频率的有如下接口:

  • /api/searchbiz 公众号列表 (调用频次相对较低,不容易出现限制)
  • /api/appmsgpublish 历史文章列表 (调用频次高,很容易出现限频)

注意

  1. 文章下载不涉及到微信接口调用,因为文章链接是公开可访问的,不需要携带cookie
  2. 即使/api/appmsgpublish接口被限频,仍可以通过带关键字进行调用

对象数据结构如下:

ts
type ApiName = 'searchbiz' | 'appmsgpublish'


interface APICall {
    /**
     * 接口名称
     */
    name: ApiName

    /**
     * 调用账号(nickname)
     */
    account: string

    /**
     * 调用时间
     */
    call_time: number

    /**
     * 调用结果是否正常
     * 
     * true: 正常
     * false: 被封禁
     */
    is_normal: boolean

    /**
     * 请求参数
     */
    payload: Record<string, any>
}

需要的索引:

  • account
  • account_call_time (复合索引)

version: 3

`proxy` store

用于统计代理使用情况。

对象数据结构如下:

ts
interface Proxy {
    // 代理地址 (keyPath)
    address: string

    // 是否正在被使用
    busy: boolean

    // 是否处于冷静期
    cooldown: boolean

    // 使用次数
    usageCount: number

    // 成功次数
    successCount: number

    // 失败次数
    failureCount: number

    // 下载流量
    traffic: number
}

File: src/others/下载流量分析.md

流量分析

接口的流量

服务器主要是通过proxyMpRequest函数代理微信接口,包括:

  • 登录流程
  • 获取登录公众号头像/昵称(从 html 中提取)
  • 获取公众号列表
  • 获取文章列表

资源下载的流量

这部分是代理流量的消耗大户,资源采用代理池并行下载,包括:

  • 下载文章 html 文本
  • 下载 html 中的资源文件(图片/样式)

页面上图片的显示

通过service worker技术规避掉图片的防盗链,这样就可以直接在客户端下载显示,不需要消耗代理流量。

  • 公众号头像显示
  • 文章封面图的显示

File: src/others/公众号登录原理.md

微信公众号二维码登录原理

页面展示的二维码图片地址如下:

text
https://mp.weixin.qq.com/cgi-bin/scanloginqrcode?action=getqrcode&random=

其中,random参数为系统当前的时间戳:

js
const random = new Date().getTime()

该链接返回的二维码图片内部编码了一个 url 地址,如下所示:

text
http://mp.weixin.qq.com/mp/scanlogin?action=index&qrticket=b5e94c8689978a087986ef0bb00fe2ea&scanscene=0#wechat_redirect

页面加载时,首先调用getIngorePassList接口,参数如下:

http request
POST /cgi-bin/bizlogin
Host: https://mp.weixin.qq.com

action=prelogin

根据返回结果,会调用report接口,如下:

http request
POST /cgi-bin/webreport
Host: https://mp.weixin.qq.com

reportJson={"devicetype":1,"newsessionid":"172059629456827","optype":1,"page_state":3,"log_id":19015}

newsessionid的取值逻辑如下:

js
this.sessionid = new Date().getTime() + "" + Math.floor(Math.random() * 100);

然后调用this.getQrcode()方法获取二维码,如下:

http request
POST /cgi-bin/bizlogin?action=startlogin
Host: https://mp.weixin.qq.com

userlang=zh_CN
redirect_url=
login_type=3
sessionid=sessionid

接口返回:

json
{"base_resp":{"err_msg":"ok","ret":0}}

表示正常。

此时页面会把this.hasStartLogin标志改为true,表示登录流程已开始。同时调用this.refreshQrcode()获取二维码图片并展示

未扫码:

json
{
  "acct_size": 0,
  "base_resp": {
    "err_msg": "ok",
    "ret": 0
  },
  "binduin": 0,
  "status": 0,
  "user_category": 0
}

已扫码:

json
{
  "acct_size": 19,
  "base_resp": {
    "err_msg": "ok",
    "ret": 0
  },
  "binduin": 0,
  "status": 4,
  "user_category": 0
}

已登录:

json
{
  "acct_size": 19,
  "base_resp": {
    "err_msg": "ok",
    "ret": 0
  },
  "binduin": 0,
  "status": 1,
  "user_category": 2
}

File: src/others/微信接口频率限制.md

微信获取文章列表接口的频率限制研究

目前测试结果,短时间内获取超过 600 条文章数据就很容易触发200013 freq control限制,然后会导致 24 小时内不能再调用该接口。

目前正在确定这个时间范围是多少。

17:20 左右出现

json
{
  "base_resp": {
    "err_msg": "freq control",
    "ret": 200013
  }
}

19:00 解封了

1~2个小时

注意:该接口被限制后仍然可以通过关键字搜索继续调用该接口,但不确定搜索是否也会触发freq control限制。

几个因素:

  • 数据量
  • 请求次数

根据测试,数据量在达到 1100 时达到了限制

触发限流的示例:

  • 20分钟请求了 1100 条数据量

cookie登录有效期4d


File: src/tutorials/export-article-links.md

如何批量导出某个公众号的全部文章链接?

:::info 本文需要的工具

  1. 一个微信 订阅号 或 服务号 (没有的话,可以去 微信公众平台 免费注册)
  2. 微信公众号文章导出工具
    :::

:::warning 注意
如果目标公众号关闭了搜索功能,则本文的方法无效。

:::

1. 注册公众号

::: tip 提示
若你已经有一个可用的 订阅号 或者 服务号 的话可跳过该步骤。
:::

前往 微信公众平台 进行注册,公众号(原订阅号)和服务号皆可。

2. 扫码登录网站

注册完公众号之后,进入网站的 登录页面,用微信扫描页面上的二维码,选择自己注册的 公众号 进行登录。

::: tip 提示
注意,这里必须选择 公众号 或者 服务号 进行登录,不能使用 小程序 登录,否则后续无法获取数据。
:::

3. 添加公众号,开始抓取文章链接

在 公众号管理 页面添加一个公众号,通过同步按钮拉取该公众号的所有文章链接。

4. 导出链接

在 文章下载 页面选择目标公众号,勾选对应的文章,通过右上角的【导出】选择导出excel/json,如下图所示:

--- METRICS ---