Chrome插件下载微信公众号文章:原理与实现

一、问题背景

我有一个微信公众号”默耘成长社”,想把自己的全部文章导入向量数据库,做一个能搜我历史文章的 AI 问答系统。

微信公众平台提供了 API,但我的个人订阅号没认证,freepublish/batchget 接口在 2025 年 7 月被微信官方回收了权限。更糟的是,从服务器 IP 直接访问 mp.weixin.qq.com/s/xxx 文章页面,会被微信的风控系统拦截,返回「环境异常」验证页面。

无论换什么 User-Agent、加 Referer、Playwright + stealth 、甚至 Cookie 预热,全被拦死——微信是 IP 级别的风控。

但我发现一条路:在自己已登录的 Chrome 浏览器里,文章页面能正常打开。 那就写一个 Chrome 扩展,利用浏览器的登录态来下载。

二、整体思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────┐
│ 用户浏览器(已登录微信后台) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ popup.js │ │ content.js │ │background.js│ │
│ │ (弹窗UI) │──│ (注入到后台) │──│ (下载文件) │ │
│ └──────────┘ └──────┬───────┘ └─────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ list_ex │ │ 文章页面 │ │ 文章页面 │ │
│ │ 内部API │ │ iframe 1 │ │ iframe 2 │ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────┘

三步走:

  1. 拿文章列表——调用微信后台内部 AJAX 接口(带浏览器 Cookie,不触发风控)
  2. 逐篇抓正文——创建隐藏 iframe 加载文章阅读页,同源带 Cookie,提取 DOM 内容
  3. 打包下载——全部抓完后,JSON 文件通过 chrome.downloads API 触发浏览器下载

三、关键技术实现

3.1 Manifest V3 权限

1
2
3
4
5
6
7
8
9
{
"manifest_version": 3,
"permissions": ["activeTab", "scripting", "downloads", "storage"],
"host_permissions": ["https://mp.weixin.qq.com/*"],
"content_scripts": [{
"matches": ["https://mp.weixin.qq.com/*"],
"js": ["content.js"]
}]
}

content_scriptscontent.js 在用户每次打开微信公众号后台页面时自动注入。注入后就能访问页面的 DOM、Cookie、以及发出同源请求。

3.2 获取文章列表——调用微信内部 API

打开 Chrome DevTools 的 Network 面板,在公众号后台翻页时能看到一个请求:

1
POST /cgi-bin/appmsg?action=list_ex&begin=0&count=5&type=9&token=xxx&lang=zh_CN&f=json&ajax=1

返回的 JSON 长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"app_msg_cnt": 58,
"app_msg_list": [
{
"aid": "1234567890",
"appmsgid": 2247484232,
"title": "一个顶级思维方法:帮你立刻消除烦恼",
"link": "http://mp.weixin.qq.com/s?__biz=...",
"create_time": 1749363911,
"cover": "https://mmbiz.qlogo.cn/...",
"digest": "我们应该这样生活..."
}
]
}

这个接口不需要额外的 API 权限——它就是你打开后台页面时,前端 JS 自己调的那个接口。我们只是借用浏览器的 Cookie 替我们调。

Token 怎么拿? Token 就在当前页面的 URL 参数或某个 <script> 标签里。在 content.js 里直接读:

1
2
3
4
5
6
7
8
9
10
11
12
13
function extractToken() {
// 方法1:URL 参数
const params = new URLSearchParams(location.search);
let token = params.get('token');
if (token) return token;

// 方法2:页面内嵌 JavaScript 变量
for (const script of document.querySelectorAll('script')) {
const m = script.textContent.match(/token\s*[:=]\s*["'](\d{8,})["']/);
if (m) return m[1];
}
return null;
}

有了 token,fetchcredentials: 'include' 调用上述 API,自动翻页,就能拿到全部文章列表。

3.3 获取文章正文——iframe 同源加载

列表 API 只返回元数据(标题、链接、摘要),不含正文。正文要从文章阅读页(mp.weixin.qq.com/s/xxx)提取。

为什么不能用 fetch? 因为从服务器 IP fetch 文章页面会被风控拦截。但我们是在用户已登录的浏览器里,Cookie 有效,不会被拦。

问题来了:就算在浏览器里,fetch(articleUrl) 有时候也会触发验证页面。可能是因为 fetch 不带完整的浏览器上下文,微信的风控检测到异常。

解决方案:iframe 同源嵌入

既然是同源(mp.weixin.qq.com),在后台页面里塞一个隐藏 iframe,指向文章阅读页,等加载完直接读 DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function fetchArticleContent(article) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;';
iframe.src = article.link;

const timeout = setTimeout(() => {
article.no_content = true;
resolve(article);
}, 8000);

iframe.onload = () => {
try {
const doc = iframe.contentDocument;
const html = doc.documentElement.outerHTML;
// 从 HTML 中提取 var msg_title、#js_content 等
extractFromHTML(html, article);
} catch(e) {}
clearTimeout(timeout);
iframe.remove();
resolve(article);
};

document.body.appendChild(iframe);
});
}

iframe 的 contentDocument 在同源情况下可以直接访问,微信文章页面结构很规整:

  • var msg_title = "..." — 标题
  • var nickname = "..." — 作者
  • var msg_create_time = "..." — 发布时间
  • #js_content div — 正文 HTML

用正则提取就行:

1
2
3
4
5
6
7
function extractFromHTML(html, article) {
const t = html.match(/var\s+msg_title\s*=\s*(['"])([\s\S]*?)\1/);
if (t) article.title = decodeURIComponent(JSON.parse('"' + t[2] + '"'));

const contentMatch = html.match(/<div[^>]*id="js_content"[^>]*>([\s\S]*?)<\/div>/i);
if (contentMatch) article.content_html = contentMatch[1];
}

3.4 防风控:10 秒间隔

微信对异常请求很敏感。并发批量抓取容易被检测。改成逐篇串行 + 10 秒间隔

1
2
3
4
5
6
7
8
9
for (const art of articles) {
const result = await fetchArticleContent(art);
results.push(result);
done++;

if (done < articles.length) {
await sleep(10000); // 每篇间隔 10 秒
}
}

这样一来,58 篇文章大约需要 10 分钟。看似慢,但稳定可靠。如果 58 篇文章并行发出去,微信三秒内就封你。

3.5 下载 JSON——background.js

Chrome 扩展的 popup 生命周期很短(弹窗关闭就销毁),下载操作交给 Service Worker(background.js):

1
2
3
4
5
6
7
8
9
10
11
// background.js
chrome.runtime.onMessage.addListener((msg) => {
if (msg.action === 'download') {
const blob = new Blob([JSON.stringify(msg.data)], { type: 'application/json' });
const reader = new FileReader();
reader.onload = () => {
chrome.downloads.download({ url: reader.result, filename: msg.filename });
};
reader.readAsDataURL(blob);
}
});

3.6 进度回传——chrome.storage

popup 需要实时显示进度条。content.js 把进度写入 chrome.storage.local,popup.js 每 500ms 轮询一次:

1
2
3
4
5
6
7
8
9
10
11
12
// content.js — 写进度
await chrome.storage.local.set({
wxDownloadProgress: { percent: 45, current: 26, total: 58, detail: '...' }
});

// popup.js — 读进度
setInterval(async () => {
const data = await chrome.storage.local.get('wxDownloadProgress');
if (data.wxDownloadProgress) {
progressFill.style.width = data.wxDownloadProgress.percent + '%';
}
}, 500);

四、完整数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
用户点击"开始下载"


popup.js ──chrome.tabs.sendMessage──▶ content.js

┌───────────┴───────────┐
▼ ▼
提取 token DOM 方式回退
│ │
▼ │
调用 list_ex API ◀────────────────┘
(带 Cookie, fetch)


文章列表 (58篇)
[{title, link, aid...}]


逐篇创建隐藏 iframe
(mp.weixin.qq.com/s/xxx)


iframe onload → 读 DOM
var msg_title → 标题
#js_content → 正文


全部抓完 → 打包 JSON


background.js → chrome.downloads


浏览器弹出下载

五、为什么这招管用?

核心原因:你在浏览器里登录微信后台后,mp.weixin.qq.com 域下的所有 Cookie 都存在。Chrome 扩展注入的脚本和后台页面同源,天然带 Cookie。微信的风控系统看到的是”一个正常登录的用户在浏览自己的后台和文章”,不会触发验证。

对比:

方式 Cookie 同源 是否被拦
服务器 curl ✅ 被拦
服务器 Playwright ✅ 被拦
搜狗中转 ✅ 被拦
浏览器 fetch ⚠️ 偶尔被拦
浏览器 iframe 稳定

fetch 和 iframe 都有 Cookie,但为什么 fetch 偶尔被拦?我的猜测是:微信风控系统还检测了 TLS 指纹、HTTP 头完整性等信号。fetch 发出的请求头比正常页面导航少了很多(比如少了 Sec-Fetch-Dest: iframeSec-Fetch-Site 等),这些差异足以触发风控。而 iframe 是浏览器原生导航行为,请求头完整,风控系统看不出异常。

六、总结

这个方案的核心思路不是”破解”微信的风控,而是利用已登录浏览器作为信任边界

  1. 浏览器登录 → 信任
  2. 同源 iframe → 不触发跨域检查
  3. 浏览器的完整请求上下文 → 风控看不出来
  4. 间隔 10 秒 → 不像爬虫

对于后端开发者来说,其实是一个”客户端代理”模式——把爬虫逻辑下沉到用户浏览器,利用浏览器已有的身份认证来绕过服务端风控。反过来想,很多 SaaS 产品的后台批量导出功能,本质上也是这个原理。