通过微信读书,把你关注的公众号最新文章抓成一份列表。
node bin/weread.mjs --format md### 某某公众号
| 时间 | 标题 | 链接 |
|---|---|---|
| 2026-08-04 17:01 | 某某某某某某某某某某 | [原文](https://mp.weixin.qq.com/s/xxxxx) |
| 2026-08-03 18:01 | 另一篇文章的标题 | [原文](https://mp.weixin.qq.com/s/yyyyy) |
拿到的是 mp.weixin.qq.com 原文直链,可以直接打开、可以喂给别的工具。
先花一分钟看完这节,判断它是不是你要的东西。
- 把你在微信读书里已订阅公众号的最新文章列出来(标题、发布时间、
mp.weixin.qq.com原文直链) - 输出 JSON 或 Markdown,可以直接喂给别的工具
- 帮你把新公众号加进来:给一篇它的文章链接就行
- ❌ 不替你过验证码。弹了就停下来提示你手动完成,不识别、不点选、不绕过
- ❌ 不做大规模采集。内置每日次数硬闸门(默认 2 次/天),适合十几个号以内
- ❌ 不碰你账号看不到的内容。它只是把你本来就能读的东西用程序读出来
- ❌ 不搜公众号名字。微信读书网页端搜不到公众号(实测无解),所以用文章链接代替
① git clone + cp config.example.json config.json
② 用专用 profile 再开一个 Chrome(--remote-debugging-port=9333 + --user-data-dir,
日常那个 Chrome 不用关)
③ 在这个新窗口里扫码登录微信读书 ← 全程唯一需要你动手的一步
④ node bin/weread.mjs --add <文章链接> ← 书架空的才需要
⑤ 填 config.json → node bin/weread.mjs --format md
前置条件:Node.js 18+(零第三方依赖,不用 npm install)、桌面版 Chrome、一个微信号。
| 局限 | 说明 |
|---|---|
| 收录不全 | 部分公众号在微信读书侧收录滞后,实测遇到过滞后半个多月的。平台侧问题,技术上无解 |
| 不实时 | 收录通常比公众号发布晚几个小时 |
| 登录要人工 | 扫码没法自动化,登录失效后需要你手动重新扫一次 |
| 规模有限 | 适合十几个以内的号。上百个号需要账号池 + 更严格限频,不在本工具范围 |
| 会遇到验证码 | 需要你手动过,工具会停下来等 |
| 平台随时可能变 | 2026 年 7 月底微信在同一周收紧了多条第三方采集路线,这条同样可能失效。建议留降级方案 |
- 本工具只是把你自己账号本来就能看到的内容用程序读出来,不解密、不越权、不绕过任何安全机制
- 请遵守微信读书的服务条款,控制频率,别拿去做大规模采集或商业分发
- 抓到的文章版权归原作者,转载请遵守对方要求
- 别把它挂成每 5 分钟跑一次的定时任务 —— 真那么干,坏的是你自己的账号
- 用了出问题自己负责
只想用的话可以跳过这节,直接看「快速开始」。
公众号文章一直不好批量拿。2026 年 7 月底微信关掉了公众平台后台的跨账号查询接口,一批依赖它的开源项目同时失效,其中一个 12k+ star 的项目直接归档了。
微信读书是另一条路:它把公众号当成「书」,bookId 形如 MP_WXS_<一
8000
数字>,文章通过网页版接口取,返回里带着原文 id,拼起来就是原文直链。这条路和上面那个被关掉的接口是两套系统,没受影响。
做法就是:在你自己那个已经登录微信读书的 Chrome 里,执行一小段 JS 去调它自己的接口。不用 Docker、不用服务器、不用申请公众号、不用装手机 App、不用第三方 API。
前置条件见上方「项目概览」。
git clone https://github.com/Pengyf04/weread-mp-fetcher.git
cd weread-mp-fetcher
cp config.example.json config.json工具需要在一个已登录微信读书的 Chrome 里执行 JS,所以要用调试端口启动它。
有两条路。推荐方案 A,方案 B 只在你确实不想再开一个 Chrome 实例时才用。
本节结论基于 Chrome 151(2026-08)实测。Chrome 每 4 周一个大版本,行为可能变。
macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9333 \
--user-data-dir="$HOME/.weread-mp-fetcher/chrome-profile"Windows(PowerShell)
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9333 `
--user-data-dir="$env:USERPROFILE\.weread-mp-fetcher\chrome-profile"Linux
google-chrome \
--remote-debugging-port=9333 \
--user-data-dir="$HOME/.weread-mp-fetcher/chrome-profile"对应的 config.json:
{ "chromePort": 9333, "chromeProfileDir": "~/.weread-mp-fetcher/chrome-profile" }几件要知道的事:
- 这是第二个独立的 Chrome 实例,和你日常那个同时开着互不影响(因为
--user-data-dir不同)。不需要退出你日常的 Chrome。 - 这个新窗口第一次确实是没登录的,扫一次码就好——登录态存在这个专用目录里,Chrome 完全退出再启动仍然保持登录(实测)。
- 端口用 9333 不是 9222。 9222 是各类调试工具的事实默认端口,极易被占;而且端口冲突不会报错——实测 9222 被占时,第二个实例会静默退到 IPv6
[::1]:9222,此时curl http://127.0.0.1:9222/json/version打到的是另一个实例、返回 404,极具迷惑性。 ⚠️ 同一个--user-data-dir同时只能有一个 Chrome 进程。目录已被占用时再带参数启动,只会打印「正在现有的浏览器会话中打开」并给旧进程开个新窗口,你新加的所有参数被静默忽略。所以「改了参数没生效」时,先确认这个专用实例是不是已经在跑。⚠️ Chrome 136(2025-04)起,在默认用户数据目录上--remote-debugging-port会被静默忽略(官方说明),端口根本不会开,而且没有任何报错。所以--user-data-dir不是可选项。
调试端口只监听
127.0.0.1,外部访问不到。但开着它意味着本机程序都能控制这个 Chrome,介意的话用完把这个专用实例关掉即可(不影响你日常那个)。
Chrome ≥ 144 提供了一个开关,可以在默认 profile 上开调试:chrome://inspect/#remote-debugging → 打开 "Allow remote debugging for this browser instance"(官方说明)。
选它之前必须知道(实测):
-
工具每建立一次新连接,你都要切到 Chrome 手动点一次「允许」。 点了约 2.6 秒握手成功;不点就一直挂着(实测 60 秒 / 90 秒),而且授权不跨连接复用。→ 不能自动化、不能定时跑、不能无人值守。
-
这个模式下
http://127.0.0.1:<端口>/json/version返回 404,工具改从默认 profile 目录下的DevToolsActivePort文件里读端口和 WebSocket 路径。 -
对应的
config.json是两项都留空(照抄方案 A 的配置会两处都指错):{ "chromePort": null, "chromeProfileDir": null }
「每次都要点允许」是 Chrome 的设计,本工具不会也不应该绕过它。
在第 2 步启动的那个 Chrome 里打开 https://weread.qq.com/,微信扫码登录。
就这一步需要你动手,剩下的工具全自动。(方案 A 第一次是空白 profile,扫一次码之后长期有效。)
如果你在微信读书里已经订阅过公众号,直接看看有哪些:
node bin/weread.mjs --shelf如果一个都没有(新账号常见),随便打开该公众号的任意一篇文章,复制链接,然后:
node bin/weread.mjs --add https://mp.weixin.qq.com/s/xxxxxxxx工具会自己算出这个号的 bookId 并订阅:
解析成功:MP_WXS_0000000000 (某某公众号) ← 文章页里的 __biz
已加入书架:MP_WXS_0000000000
为什么要给文章链接而不是名字? 微信读书的网页端搜不到公众号(实测 6 种接口/参数组合全部无效)。但公众号的身份标识
__biz就明写在它每一篇文章的页面里,bookId = MP_WXS_ + base64解码(__biz)——所以给一篇文章比给名字更可靠。文章链接在微信里长按「复制链接」就有。
--add也接受多个,以及直接给bookId:node bin/weread.mjs --add <链接1> <链接2> MP_WXS_0000000000
把 --shelf 输出里想监控的号,粘进 config.json 的 accounts:
"accounts": [
{ "name": "某某公众号", "bookId": "MP_WXS_0000000000" }
]然后:
node bin/weread.mjs --format md # 打到屏幕上
node bin/weread.mjs --format md --out # 直接写成文件完成。
不用手动复制阅读器页 URL。 工具会从书架接口自动推导出来。(早期版本要求手动复制,现在不需要了。)
node bin/weread.mjs --format md --out # → out/weread-20260811-1530.md
node bin/weread.mjs --format md --out notes/a.md # → 相对你当前所在目录
node bin/weread.mjs --out ~/Desktop # 给的是已存在的目录 → 在里面用默认文件名- 文件一律 UTF-8、无 BOM,不加文件头、不加统计行——内容就是不带
--out时你在屏幕上看到的那一份,一个字节都不差。用--out可以避开各家 shell 重定向(>)的编码差异。 - 加了
--out时 stdout 一个字节都不输出,写入路径、字节数、行数、篇数这些提示走 stderr。所以--out不能和| pbcopy这类管道一起用——想要管道就别加--out。 - 文件名带到分钟。同名文件直接覆盖,并在 stderr 多打一行提示,不会自动加
-2、-3之类的序号。 ⚠️ 两条相对路径的基准不一样,这是刻意的:config.json里的outDir(常设默认)相对仓库根,跟.gitignore里的out/对齐;命令行上现敲的--out <相对路径>相对你当前所在目录,符合命令行直觉。
| 命令 | 作用 |
|---|---|
node bin/weread.mjs |
抓取,输出 JSON |
node bin/weread.mjs --format md |
抓取,输出 Markdown 表格 |
--out [路径] |
把结果写进文件而不是打到屏幕上。路径可省略,省略就写 out/weread-<年月日-时分>.<md|json> |
--pages N |
每个号往回多翻几页,用来看更旧的文章。默认 1,上限 maxPagesPerRun(默认 3) |
node bin/weread.mjs --probe |
只看页面状态,不抓取。不消耗每日次数(无现成阅读器标签页时可能发 1 个书架请求,见「限频」节) |
node bin/weread.mjs --shelf |
列出你已订阅的公众号、bookId 和阅读器页 URL |
node bin/weread.mjs --add <链接|bookId>... |
订阅公众号。给文章链接会自动算出 bookId |
node bin/weread.mjs --quota |
看今天已经抓了几次、发了多少个请求 |
--config other.json |
用别的配置文件 |
node bin/weread.mjs --format md --pages 3 # 每个号翻 3 页- 单位是「页/请求」,不是「文章数」。 1 页 ≈ 20 次群发 ≈ 70–80 篇(因号而异)。
之所以不做
--limit 50 篇或--since 某天:那两种写法的请求成本没法预判, 而本工具最该让你看清楚的就是"这一条命令要打几个请求"。请求数 = 公众号个数 × N,跑之前会打印出来。 - 先纠正一个常见误解:默认拿到的不是"20 篇文章",是"20 条群发"——一条群发里可以有好几篇, 所以你现在拿到的其实已经是 70–80 篇了。
- 有页失败时,已经抓到的页照常输出,md 里会在该号表格后面附一行警告,JSON 里是
partialErr。
微信读书对这个接口有风控。作者在验证方案那天,一天里请求了 30 多次,直接触发反爬——页面白屏了好几个小时。
所以工具内置了三道硬约束:
"maxRunsPerDay": 2, // 一天最多跑几次(一次不算全败的运行 = 1 次)
"maxRequestsPerDay": 40, // 一天最多发多少个真实请求(文章 + 书架)
"maxPagesPerRun": 3, // --pages 的上限,超过直接拒绝
"requestIntervalMs": 3000 // 每两个请求之间的间隔(跨页也跨号)- 三道都是拒绝执行,不是警告
- 请求数怎么算:公众号个数 ×
--pages。4 个号--pages 3= 12 个请求,maxRunsPerDay: 2下一天最多 24 个 - 再加 1 个的情况:没有可复用的阅读器标签页、
config.json里又没写readerUrl时, 工具要问一次书架接口才能推导出阅读器页 URL。这 1 个请求会如实记进账本的"书架"一栏, 也占maxRequestsPerDay的预算(运行前那句"预计 N 个请求"只按 号数 × pages 预估,不含它)。 把阅读器标签页常开着、或在 config 里写死readerUrl,就不会发生。 例外:这次运行若在后续环节提前终止(额度闸门拒绝、页面没 ready、全部号失败等), 整轮不写账本——此时这 1 个书架请求已经发出但不会被记上,账本只可能少记、不会多记 --probe不算次数、不发文章请求;但它同样要先找到阅读器页,所以上面那 1 个书架请求 在探针路径上也可能发生(探针路径整体不计账)- 抓取失败不计数,但也别立刻重试,隔几小时再说
--quota会告诉你今天已经花了多少:今日(…)已抓 1/2 次;请求 12/40(文章 12,书架 0)
⚠️ maxRequestsPerDay: 40是个默认预算,不是"安全线"。 唯一的参照点是作者那次 「一天 30 多个请求触发反爬」的一手观察——那是一次观察,真实阈值未知也测不出来, 别把 40 当成"40 以内一定安全"。另外:全部公众号都失败时,工具会额外发 1 个书架接口请求去看看"账号是不是还好着" (见下面
-2041)。它只在这条已经出错的路径上发生,而全失败那一轮整轮都不计账, 所以这 1 个请求进不了账本——与上面推导readerUrl那个书架请求(会记账)不是一回事。
社区经验是:几个号、每天几次,没问题;上百个号必须账号池加严格限频,那不在本工具范围内。
页面可能会弹出腾讯防水墙的验证码(图片选择或滑块)。这时候:
$ node bin/weread.mjs
页面弹出了验证码,需要你在 Chrome 里手动完成。
完成后重新运行本命令即可。
切到 Chrome 里手动过一下,然后重新运行命令就行。 工具会自动确认页面恢复正常之后才继续抓取。
本工具不会替你识别、点选或绕过验证码——那是绕过人机校验,不做。它做的是「发现验证码 → 停下来 → 告诉你 → 等你处理完 → 自己确认真的好了 → 继续」。
想少碰到验证码,最有效的办法是:让那个阅读器标签页一直开着。反复新开标签页是触发风控的主要原因。
接口返回 -2041
最常见的一种是上下文校验:请求必须发在阅读器页(/web/mp/reader/...)里,在微信读书首页发同样的请求一定失败。工具已经处理了(自动导航到阅读器页再发请求),正常不会遇到。
但 -2041 的成因不止一种,也没穷尽——还观察到过"探针报正常、接口却 -2041,手动刷新页面后验证码显形,处理完就恢复了"。所以真遇到时,工具会自动刷新一次阅读器页 → 重探 → 把观察到的东西如实报给你:
接口返回 -2041。已自动刷新阅读器页并重探,观察到:
· 刷新阅读器页: 已刷新
· 刷新后探针判定: captcha
· 书架接口 /web/shelf/sync: 可用 ← 可用说明登录态还在
它不会替你下结论说"这是被限流了",也不会自动重发请求——刷新这个动作同时改变了好几个变量,从这里推不出因果。书架那一行只在所有号都失败时才去问(多 1 个请求),部分号成功时不问。
接口返回 -2010 用户不存在
登录失效了,在 Chrome 里重新扫码登录微信读书即可。
页面白屏、--probe 报 blank
触发风控了。停手,隔几个小时再试,并且改掉反复新开标签页的习惯。
某个公众号一直取不到新文章 先确认不是工具的问题:在浏览器里手动打开那个公众号看看。微信读书对部分公众号的收录会滞后,实测遇到过滞后半个多月的。这是平台侧的事,技术上解决不了。
一天明明发了好几篇,只取到一篇
如果你是照着思路自己实现的,检查有没有只读了 subReviews[0]。微信读书的结构是「一次群发 = 一个条目,里面的 subReviews 才是一篇篇文章」,有的号一天群发 3–4 篇。本工具已经展开了。
能不能直接给公众号名字,让它自动搜出来?
不能。微信读书网页端搜不到公众号(实测 /web/search/global 加 type/scope 等参数返回的都是书;MP 专用端点全 404)。用 --add <文章链接> 代替,效果一样且更可靠。
找不到 Chrome 调试端口 先跑一条 curl,按结果对号入座(把 9333 换成你实际用的端口):
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9333/json/version| 结果 | 含义 | 怎么办 |
|---|---|---|
200 |
方案 A 正常 | 直接跑工具 |
404 |
你连到的是一个方案 B(auto-connect)实例,不是方案 A 的专用实例 | 换个端口重开专用实例,或确认启动命令真的生效了 |
000 / 拒绝连接 |
端口没开 | 两个常见原因:① 参数加在了默认用户数据目录上(Chrome 136+ 会静默忽略);② 那个专用目录已经有一个 Chrome 进程在跑,新参数被静默忽略 |
| 工具挂起几十秒没输出 | 多半是方案 B 在等你点「允许」 | 切到 Chrome 点一下,或改用方案 A |
也可以在 config.json 里显式写 "chromePort": 9333。
{
"onReader": true,
"meta": { "pages": 3, "requestsTotal": 3 },
"sources": [
{
"name": "某某公众号",
"bookId": "MP_WXS_0000000000",
"items": [
{
"t": 1785636055,
"title": "文章标题",
"url": "https://mp.weixin.qq.com/s/xxxxx",
"rid": "MP_WXS_xxx_yyy"
}
],
"pageMeta": [{ "offset": 0, "reviews": 20, "items": 74, "oldest": 1784000000 }],
"pagesFetched": 3
}
]
}t是 Unix 秒。做增量的话,记住每个号上次的最大t,下次只取更大的⚠️ 同一次群发的多篇文章t完全相同,用>比较会把它们重复取出来。按url/rid去重更稳(工具内部跨页去重用的也是这个键)- 单个号一页能取约 20 次群发,展开后通常 70–80 篇
meta.requestsTotal是本次实际发出的请求数(提前取完会比预告少)pageMeta逐页记{offset, reviews, items, oldest},用来核对翻页是不是真的翻动了:三页的oldest应当严格递减- 某个号的字段含义:
err= 这个号一篇都没取到;partialErr= 翻到第pagesFetched+1页时停了,前面的都在items里
- 只有
offset有效。count和maxIdx会被服务端忽略——实测offset=0、offset=0&count=50、maxIdx=20三种请求返回逐字段一致。所以本工具只发offset。(有个第三方项目用的是maxIdx=0&count=100,照抄会翻车。) - 熔断器只兜 CDP 层的抛错(比如你把那个标签页关了),连续 2 次就停手、剩下的号标成"未尝试"。它不管
errCode类失败:每个号都返回-2041时,请求照样会一个个发出去——这和加--pages之前的行为一致,不是缺陷。
bin/weread.mjs 命令行入口
lib/cdp.mjs 零依赖的 Chrome 控制客户端(自己实现了 WebSocket)
lib/scripts.mjs 注入页面执行的 JS:状态探针 / 取一页文章 / 书架 / 订阅
lib/fetchflow.mjs 翻页调度:翻页、去重、间隔、部分失败、-2041 出错路径(纯逻辑,零网络)
lib/render.mjs 时间格式化 + Markdown 表格(唯一的日期格式化实现)
lib/save.mjs 写文件(--out)
lib/args.mjs 命令行参数解析
lib/mp.mjs 从公众号文章链接推导 bookId(解析 __biz)
lib/quota.mjs 每日次数 + 请求预算两道闸门,账本记明细
test/offline.test.mjs 离线自测,不连 Chrome 也能跑
test/fixtures/ 翻页回归用的假数据与基线产物
config.example.json 配置模板
docs/HOW-IT-WORKS.md 原理、踩过的坑、判据是怎么定出来的
没有 npm install,没有 node_modules,没有编译步骤。
MIT