跳到正文

目录

yarb 深度解构:5 年 800+ stars 的中文安全资讯聚合机器人到底在做什么

yarb 深度解构:5 年 800+ stars 的中文安全资讯聚合机器人到底在做什么

仓库:github.com/Vu1nT0tal/yarb(README 内 clone URL 指向 VulnTotal-Team/yarb) 代码量:yarb.py 242 行 + bot.py 327 行 + utils.py ≈ 600 行 Python 单体 维护方:VulnTotal安全 / Vu1nT0tal 最新协议:GPL-3.0 本文完成时:800+ stars(当前 824)· 5 年持续运行(2022-04-07 创建,archive 目录横跨 2022-2026)

1. 一句话定位:RSS 聚合 + 多通道推送的中文安全资讯日报

yarb 是 Yet Another Rss Bot 的递归缩写。它做一件事:每日定时抓 5 个启用的 RSS 源、用关键词过滤后,把昨日发布的文章按 feed 分组推送到 6 个通道

读 README 第一段就知道它不是又一个"通用 RSS 阅读器":

一个方便获取每日安全资讯的爬虫和推送程序。支持导入 opml 文件,因此也可以订阅其他任何 RSS 源。

两个核心动作: + 。中间夹一层过滤(28 个关键词黑名单,如"招聘"“开班"“新冠"“年薪”)。

跟同类项目比,yarb 的差别是:

维度通用 RSS 阅读器yarb
部署形态Web 服务 / 客户端 appPython 单体脚本 + GitHub Actions
触发方式手动刷新 / WebSocket每日定时(cron 0 2 * * * UTC = 北京时间 10:00)
输出通道Web / App 通知6 个即时通讯通道(邮件 + 飞书 + 企业微信 + 钉钉 + QQ + Telegram)
数据归属第三方服务上云本地归档 archive/YYYY/YYYY-MM-DD.md + today.md,5 年不断档
协议MIT/Apache 居多GPL-3.0(copyleft,衍生作品必须开源)
时间窗口任意仅昨日文章(pubday == yesterday)

通用 RSS 阅读器是"你想看的时候去看”。yarb 是"早上 10 点整,群里准时弹一份昨日安全要闻”。

2. 仓库结构:一个 Python 单体的克制

1607 个文件,只有 ~600 行 Python 真代码,其余是 archive/ 5 年历史资讯 + rss/ 订阅源 opml + today.md 当日归档:

yarb/
├── yarb.py              ← 主入口 CLI(242 行)
├── bot.py               ← 6 通道推送机器人(327 行)
├── utils.py             ← 工具函数(rich + Pattern)
├── config.json          ← 配置(代理 + 7 RSS + 6 通道 + 28 关键词)
├── requirements.txt     ← 10 个 PyPI 依赖 + 1 个 git 源(listparser)
├── install.sh           ← pip install + go-cqhttp 下载
│
├── .github/workflows/
│   └── action.yml       ← GitHub Actions(cron + del_runs)
│
├── rss/                 ← 7 个 OPML 订阅源(约 640 KB)
│   ├── CustomRSS.opml
│   ├── CyberSecurityRSS.opml
│   ├── CyberSecurityRSS-tiny.opml
│   ├── Chinese-Security-RSS.opml
│   ├── awesome-security-feed.opml
│   ├── wechatRSS.opml
│   └── chinese-independent-blogs.opml
│
├── archive/             ← 5 年历史资讯(2022/2023/2024/2025/2026)
├── today.md             ← 当日归档
└── cqhttp/              ← QQ 机器人 go-cqhttp 集成

仓库根 README.md、.gitignoreLICENSE_config.yml(Jekyll 配置,可能是历史博客部署残留)。

3. 主入口 yarb.py 242 行:5 段流水线

yarb.py 把整个工作流拆成 5 段,完全照着 README 的 usage 走:

argparse 解析 CLI 参数
    ↓
init_rss(conf)  ─── 7 个 RSS 源,按 enabled 过滤,合并去重
    ↓
parseThread × 100  ── ThreadPoolExecutor(100) 并发抓 feed
    ↓
update_today(results)  ── 写 today.md + archive/YYYY/YYYY-MM-DD.md
    ↓
init_bot(conf) + bot.send(parse_results)  ── 6 通道推送

3.1 关键代码段(原文引用 + 注释)

100 线程并发抓取:

with ThreadPoolExecutor(100) as executor:
    tasks.extend(executor.submit(parseThread, conf['keywords'], url, proxy_rss) for url in feeds)
    for task in as_completed(tasks):
        title, result = task.result()

昨日文章过滤:

yesterday = datetime.date.today() + datetime.timedelta(-1)
pubday = datetime.date(d[0], d[1], d[2])
if pubday == yesterday and filter(entry.title):
    item = {entry.title: entry.link}

关键词黑名单(28 项):

"exclude": [
    "招聘", "招生", "开班", "报名", "双非", "倒计时", "圆满", "抽奖",
    "好礼", "福利", "领取", "火热", "热烈", "欢迎", "美女",
    "喜报", "喜讯", "惊喜", "恭喜", "签约", "快乐", "颜值",
    "新冠", "疫情", "疫苗", "核酸",
    "月薪", "年薪"
]

这份黑名单读起来像中文互联网梗百科。“圆满"“抽奖"“喜报"“月薪” 都在过滤列表里,说明运营者很清楚哪些是营销水文、哪些是技术干货。

3.2 时间窗的工程哲学:为什么是「昨日」而不是「当日」?

pubday == yesterday 这一行决定了 yarb 是"日报"而不是"实时流”。背后有三层考虑:

第一层:时区对齐yesterday = datetime.date.today() + timedelta(-1) 取的是服务器本地日期,而 entry.published_parsed 是 RFC 2822 的 GMT 时间戳,pubday 由它换算成 UTC 日期。在 UTC+8 的服务器上按「当日」过滤,会把北京时间 0:00-7:59 发布、但 UTC 日期还是昨天的文章漏掉;按「昨日」过滤则把它们兜进来——这正是早上 10 点看早报的用户最关心的窗口。

第二层:去重逻辑。RSS 聚合的痛点是同一篇文章被多个源转发(比如 CVE 公告同时出现在 SecurityWeek 和 Packet Storm)。yarb 不靠去重,靠时间窗——任何源在「昨日」发布的新文章都收,内容重复由用户肉眼识别。这跟 Twitter timeline、微博热搜的"时间窗 = 内容"思路一致。

第三层:日报节奏。GitHub Actions cron 0 2 * * * UTC = 北京时间 10:00,这个点用户打开群消息,看到的恰好是"昨天一天的安全要闻”。时间窗跟推送时间耦合在一起。

——pubday == yesterday 一行,顶起了"日报 vs 实时流"的本质区别。

3.3 推送机器人工厂:6 个 class 同构

bot.py 327 行里,6 个推送 channel 是同构的 class,每个 class 三个方法:

class feishuBot:
    def __init__(self, key, proxy_url='')          # 构造
    @staticmethod
    def parse_results(results: list)               # 渲染(text/markdown)
    async def send(self, text_list: list)          # 推送(含限流)

bot.pyfeishuBotwecomBot 的实现,会发现飞书 webhook 用 msg_type: text 而企业微信 / 钉钉用 markdownmsgtype,Telegram 走 parse_mode='HTML'。各家 webhook 协议不完全一样,飞书自定义机器人 webhook 文档明确建议富文本走 card payload,yarb 用 text 是最简版本。

统一的频率限制器:pyrate_limiterRate(20, Duration.MINUTE) + InMemoryBucket + Limiter,每条推送前 limiter.try_acquire('identity')。飞书和邮件不设限,其余 4 家(企业微信 / 钉钉 / QQ / Telegram)都套了这层 20 条/分钟的兜底——即便各平台并不强制,作者也做了"宁滥勿缺"的防御。

3.4 真实的推送样本(today.md 当日归档)

today.md 是 GitHub Actions 自动 commit 回来的当日归档,撰写时点的样本是 82 行、涵盖 11 个 feed:

  • Recent Commits to cve:main(CVE 实时更新)
  • CXSECURITY Database RSS Feed(MODX TLS cookie / Linux Kernel Use-After-Free)
  • Sukka’s Blog / SecWiki News / 博客园 / GT’s Blog / Taxodium
  • Reverse Engineering(ML debugger detection / Ghidra decompilation layer)
  • 奇客Solidot / 黑海洋Wiki / 黑鸟 / 安全圈 / 青衣十三楼飞花堂 / 极客公园

一份 11 个 feed、~50 篇文章的中文安全/技术早报,覆盖 CVE 公告、独立博客、安全公众号、安全媒体、技术社区 5 大类。

4. GitHub Actions:用 fork 即可部署的极致简单

.github/workflows/action.yml 把整个部署抽象成 3 步:

on:
  schedule:
    - cron: '0 2 * * *'   # UTC 02:00 = 北京时间 10:00
  workflow_dispatch:        # 手动触发

jobs:
  build:
    steps:
      - ./install.sh                    # pip + go-cqhttp
      - python3 yarb.py                 # 跑抓取 + 推送
      - git commit "每日安全资讯(`date +'%Y-%m-%d'`)"
      - git push                        # archive + today.md 自动归档
  del_runs:                               # 7 天前 workflow run 自动清理
    steps:
      - uses: Mattraks/delete-workflow-runs@v2

用户使用流程:

  1. Fork 仓库
  2. 在 Settings → Secrets 加 6 个 channel 的 key(FEISHU_KEY / WECOM_KEY / DINGTALK_KEY / QQ_KEY / TELEGRAM_KEY / MAIL_KEY)
  3. 启用 Actions

零代码、零服务器、零运维成本,只要 GitHub 不挂就稳定每日推送。Action 末尾还有 del_runs 清理 7 天前 workflow run,防止 Actions 配额被历史日志吃光。

5. 7 个 OPML 订阅源的安全纵深

rss/ 目录下 7 个 OPML 文件,合计约 640 KB。config.json 里按 enabled 字段决定抓哪些,默认启用 5 个、停用 2 个。读每个文件头能看出定位:

OPML来源定位默认
CustomRSS.opml自维护奇安信攻防社区启用
CyberSecurityRSS.opmlzer0yu/CyberSecurityRSS国际 CVE + Exploit + 漏洞 PoC启用
CyberSecurityRSS-tiny.opml同上精简版去重后的精简源停用
Chinese-Security-RSS.opmlzhengjim/Chinese-Security-RSS国内安全媒体/公众号启用
awesome-security-feed.opmlmrtouch93/awesome-security-feed国际安全博客精选启用
wechatRSS.opmlttttmr/Wechat2RSS微信公众号 RSS 化启用
chinese-independent-blogs.opmltimqian/chinese-independent-blogs中文独立博客停用

覆盖面从 CVE/漏洞 PoC 到中文安全媒体、微信公众号、独立博客,纵深 5 大类。对一个单体 Python 脚本来说足够全面。

5.1 实战:加一个新 RSS 源

加 RSS 源有两条路径,任选其一:

路径 A:在 config.json 加远程仓库条目

{
  "rss": {
    "MyCustomRSS": {
      "enabled": true,
      "url": "https://raw.githubusercontent.com/yourname/your-feed/master/feed.opml",
      "filename": "MyCustomRSS.opml"
    }
  }
}

url 是远程 OPML 文件地址,yarb.py--update 时会把它下载到 rss/MyCustomRSS.opml,然后下次跑 cron 自动被合并到 feeds 列表。

路径 B:直接编辑 rss/CustomRSS.opml

<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head><title>CustomRSS</title></head>
<body>
<outline type="rss" xmlUrl="https://forum.butian.net/Rss" text="奇安信攻防社区" title="奇安信攻防社区" htmlUrl="https://forum.butian.net" />
<outline type="rss" xmlUrl="https://example.com/feed.xml" text="我的新源" title="我的新源" htmlUrl="https://example.com" />
</body>
</opml>

CustomRSS.opml 是「本地文件」型源,yarb 默认不会去下载它,直接读本地。适合你自己维护的不变源清单

init_rsslistparser 解析所有 OPML,然后按 URL 短名合并去重:

url = feed.url.strip().rstrip('/')
short_url = url.split('://')[-1].split('www.')[-1]
check = [feed for feed in feeds if short_url in feed]
if not check:
    feeds.append(url)

——这条去重逻辑有个:如果两个源用同一域名但不同路径(比如 blog.com/post1/feedblog.com/post2/feed),会被误判为重复。yarb 没处理这个边界,但中文安全圈很少有这种 RSS 结构,5 年没出 bug 是因为数据本身的约束。

6. 同构设计哲学:6 个 channel 共享同一份心智模型

bot.py 完整 327 行会发现,所有 6 个 channel 的代码差异只在接入端点 + payload 结构,其余全是复制粘贴:

init   →   parse_results (text/markdown)   →   send (限流 + POST)

这种"同构复制"在小工具里反而是优点:

  1. 新加 channel 成本极低——复制一份 class,改 webhook URL 即可
  2. 修 bug 不会牵连——每个 channel 独立 class,改一处不破坏其他
  3. 频率限制抽出来——pyrate_limiter 是公共工具,各家限速策略一致
  4. 异步统一——async def send,全部走 asyncio.run() 调度

对比很多 RSS 项目用配置文件 YAML 描述 webhook,yarb 选择写死 Python class。协议简单到配置文件反而冗余。

7. 5 年稳定性:从 archive 目录看过来

archive/ 下 5 个年份目录(2022/2023/2024/2025/2026),README.md 提供导航。读最早的 archive/2022-07-25.md:

# 每日安全资讯(2022-07-25)

- HackerOne Hacker Activity
  - [CVE-2022-27781: CERTINFO never-ending busy-loop](...)
- CXSECURITY Database RSS Feed - CXSecurity.com
  - [Moqui Framework 2.1.3 - Reflected Cross Site Scripting](...)
- Twitter @Nicolas Krassas
  - [RT Daniel Küffer: Day 33 of 365 ...]
- 嘶吼 RoarTalk
  - [从窃取cookie到BEC:攻击者使用AiTM钓鱼网站作为进一步财务欺诈的入口](...)

5 年前的早报结构跟 2026-08-03 当天的 today.md 完全一致:feed 名 + 文章列表 + 链接。说明 yarb.pyupdate_today() 函数核心逻辑从 2022 年到现在没改过。

2022-12-22.md 里出现”Senayan Library Management System 9.2.2 SQL Injection"。SQL 注入、安全公告、RSS 推送,这三件事 5 年没变。

8. 实战:5 分钟本地跑起来

# 1. clone + 装依赖
git clone https://github.com/Vu1nT0tal/yarb.git
cd yarb
./install.sh
# install.sh 内部:
#   python3 -m pip install -r requirements.txt
#   下载 go-cqhttp 二进制到 cqhttp/(用于 QQ 通道)

# 2. 改 config.json:把代理和启用的 channel 写好
# 默认 config.json 里所有 channel 都是 enabled: false
# 你只需要把要启用的 channel 改成 enabled: true,并填 key
{
  "proxy": {
    "url": "http://127.0.0.1:7890",
    "rss": true,
    "bot": true
  },
  "bot": {
    "feishu": {
      "enabled": true,
      "secrets": "FEISHU_KEY",
      "key": "your-feishu-webhook-uuid-here"
    }
  }
}
# 3. 先用 --test 跑一遍(不抓真文章,只推 19 条假数据)
python3 yarb.py --test
# 你应该能在飞书群里收到 [ test1 / test2 / ... test19 ] 共 19 条假消息

# 4. 跑一次真实抓取(单次,不进 cron 循环)
python3 yarb.py

# 5. 跑 cron 循环(每天 11:00 跑)
nohup python3 yarb.py --cron "11:00" > run.log 2>&1 &

第 3 步的 --test 是最容易踩的坑:它不抓真 RSS(避免污染 today.md),只推 19 条假数据验证通道可达。生产前必跑。

9. 实战:自己加一个 SlackBot

按同构设计哲学,加一个新 channel 是复制粘贴级别的工作量。bot.py 里加 slackBot class:

class slackBot:
    """Slack webhook 机器人
    https://api.slack.com/messaging/webhooks
    """

    def __init__(self, key, proxy_url='') -> None:
        self.key = key
        self.proxy = {'http': proxy_url, 'https': proxy_url} if proxy_url else {
            'http': None, 'https': None}

    @staticmethod
    def parse_results(results: list):
        text_list = []
        for result in results:
            (feed, value), = result.items()
            text = f'*{feed}*\n'
            for title, link in value.items():
                text += f'• <{link}|{title}>\n'
            text_list.append(text.strip())
        return text_list

    async def send(self, text_list: list):
        rates = [Rate(20, Duration.MINUTE)]
        bucket = InMemoryBucket(rates)
        limiter = Limiter(bucket, max_delay=Duration.MINUTE.value)

        for text in text_list:
            limiter.try_acquire('identity')
            data = {"text": text}
            headers = {'Content-Type': 'application/json'}
            url = f'https://hooks.slack.com/services/{self.key}'
            r = requests.post(url=url, headers=headers,
                              data=json.dumps(data), proxies=self.proxy)

            if r.status_code == 200:
                console.print('[+] slackBot 发送成功', style='bold green')
            else:
                console.print('[-] slackBot 发送失败', style='bold red')
                print(r.text)

然后在 config.json 加一个 channel 条目:

"bot": {
  "slack": {
    "enabled": true,
    "secrets": "SLACK_KEY",
    "key": "T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
  }
}

__all__ 列表加 "slackBot",init_bot() 的 elif 分支加一行:

elif name == 'slack':
    bot = globals()[f'{name}Bot'](key, proxy_url)
    bots.append(bot)

总共新增/改动 ~40 行 Python。这是 yarb 同构设计的核心红利:协议简单到加 channel 是配置 + 复制粘贴

10. 给工程师的 takeaway

  1. 单体 Python + GitHub Actions = 极致部署简单——5 年无需服务器,只要 GitHub 不挂就稳定每日推送
  2. 同构复制是合理的取舍——6 个推送 channel 的代码差异只在端点 + payload,新加 channel 复制 class 即可
  3. 关键词黑名单是运营护城河——28 个词精准屏蔽中文互联网水文(“圆满"“喜报"“月薪”),这是懂中文互联网的人才能写出的列表
  4. 昨日时间窗是 RSS 推送的灵魂——pubday == yesterday 顶起"日报 vs 实时流"的本质区别
  5. GPL-3.0 是给"开源信仰者"的强约束——copyleft 强传染,衍生作品必须开源,跟 MIT 友好的"随便用"形成鲜明对比

11. 留给读者的 5 个问题

  • init_rsslistparser 解析 OPML,合并去重按 URL 短名匹配,如果有 feed 换域名会怎样?
  • 关键词过滤是简单的 if i in title,没考虑子串误伤(比如"月薪"会过滤掉"月薪过万的技术总监”)——这是 by design 还是 bug?
  • today.md 自动 commit + push,5 年下来累计几千次 commit,会触发 GitHub Actions 的 push 触发吗?(看 action.yml 没有 on: push,应该不会)
  • QQ 机器人用 go-cqhttp 1.0.0-rc2 是 2022 年的版本,5 年没升级,会不会因 QQ 协议变更而失效?
  • del_runs 7 天清理能省多少 Actions 配额?这个数字在 free tier 下值得关心吗?

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。