yarb 深度解构:5 年 800+ stars 的中文安全资讯聚合机器人到底在做什么
posts posts 2026-08-03T16:42:00+08:00yarb (Yet Another Rss Bot) 用 242 + 327 行 Python 单体,撑起 5 个启用 RSS 源 + 6 个推送通道 + 5 年每日自动化的中文安全资讯聚合。本文逐层拆开。技术笔记rss, security, bot, python, open-source, architectureyarb 深度解构: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 服务 / 客户端 app | Python 单体脚本 + 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、.gitignore、LICENSE、_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.py 里 feishuBot 与 wecomBot 的实现,会发现飞书 webhook 用 msg_type: text 而企业微信 / 钉钉用 markdown 的 msgtype,Telegram 走 parse_mode='HTML'。各家 webhook 协议不完全一样,飞书自定义机器人 webhook 文档明确建议富文本走 card payload,yarb 用 text 是最简版本。
统一的频率限制器:pyrate_limiter 的 Rate(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用户使用流程:
- Fork 仓库
- 在 Settings → Secrets 加 6 个 channel 的 key(
FEISHU_KEY/WECOM_KEY/DINGTALK_KEY/QQ_KEY/TELEGRAM_KEY/MAIL_KEY) - 启用 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.opml | zer0yu/CyberSecurityRSS | 国际 CVE + Exploit + 漏洞 PoC | 启用 |
| CyberSecurityRSS-tiny.opml | 同上精简版 | 去重后的精简源 | 停用 |
| Chinese-Security-RSS.opml | zhengjim/Chinese-Security-RSS | 国内安全媒体/公众号 | 启用 |
| awesome-security-feed.opml | mrtouch93/awesome-security-feed | 国际安全博客精选 | 启用 |
| wechatRSS.opml | ttttmr/Wechat2RSS | 微信公众号 RSS 化 | 启用 |
| chinese-independent-blogs.opml | timqian/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_rss 用 listparser 解析所有 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/feed 和 blog.com/post2/feed),会被误判为重复。yarb 没处理这个边界,但中文安全圈很少有这种 RSS 结构,5 年没出 bug 是因为数据本身的约束。
6. 同构设计哲学:6 个 channel 共享同一份心智模型
读 bot.py 完整 327 行会发现,所有 6 个 channel 的代码差异只在接入端点 + payload 结构,其余全是复制粘贴:
init → parse_results (text/markdown) → send (限流 + POST)这种"同构复制"在小工具里反而是优点:
- 新加 channel 成本极低——复制一份 class,改 webhook URL 即可
- 修 bug 不会牵连——每个 channel 独立 class,改一处不破坏其他
- 频率限制抽出来——
pyrate_limiter是公共工具,各家限速策略一致 - 异步统一——
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.py 的 update_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
- 单体 Python + GitHub Actions = 极致部署简单——5 年无需服务器,只要 GitHub 不挂就稳定每日推送
- 同构复制是合理的取舍——6 个推送 channel 的代码差异只在端点 + payload,新加 channel 复制 class 即可
- 关键词黑名单是运营护城河——28 个词精准屏蔽中文互联网水文(“圆满"“喜报"“月薪”),这是懂中文互联网的人才能写出的列表
- 昨日时间窗是 RSS 推送的灵魂——
pubday == yesterday顶起"日报 vs 实时流"的本质区别 - GPL-3.0 是给"开源信仰者"的强约束——copyleft 强传染,衍生作品必须开源,跟 MIT 友好的"随便用"形成鲜明对比
11. 留给读者的 5 个问题
init_rss用listparser解析 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_runs7 天清理能省多少 Actions 配额?这个数字在 free tier 下值得关心吗?
参与讨论
使用 GitHub 登录。欢迎补充事实、异议与实践。
讨论暂时无法加载。