01月18日
一、今日完成情况
- 配置微信公众号批量采集工具 –任务分解 –完成
- 了解- Deno Deploy 私有代码部署平台和- Cloudflare Worker 平台
- 了解私有代理地址的定义配置方法,知晓其意义
- 任务一:域名“接入” Cloudflare(这是基石)
- 任务二:部署 Worker 逻辑
- 任务三:绑定你的域名
easytake.work - 任务四:本地 Docker 项目联调
- 私有项目部署Docker
- 配置mac输入法以及AI模型 https://www.bilibili.com/video/BV1VXvDBXED4 –未完成
- 配置Obsidian的Claude skill,实现修改笔记和canvas视图的效果。
二、今日感悟
- 核心业务数据:
- 今日工作总结:
- 明日工作计划:
- 今日学习成长:
三、备注
- 无
四、微信公众号批量爬取
1、私有化项目部署
私有部署链接:
https://docs.mptext.top/advanced/private-deploy.html
docker pull ghcr.io/wechat-article/wechat-article-exporter:latest
启动容器:
docker run -d --rm \
--restart always \
--network host \
--name wechat-article-exporter \
-p 3000:3000 \
-v .data:/app/.data \
ghcr.io/wechat-article/wechat-article-exporter:latest
注意: > * 如果你运行命令时带了 --rm,容器在停止后会自动删除,你不需要再运行 docker rm。
所以此指令以及配置好了,如果开始启动之后选择ctrl C停止,则容器会自动关闭。
正常关闭:
docker stop wechat-article-exporter
暴力关闭:
docker kill wechat-article-exporter
方式错误,在MacBook的环境下,建议使用docker compose + mkcert 自签名证书 的方法。
下面是Docker-compose的配置正确的路径,请仔细查看:
安装证书管理包:
相当于Linux的sudo apt --upgrade 更新的包管理器homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
mkcert -install
安装完毕,现在按照教程方法修改
创建项目文件
cd Documents/code/Docker
mkdir wechat
cd wechat
mkdir certs #管理公钥私钥
cd certs
执行以下代码生成本地 IP 对应的证书文件:
mkcert localhost 127.0.0.1 ::1
配置Docker管理文件
# nginx.conf 文件
server {
listen 80;
server_name localhost;
# HTTP 自动重定向到 HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name localhost;
# SSL 证书路径(确保文件名与 ./certs 目录下的实际文件一致)
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
# 推荐的 SSL 配置(增强安全性)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
# 代理到应用容器
# 注意:这里的 app 必须对应 docker-compose.yml 中的服务名
# 请确认容器内程序监听的确实是 3000 端口
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
services:
app:
image: ghcr.io/wechat-article/wechat-article-exporter:latest
restart: always
volumes:
# 持久化 KV 数据(防止容器重启丢失)
- ./.data:/app/.data
nginx:
image: nginx:alpine
container_name: wechat-article-nginx
restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
volumes:
- ./certs:/etc/nginx/certs:ro # 挂载证书(只读)
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # 挂载配置
depends_on:
- app
此时项目架构如下所示:
启动项目
docker compose up -d
关闭项目
docker compose down
项目启动之后,浏览器访问如下路径即可:
2、私有代理配置(理论方法)
既然你已经拥有了域名 easytake.work,我们接下来的目标就是利用 Cloudflare 的边缘计算能力,把这个域名变成你的专属微信爬取加速节点。
任务一:域名“接入” Cloudflare(这是基石)
添加站点:登录 Cloudflare 控制台,点击 Add Site,输入
easytake.work。修改 DNS 服务器:
- Cloudflare 会分配给你两个地址,类似
ns1.cloudflare.com和ns2.cloudflare.com。 - 操作:你需要登录你购买
easytake.work的域名注册商后台(比如 Namecheap, GoDaddy 等),把原来的 DNS 服务器换成 Cloudflare 提供的这两个。 - 验证:Cloudflare 首页显示
easytake.work状态为 Active 即可。
- Cloudflare 会分配给你两个地址,类似
任务二:部署 Worker 逻辑
创建 Worker:在左侧菜单选 Workers 和 Pages -> 创建应用程序 -> 创建 Worker。
起名:建议起名为
wechat-proxy注入代码:
- 点击 编辑代码。
- 将你之前贴出的那一大段 JavaScript 节点代码(包含
parseRequest和wfetch的那部分)全选覆盖掉现有的默认代码。 - 点击 部署
任务三:绑定你的域名 easytake.work
这是最关键的一步,能让你彻底摆脱翻墙访问 workers.dev 的限制:
- 进入你刚才创建的
wechat-proxyWorker 详情页。 - 点击顶部菜单的 设置 (Settings) -> 触发器 (Triggers)。
- 在 自定义域 (Custom Domains) 栏目下,点击 添加自定义域
- 输入:
proxy.easytake.work(这会自动在你的 DNS 记录里生成一条 CNAME)。 - 点击确认,等待状态变为 有效 (Active)。
任务四:本地 Docker 项目联调
现在你的私有节点地址就是:https://proxy.easytake.work。
- 打开本地工具:浏览器访问你的 M4 Mac 上的
https://localhost。 - 配置代理:在设置界面找到“私有代理”或“节点地址”输入框。
- 填入地址:填入
https://proxy.easytake.work。 - 测试抓取:尝试批量添加几个公众号链接,看是否能正常解析出文章列表。
任务五:安全加固(防止域名被他人盗用流量)
因为你的域名 proxy.easytake.work 现在是公开可访问的,如果不加限制,别人也可以用你的节点。
极客建议:
利用 Cloudflare 的 WAF (防火墙) 规则:
在 Cloudflare 仪表盘进入
easytake.work站点。选择 安全性 (Security) -> WAF。
点击 创建规则:
- 名字:仅限本地使用。
- 匹配条件:如果
Referer(引用方) 不包含localhost。 - 操作:阻止 (Block)
价值:这样只有当你自己在本地电脑打开
https://localhost的工具时,你的 Worker 才会提供服务,其它地方直接访问会被拦截。
总结你的配置清单:
- 管理后台:
https://localhost(Mac 本地运行) - 私有代理:
https://proxy.easytake.work(CF 边缘运行) - 数据存储:
~/Documents/code/Docker/wechat/.data(本地持久化)
按照以上方法,很快就配置好了,没有遇到问题,因此不展开介绍了,按照教程即可,现在开始批量拉取了:
https://a1.easytake.work
https://a2.easytake.work
https://c1.easytake.work
https://l1.easytake.work
https://o1.easytake.work
文章批量抓取之后,可以选择导出。
数据下载完毕,如图所示:![]()
未来这些文章可以放在个人离线数据库当中,进行检索,可以快速搜寻信息。
配置和采集已经完毕,可是下面还有一些原理上的事情,我需要单独了解和搜索,私有代理的配置后面的原理,我需要打通环节,这是接下来的工作。
3、踩坑记录
本地部署踩坑 (M4 Mac):
1、Docker 运行方式
避坑:放弃复杂的单行
docker run命令。--rm(退出即删)与--restart always(总是重启)逻辑互斥,且 M4 网络隔离环境不适合--network host。正解:使用
docker-compose.yaml管理。- 架构适配:M4 (ARM64) 运行 x86 镜像时,确保开启 Rosetta 2 或指定
platform: linux/amd64(如果是多架构镜像则自动适配)。
- 架构适配:M4 (ARM64) 运行 x86 镜像时,确保开启 Rosetta 2 或指定
2、HTTPS 强制要求
原因:工具前端调用微信接口和剪贴板权限必须使用 HTTPS。
方案:Nginx 反向代理 +
mkcert自签名证书。关键配置:
Nginx 容器内的证书路径 (
/etc/nginx/certs) 必须与docker-compose的挂载源路径 (./certs) 严格对应。报错修复:如果 Nginx 无限重启 (Restarting),通常是
nginx.conf里的证书路径填成了宿主机的绝对路径,必须填容器内路径。
3、域名托管 (DNS Migration)
- 注册商:Porkbun -> 托管方:Cloudflare (CF)。
- 操作:修改 Nameservers 为 CF 提供的地址(如
hank/olivia)。 - DNSSEC 巨坑:迁移前必须在 Porkbun 删除/关闭 DNSSEC,否则会导致全球解析失败 (SERVFAIL)。
4、协议共存 (Hysteria vs. Worker)
Hysteria/Clash 节点 (
www/@):- 配置:A 记录指向 VPS IP。
- 状态:必须是 DNS Only (灰云)。
- 原理:Hysteria 走 UDP 协议,CF CDN (橙云) 默认不支持,开启会被拦截。
微信爬取节点 (
mp,node1)- 配置:Worker -> Settings -> Triggers -> Custom Domain。
- 状态:自动生成 Proxied (橙云)。
- 原理:利用 CF 边缘网络进行 HTTP/HTTPS 转发。
4、配置原理
核心矛盾:为什么微信文章不能直接下载?
本地工具(Localhost)想要下载微信服务器上的文章和图片,但微信设置了两道关卡:
- CORS (跨域资源共享) 限制:浏览器的安全机制。微信服务器会说:“我只允许
weixin.qq.com的网页读取我的数据,你localhost算老几?” - 防盗链 (Hotlinking Protection):微信图片服务器会检查请求头中的
Referer(来源)。如果发现来源不是微信自己,就拒绝返回图片,或者返回一张“此图片不可引用”的图。
反向代理 (Reverse Proxy)
为了破解这两道关卡,我们需要一个**“中间人”**。
配置原理:Cloudflare Worker 就是这个中间人。
工作流程:
- 你对 Worker 说:“帮我把这张图拿回来。”
- Worker 转身伪装成微信客户端(修改
Referer和User-Agent),向微信服务器要数据。 - 微信以为是自己人,交出数据。
- Worker 再把数据转交给你,并告诉你的浏览器:“这是我允许你读的”(解决 CORS)。
此代码它本质上是一个专门用来“欺骗”微信服务器的代理程序
const UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";
const PRESETS = {
mp: {
Referer: "https://mp.weixin.qq.com",
},
};
function error(msg, status = 400) {
return new Response(msg, {
status: status,
});
}
/**
* 解析请求
*/
async function parseRequest(req) {
const origin = req.headers.get("origin") || '*';
// 代理目标的请求参数
let targetURL = '';
let targetMethod = "GET";
let targetBody = '';
let targetHeaders = {};
let preset = '';
const method = req.method.toLowerCase();
if (method === "get") {
// GET
// ?url=${encodeURIComponent(https://example.com?a=b)}&method=GET&headers=${encodeURIComponent(JSON.stringify(headers))}
const {searchParams} = new URL(req.url);
if (searchParams.has("url")) {
targetURL = decodeURIComponent(searchParams.get("url"));
}
if (searchParams.has("method")) {
targetMethod = searchParams.get("method");
}
if (searchParams.has("body")) {
targetBody = decodeURIComponent(searchParams.get("body"));
}
if (searchParams.has("headers")) {
try {
targetHeaders = JSON.parse(
decodeURIComponent(searchParams.get("headers")),
);
} catch (_) {
throw new Error("headers not valid");
}
}
if (searchParams.has("preset")) {
preset = decodeURIComponent(searchParams.get("preset"));
}
} else if (method === "post") {
// POST
/**
* payload(json):
* {
* url: 'https://example.com',
* method: 'PUT',
* body: 'a=1&b=2',
* headers: {
* Cookie: 'name=root'
* },
* preset: '',
* }
*/
const payload = await req.json();
if (payload.url) {
targetURL = payload.url;
}
if (payload.method) {
targetMethod = payload.method;
}
if (payload.body) {
targetBody = payload.body;
}
if (payload.headers) {
targetHeaders = payload.headers;
}
if (payload.preset) {
preset = payload.preset;
}
} else {
throw new Error("Method not implemented");
}
if (!targetURL) {
throw new Error("URL not found");
}
if (!/^https?:\/\//.test(targetURL)) {
throw new Error("URL not valid");
}
if (targetMethod === "GET" && targetBody) {
throw new Error("GET method can't has body");
}
if (Object.prototype.toString.call(targetHeaders) !== "[object Object]") {
throw new Error("Headers not valid");
}
if (!targetHeaders["User-Agent"]) {
targetHeaders["User-Agent"] = UA;
}
// 增加预设
if (preset in PRESETS) {
Object.assign(targetHeaders, PRESETS[preset]);
}
return {
origin,
targetURL,
targetMethod,
targetBody,
targetHeaders,
};
}
/**
* 代理请求
*/
function wfetch(url, method, body, headers = {}) {
return fetch(url, {
method: method,
body: body || undefined,
headers: {
...headers,
},
});
}
export default {
async fetch(request) {
try {
const {
origin,
targetURL,
targetMethod,
targetBody,
targetHeaders,
} = await parseRequest(request);
// 代理请求
const response = await wfetch(
targetURL,
targetMethod,
targetBody,
targetHeaders,
);
return new Response(response.body, {
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Max-Age": "86400",
"Content-Type": response.headers.get("Content-Type"),
},
});
} catch (err) {
return error(err.message);
}
}
}
把前端/脚本发出的请求,伪装成“正常浏览器请求”,通过服务器转发到微信文章页,从而绕过浏览器限制与部分反爬规则。
微信文章爬取的三大经典障碍:
| 障碍 | 具体表现 |
|---|---|
| ① CORS | 浏览器 JS 不能直接 fetch mp.weixin.qq.com |
| ② Referer 校验 | 没有 Referer: https://mp.weixin.qq.com 会 403 |
| ③ UA 校验 | 非浏览器 UA 容易被限流或返回空内容 |
统一 UA(非常关键),
const UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";
意义:
- 避免
curl / node-fetch默认 UA 被微信识别 - Chrome 100 是一个非常“安全”的旧版本
- 不触发新版 Chrome 的一些指纹策略
PRESETS:为 mp.weixin.qq.com 专门做的“反爬适配层”
const PRESETS = {
mp: {
Referer: "https://mp.weixin.qq.com",
},
};
parseRequest:为“批量爬取”设计的灵活入口,统一走这一层代理.
function wfetch(url, method, body, headers = {}) {
return fetch(url, {
method,
body: body || undefined,
headers,
});
}
关键点:
- 使用 服务器 fetch
- 请求是在:
- Cloudflare Workers
- 或 Node 服务端
- 不是浏览器环境
此部分可以绕过CORS,以上代码是核心。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 kipleyarch@gmail.com