2026-01-18

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
Pasted image 20260118161344

安装完毕,现在按照教程方法修改

创建项目文件

cd Documents/code/Docker
mkdir wechat
cd wechat
mkdir certs #管理公钥私钥
cd certs

执行以下代码生成本地 IP 对应的证书文件:

mkcert localhost 127.0.0.1 ::1
Pasted image 20260118161649

配置Docker管理文件

Pasted image 20260118161725
# 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

此时项目架构如下所示:

Pasted image 20260118161934

启动项目

docker compose up -d

关闭项目

docker compose down

项目启动之后,浏览器访问如下路径即可:

https://localhost/

Pasted image 20260118162124

2、私有代理配置(理论方法)

既然你已经拥有了域名 easytake.work,我们接下来的目标就是利用 Cloudflare 的边缘计算能力,把这个域名变成你的专属微信爬取加速节点

任务一:域名“接入” Cloudflare(这是基石)

  1. 添加站点:登录 Cloudflare 控制台,点击 Add Site,输入 easytake.work

  2. 修改 DNS 服务器

    • Cloudflare 会分配给你两个地址,类似 ns1.cloudflare.comns2.cloudflare.com
    • 操作:你需要登录你购买 easytake.work 的域名注册商后台(比如 Namecheap, GoDaddy 等),把原来的 DNS 服务器换成 Cloudflare 提供的这两个。
    • 验证:Cloudflare 首页显示 easytake.work 状态为 Active 即可。

任务二:部署 Worker 逻辑

  1. 创建 Worker:在左侧菜单选 Workers 和 Pages -> 创建应用程序 -> 创建 Worker

  2. 起名:建议起名为 wechat-proxy

  3. 注入代码

    • 点击 编辑代码
    • 将你之前贴出的那一大段 JavaScript 节点代码(包含 parseRequestwfetch 的那部分)全选覆盖掉现有的默认代码。
    • 点击 部署

任务三:绑定你的域名 easytake.work

这是最关键的一步,能让你彻底摆脱翻墙访问 workers.dev 的限制:

  1. 进入你刚才创建的 wechat-proxy Worker 详情页。
  2. 点击顶部菜单的 设置 (Settings) -> 触发器 (Triggers)
  3. 自定义域 (Custom Domains) 栏目下,点击 添加自定义域
  4. 输入:proxy.easytake.work(这会自动在你的 DNS 记录里生成一条 CNAME)。
  5. 点击确认,等待状态变为 有效 (Active)

任务四:本地 Docker 项目联调

现在你的私有节点地址就是:https://proxy.easytake.work

  1. 打开本地工具:浏览器访问你的 M4 Mac 上的 https://localhost
  2. 配置代理:在设置界面找到“私有代理”或“节点地址”输入框。
  3. 填入地址:填入 https://proxy.easytake.work
  4. 测试抓取:尝试批量添加几个公众号链接,看是否能正常解析出文章列表。

任务五:安全加固(防止域名被他人盗用流量)

因为你的域名 proxy.easytake.work 现在是公开可访问的,如果不加限制,别人也可以用你的节点。
极客建议:

利用 Cloudflare 的 WAF (防火墙) 规则:

  1. 在 Cloudflare 仪表盘进入 easytake.work 站点。

  2. 选择 安全性 (Security) -> WAF

  3. 点击 创建规则

    • 名字:仅限本地使用。
    • 匹配条件:如果 Referer (引用方) 不包含 localhost
    • 操作阻止 (Block)
  4. 价值:这样只有当你自己在本地电脑打开 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
Pasted image 20260118233347 Pasted image 20260118233426

文章批量抓取之后,可以选择导出。

Pasted image 20260118224749

数据下载完毕,如图所示:
Pasted image 20260118233117

未来这些文章可以放在个人离线数据库当中,进行检索,可以快速搜寻信息。

配置和采集已经完毕,可是下面还有一些原理上的事情,我需要单独了解和搜索,私有代理的配置后面的原理,我需要打通环节,这是接下来的工作。

3、踩坑记录

本地部署踩坑 (M4 Mac):

1、Docker 运行方式

  • 避坑:放弃复杂的单行 docker run 命令。--rm(退出即删)与 --restart always(总是重启)逻辑互斥,且 M4 网络隔离环境不适合 --network host

  • 正解:使用 docker-compose.yaml 管理。

    • 架构适配:M4 (ARM64) 运行 x86 镜像时,确保开启 Rosetta 2 或指定 platform: linux/amd64(如果是多架构镜像则自动适配)。

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)想要下载微信服务器上的文章和图片,但微信设置了两道关卡:

  1. CORS (跨域资源共享) 限制:浏览器的安全机制。微信服务器会说:“我只允许 weixin.qq.com 的网页读取我的数据,你 localhost 算老几?”
  2. 防盗链 (Hotlinking Protection):微信图片服务器会检查请求头中的 Referer(来源)。如果发现来源不是微信自己,就拒绝返回图片,或者返回一张“此图片不可引用”的图。

反向代理 (Reverse Proxy)

为了破解这两道关卡,我们需要一个**“中间人”**。

  • 配置原理:Cloudflare Worker 就是这个中间人。

  • 工作流程

    1. 你对 Worker 说:“帮我把这张图拿回来。”
    2. Worker 转身伪装成微信客户端(修改 RefererUser-Agent),向微信服务器要数据。
    3. 微信以为是自己人,交出数据。
    4. 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
Obsidian