8000
Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

weread-mp-fetcher

CI

通过微信读书,把你关注的公众号最新文章抓成一份列表。

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。


快速开始

前置条件见上方「项目概览」。

第 1 步:装上

git clone https://github.com/Pengyf04/weread-mp-fetcher.git
cd weread-mp-fetcher
cp config.example.json config.json

第 2 步:让 Chrome 开着调试端口

工具需要在一个已登录微信读书的 Chrome 里执行 JS,所以要用调试端口启动它。

有两条路。推荐方案 A,方案 B 只在你确实不想再开一个 Chrome 实例时才用。

本节结论基于 Chrome 151(2026-08)实测。Chrome 每 4 周一个大版本,行为可能变。

方案 A(默认推荐):专用调试 Profile

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,介意的话用完把这个专用实例关掉即可(不影响你日常那个)。

方案 B(备选,不推荐做日常):用你正在用的那个 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 的设计,本工具不会也不应该绕过它。

第 3 步:登录微信读书

第 2 步启动的那个 Chrome 里打开 https://weread.qq.com/微信扫码登录

就这一步需要你动手,剩下的工具全自动。(方案 A 第一次是空白 profile,扫一次码之后长期有效。)

第 4 步:把想看的公众号加进来

如果你在微信读书里已经订阅过公众号,直接看看有哪些:

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 也接受多个,以及直接给 bookIdnode bin/weread.mjs --add <链接1> <链接2> MP_WXS_0000000000

第 5 步:配置并运行

--shelf 输出里想监控的号,粘进 config.jsonaccounts

"accounts": [
  { "name": "某某公众号", "bookId": "MP_WXS_0000000000" }
]

然后:

node bin/weread.mjs --format md            # 打到屏幕上
node bin/weread.mjs --format md --out      # 直接写成文件

完成。

不用手动复制阅读器页 URL。 工具会从书架接口自动推导出来。(早期版本要求手动复制,现在不需要了。)

关于 --out

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 重定向(>)的编码差异。
  • 加了 --outstdout 一个字节都不输出,写入路径、字节数、行数、篇数这些提示走 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 用别的配置文件

想看更旧的文章:--pages

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 里重新扫码登录微信读书即可。

页面白屏、--probeblank 触发风控了。停手,隔几个小时再试,并且改掉反复新开标签页的习惯。

某个公众号一直取不到新文章 先确认不是工具的问题:在浏览器里手动打开那个公众号看看。微信读书对部分公众号的收录会滞后,实测遇到过滞后半个多月的。这是平台侧的事,技术上解决不了。

一天明明发了好几篇,只取到一篇 如果你是照着思路自己实现的,检查有没有只读了 subReviews[0]。微信读书的结构是「一次群发 = 一个条目,里面的 subReviews 才是一篇篇文章」,有的号一天群发 3–4 篇。本工具已经展开了。

能不能直接给公众号名字,让它自动搜出来? 不能。微信读书网页端搜不到公众号(实测 /web/search/globaltype/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 有效。 countmaxIdx 会被服务端忽略——实测 offset=0offset=0&count=50maxIdx=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,没有编译步骤。


License

MIT

About

通过微信读书抓取公众号最新文章。零依赖,复用你已登录的 Chrome,输出 mp.weixin 原文直链。

Topics

Resources

Stars

23 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0