一、问题背景 我有一个微信公众号”默耘成长社”,想把自己的全部文章导入向量数据库,做一个能搜我历史文章的 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 │ ... │ │ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────┘
三步走:
拿文章列表 ——调用微信后台内部 AJAX 接口(带浏览器 Cookie,不触发风控)
逐篇抓正文 ——创建隐藏 iframe 加载文章阅读页,同源带 Cookie,提取 DOM 内容
打包下载 ——全部抓完后,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_scripts 让 content.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 ( ) { const params = new URLSearchParams (location.search ); let token = params.get ('token' ); if (token) return token; 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,fetch 带 credentials: '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 ; 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 ); } }
这样一来,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 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 await chrome.storage .local .set ({ wxDownloadProgress : { percent : 45 , current : 26 , total : 58 , detail : '...' } }); 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: iframe、Sec-Fetch-Site 等),这些差异足以触发风控。而 iframe 是浏览器原生导航行为,请求头完整,风控系统看不出异常。
六、总结 这个方案的核心思路不是”破解”微信的风控,而是利用已登录浏览器作为信任边界 :
浏览器登录 → 信任
同源 iframe → 不触发跨域检查
浏览器的完整请求上下文 → 风控看不出来
间隔 10 秒 → 不像爬虫
对于后端开发者来说,其实是一个”客户端代理”模式——把爬虫逻辑下沉到用户浏览器,利用浏览器已有的身份认证来绕过服务端风控。反过来想,很多 SaaS 产品的后台批量导出功能,本质上也是这个原理。