微读 · weidu | 公众号文章读取工坊
工坊规矩 微信反爬不是一面墙,是一堆陷阱。每一层(Jina、WebFetch、Invoke-WebRequest)都会"看起来工作"——返回 200、给你点东西——但内容是错的(验证码页、乱码、空壳)。只信原始字节 + 自己解码,别信任何中间层的"它帮你解码好了"。
接活:用户给的输入
- 链接形如
https://mp.weixin.qq.com/s/<id>—— 直接读 - 链接 + 任务:「读这个 + 摘要 / 提取观点 / 翻译 / 对比另一篇」—— 先读再做任务
- 多个链接:批量处理,但每两次 fetch 之间
Start-Sleep -Seconds 2防风控
班规总纲
- 先抓原始字节,再解码。任何"看起来工作但内容错"的中间层(PowerShell
Invoke-WebRequest、Jina)都不能信。 - 双容器都试,不假设。
js_content在就用,不在就找content_noencode——不要根据公众号名字猜。 - 元数据缺失就说缺失,不编。新版沉浸式文章
nickname/ct大概率拿不到,老实写"作者未知"。 - chars=0 不是成功。如果两个容器都没匹配上,提取脚本必须报错退出,不能静默返回空 → 给用户假象。
- 抓下来的 HTML 留 sha8。同一篇文章不同时间抓可能不同(小幅 A/B 测试),sha8 让你后期对比能定位差异。
Step 1:原始字节下载(PowerShell)
$url = "<wx-url>"
$out = "$env:TEMP\wx_raw.html"
$req = [System.Net.HttpWebRequest]::Create($url)
$req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
$req.Headers.Add("Accept-Language","zh-CN,zh;q=0.9")
$req.Timeout = 30000
$resp = $req.GetResponse()
$ms = New-Object System.IO.MemoryStream
$resp.GetResponseStream().CopyTo($ms)
[System.IO.File]::WriteAllBytes($out, $ms.ToArray())
$resp.Close()
"len=" + $ms.Length
"sha8=" + (Get-FileHash $out -Algorithm SHA256).Hash.Substring(0,8)
为什么必须这样:Invoke-WebRequest 内部对 response body 做编码推断时,会把 UTF-8 字节按系统码页(中文 Windows = GBK / cp936)解码——所有中文字符变乱码。HttpWebRequest + WriteAllBytes 走原始字节路径,由后续 Python 按 UTF-8 解码,干净。
Step 2:判断容器类型
抓下来先看是旧版还是新版:
$h = [System.IO.File]::ReadAllText("$env:TEMP\wx_raw.html", [System.Text.Encoding]::UTF8)
"js_content present? " + $h.Contains('id="js_content"')
"content_noencode present? " + $h.Contains('content_noencode:')
| 类型 | 容器 | 典型公众号 |
|---|---|---|
| 旧版 | <div id="js_content">...</div> |
APPSO、知名科技号、传统排版 |
| 新版"沉浸式"流式文章 | inline JS 变量 content_noencode: '<html-string>' |
短内容创作者、AI 圈个人号 |
两个都可能同时存在——以 js_content 优先(更结构化)。
Step 3:双容器提取(Python)
import re, html, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"<path-to-html>", encoding="utf-8") as f:
h = f.read()
def find_meta(name):
m = re.search(r'<meta[^>]+(?:property|name)="' + name + r'"[^>]+content="([^"]*)"', h)
return html.unescape(m.group(1)) if m else None
title = find_meta("og:title")
# 路径 A:旧版 js_content(优先)
m = re.search(r'<div[^>]*id="js_content"[^>]*>(.*?)</div>\s*<script', h, re.S)
body_html = ""
if m:
body_html = m.group(1)
else:
# 路径 B:新版 content_noencode JS string
i = h.find("content_noencode:")
if i > 0:
i = h.find("'", i)
j = i + 1
buf = []
while j < len(h):
c = h[j]
if c == "\\" and j + 1 < len(h):
nx = h[j+1]
if nx == "x" and j + 3 < len(h):
buf.append(chr(int(h[j+2:j+4], 16))); j += 4; continue
if nx == "u" and j + 5 < len(h):
buf.append(chr(int(h[j+2:j+6], 16))); j += 6; continue
buf.append({'n':'\n','t':'\t','r':'\r',"'":"'",'"':'"','':'','/':'/'}.get(nx, nx))
j += 2; continue
if c == "'":
break
buf.append(c); j += 1
body_html = "".join(buf)
# 班规:chars=0 不是成功
assert body_html, "FATAL: 两个容器都没匹配上——可能是新版变体或反爬命中"
# 去标签
body = re.sub(r'<br[^>]*>', '\n', body_html)
body = re.sub(r'</p>', '\n\n', body)
body = re.sub(r'</(h[1-6]|li|blockquote|section|div)>', '\n', body)
body = re.sub(r'<style[^>]*>.*?</style>', '', body, flags=re.S)
body = re.sub(r'<script[^>]*>.*?</script>', '', body, flags=re.S)
body = re.sub(r'<[^>]+>', '', body)
body = html.unescape(body)
body = re.sub(r'\n{3,}', '\n\n', body).strip()
print(f"TITLE: {title}")
print(f"chars: {len(body)}")
元数据提取
| 字段 | 出处 |
|---|---|
| 标题 | <meta property="og:title" content="..."> |
| 公众号名 | var nickname = htmlDecode("...")(旧版有,新版常缺) |
| 发布时间 | var ct = "<unix-timestamp>"(同上) |
| 原文 URL | <meta property="og:url" content="..."> |
新版沉浸式文章 nickname / ct 大概率拿不到——老实写"作者未知 / 时间未知",不要编。
已知陷阱
- ❌ 只看
js_content容器 → 新版沉浸式文章一律 0 字符正文 - ❌ Python
urllib.request直请 → 微信检测后返回反爬页(无浏览器 UA + Accept-Language) - ❌
Invoke-WebRequest -OutFile→ 同样存在编码推断问题 - ❌ 假设
content_noencode是 JSON 编码 → 实际是 JS string 转义(\x0a不是\n) - ❌ 静默返回空正文 → 给用户假象,下游任务全错
- ⚠️ 连续抓多篇 IP 易被风控 →
Start-Sleep -Seconds 2间隔 - ⚠️ 部分文章正文段落是图片 →
<img>标签里只有图片 URL,需单独提data-src走 OCR 或 vision - ⚠️ 个别公众号会做"会员可见"折叠 → 抓到的只是开头部分,要在班规里向用户报告
实测案例
完整案例见 examples/ 目录:
- appso-fable5-pingti.md — APPSO(旧版
js_content) - zhoutian-claude-songkou.md — 周天财经(新版
content_noencode)
两个版本一份脚本兜底,跑通。
一句话出师证书
微信公众号文章读取的核心不是"用什么工具",是"信哪一层"。只信原始字节 + 你自己写的解码逻辑——
Invoke-WebRequest给你 GBK 乱码、Jina 给你验证码页、js_content在新版文章里压根不存在。
发现日期:2026-06-16 · 打磨日期:2026-06-17(按鲁班 Skill v1.0 思路)