侧边栏壁纸
博主头像
云端

行动起来,活在当下

  • 累计撰写 4 篇文章
  • 累计创建 1 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

自建节点订阅中转站:自动抓取、三层校验与企业微信提醒

自建节点订阅中转站:自动抓取、三层校验与企业微信提醒

一个跑在 Docker 里的 200 行 Python 服务,把「别人的文章」变成「自己的订阅地址」,并且能自己证明拿到的是最新资源。

为什么需要中转

节点分享类站点通常每天发一篇聚合文章,正文里挂着两个附件:.txt(v2ray/base64)和 .yaml(Clash 配置)。直接收藏文章链接有两个麻烦:

  • 每天要手动点进去找附件,文章多了很难找
  • 客户端订阅要求一个固定不变的 URL,文章地址却天天变

于是我做了个很薄的中转层:定时去读文章,把附件扒下来,用固定地址对外提供订阅。

架构

RSS 源  ──►  解析条目(按 pubDate 倒序)
              │
              ├─► 从正文提取 .txt / .yaml 链接
              ├─► 下载并落盘
              ├─► 三层校验
              └─► 输出固定接口 + 变化时才推送

整个服务只有一个文件、只用 Python 标准库,镜像用 python:3.12-alpine,不装任何依赖。

关键设计

这一节是整个服务的核心。三个问题必须回答清楚:拿到的是不是最新的、网络异常时会不会断流、什么时候该打扰人。

1. 三层校验:怎么知道拿到的是最新的

只依赖「feed 第一条」并不可靠,所以做了三层交叉验证。

层级判断依据能发现的问题
文章时间显式按 pubDate 倒序排序,不依赖 feed 默认顺序拿到了旧文章
链接日期从文件名解析日期(如 11.02xxx.txt、1102Clash.yaml),与文章日期比对文章是新的,附件却是旧的
内容指纹每个文件计算 SHA256 前 12 位内容到底变没变

对外暴露 /verify,三个条件同时成立才返回 ok: true,不通过时返回 HTTP 409,方便监控直接按状态码告警。

{
  "ok": true,
  "fresh": true,
  "dateMatch": true,
  "article": { "date": "2026-11-02", "ageHours": 1.3 },
  "files": [
    { "kind": "txt",  "size": 52700, "sha256": "a7c353a914ed", "linkDate": "2026-11-02" },
    { "kind": "yaml", "size": 98701, "sha256": "e814e74c04b1", "linkDate": "2026-11-02" }
  ]
}

一个容易翻车的细节:文件名格式并不统一,11.0264bagty.txt 带小数点,1102Clashiqs.yaml 不带。日期解析必须同时兼容两种,否则会误报「日期不一致」——我第一次跑就踩了这个坑,dateMatch 直接判错。

2. IPv4 优先

容器内常常没有 IPv6 路由,而 DNS 可能优先返回 AAAA 记录,导致请求先连 IPv6 然后超时,报出来只有一句没头没尾的 fetch failed。

解决办法是在进程启动时接管域名解析,强制走 IPv4:

_orig = socket.getaddrinfo
def _ipv4_first(host, port, family=0, type=0, proto=0, flags=0):
    try:
        return _orig(host, port, socket.AF_INET, type, proto, flags)
    except socket.gaierror:
        return _orig(host, port, family, type, proto, flags)
socket.getaddrinfo = _ipv4_first

3. 重试与缓存回退

下载带两次重试,间隔 1.5 秒、3 秒,偶发抖动不该让一次刷新失败。

刷新彻底失败时不报错退出,而是继续用上一次的缓存文件对外服务,同时在 meta.json 里标记 stale: true。源站挂了,订阅不能跟着挂。

4. 通知去重与失败告警

推送的前提是「文章链接 + 文件 SHA256」组成的签名发生变化。每 30 分钟刷新一次,但一天只会在真正更新时打扰你一次。

失败则相反,要「限流地吵」:连续失败 3 次才推一次企业微信,之后 6 小时内不再重复,恢复后计数清零。这样既不会被瞬时抖动惊到,也不会在真出问题时毫无声息。

接口清单

接口用途
/sub.txtv2ray 订阅(原始 base64)
/sub.plain.txt解码后的明文节点,方便肉眼检查
/sub.yamlClash 配置
/latest.json当前文章信息、更新时间、上次检查结果
/verify三层校验报告
/refresh手动立即刷新
/diag网络诊断:DNS、源站、文件站分别测

/diag 是排障时最有用的一个。它把「RSS 所在域名」和「附件所在域名」分开测试,一眼就能看出是 DNS 问题、单个域名被挡,还是容器整体没出网:

{
  "checks": {
    "dns_www":   { "ok": true, "ips": ["64.233.187.121"] },
    "feed":      { "ok": true, "ms": 1573, "bytes": 32412 },
    "file_host": { "ok": true, "ms": 335,  "bytes": 52700 }
  }
}

部署配置

目录结构

/mnt/docker/1panel/apps/node-sub/
├── app/        # 脚本落盘位置(容器重建不丢)
│   └── node_sub.py
└── data/       # 缓存、校验结果、密钥
    ├── sub.txt
    ├── sub.yaml
    ├── meta.json
    └── .wecom-webhook.url

两个目录都挂到宿主机,容器删了重建,缓存和密钥都还在。

docker-compose.yml

services:
  node-sub:
    image: python:3.12-alpine
    container_name: node-sub
    restart: always
    ports:
      - "8097:8097"
    dns:
      - 223.5.5.5
      - 114.114.114.114
    volumes:
      - /mnt/docker/1panel/apps/node-sub/data:/data
      - /mnt/docker/1panel/apps/node-sub/app:/app
    environment:
      - TZ=Asia/Shanghai
      - PYTHONUNBUFFERED=1
      - REFRESH_MINUTES=30
      - BASE_URL=http://192.168.12.4:8097
    command:
      - sh
      - -c
      - >
        mkdir -p /app /data &&
        wget -q --no-check-certificate -O /app/node_sub.py '<脚本地址>' &&
        wget -q --no-check-certificate -O /data/.wecom-webhook.url '<密钥地址>' &&
        python3 /app/node_sub.py

启动流程设计成「先拉脚本,再运行」。这样更新只需要替换远端的脚本文件、在面板点一次重建,不用进服务器手动传文件,也不需要在镜像里 bake 任何业务代码。

环境变量

变量默认值说明
PORT8097监听端口
DATA_DIR/data缓存目录
REFRESH_MINUTES30刷新间隔
MAX_AGE_HOURS48超过多久判为「不新鲜」
BASE_URL空通知里拼可点击链接用
FEED_URL内置可换成别的 RSS 源
WECOM_URL读文件企业微信机器人地址

关于 WECOM_URL 有个坑值得单独说:它必须指向机器人地址本身,不能填「存放机器人地址的那个文件」的下载链接。我一开始就这么干的,通知全部静默失败——服务照跑,日志也不报错,只是消息一条都没发出去。

部署注意

  • Alpine 自带的 BusyBox wget 不支持 --user/--password,需要认证时只能用 --header 传 Authorization,或把凭据放在 URL 里
  • 凭据不要写进文章和公开文档,配置里一律用占位符,密钥单独存文件、权限 600
  • 给容器显式配 DNS,配合代码里的 IPv4 优先,能避免大部分「明明能上网却拉不到」的怪问题

它解决的核心问题

不是「怎么下文件」,而是「怎么确信自己下的是最新的,并且在出问题时能被及时告知」。

多花的那点代码,换来的是:早上打开客户端,订阅里永远是当天最新的节点;源站改版或网络抽风时,收到的是一条告警,而不是一个空订阅。

0

评论区