我用 Python 标准库造了个每日技术日报机器人,零依赖、零维护成本

6 阅读5分钟

我用 Python 标准库造了个每日技术日报机器人,零依赖、零维护成本

每天早上 7 点,它自动跑完,然后我打开页面,直接看到昨夜全球技术圈发生的事。没有 RSS 聚合器,没有付费订阅,没有 AI token 消耗——只有标准库 + 两个公开 API

这篇文章分享它的架构、核心代码,以及我在路上踩过的坑。


背景:为什么要造这个

我每天早上的"信息摄入仪式":打开 Hacker News → 翻 10 分钟 → 打开 GitHub Trending → 再翻 5 分钟 → 打开几个感兴趣的链接 → 半小时没了。

这 30 分钟不是阅读时间,是过滤时间。真正有价值的内容就那几条。

解决方案很简单:让机器替我过滤,我只看结果。

目标:

  • 每天自动汇总 HN Top 15 + GitHub Trending 12 个仓库
  • 生成 Markdown 日报 + 趋势分析
  • 推送到我的 GitHub Pages 网站
  • 完全不需要我盯着

架构一眼看懂

07:00 cron 触发
    │
    ▼
generator.py          ← HN Firebase API + GitHub Search API
    │   输出:posts/2026-03-25.md
    │         data/hn-stories-*.json
    │         data/github-trending-*.json
    ▼
analyzer.py           ← 读今天的 data/*.json
    │   输出:data/analysis-2026-03-25.json
    │         (热点分类、关键词频率、趋势信号)
    ▼
publish_to_github_pages.py
    │   git add + commit + push → GitHub Pages 更新
    ▼
完成,去睡觉

没有数据库,没有 Redis,没有消息队列。就是三个 Python 脚本串起来。


核心代码:数据获取

HN 数据(官方 Firebase API,免费无限制)

import urllib.request
import json

def fetch_top_stories(limit=15):
    """获取 HN Top Stories"""
    # 第一步:拿 ID 列表
    url = "https://hacker-news.firebaseio.com/v0/topstories.json"
    with urllib.request.urlopen(url, timeout=10) as r:
        story_ids = json.loads(r.read())[:limit * 2]  # 多拿一些,防止有删除的
    
    stories = []
    for sid in story_ids:
        try:
            item_url = f"https://hacker-news.firebaseio.com/v0/item/{sid}.json"
            with urllib.request.urlopen(item_url, timeout=5) as r:
                item = json.loads(r.read())
            # 只保留真实 story(有 url 的)
            if item and item.get('type') == 'story' and item.get('url'):
                stories.append({
                    'title': item['title'],
                    'url': item['url'],
                    'score': item.get('score', 0),
                    'comments': item.get('descendants', 0),
                    'by': item.get('by', ''),
                })
                if len(stories) >= limit:
                    break
        except Exception:
            continue
    
    return stories

关键细节:Firebase API 没有速率限制(只要不暴力并发),但每个 item 要单独请求。15 个 story = 16 次 HTTP 请求,加上并发保护大概 10-15 秒跑完。

GitHub Trending(用 Search API 模拟)

from datetime import datetime, timedelta

def fetch_github_trending(limit=12):
    """通过 GitHub Search API 模拟 Trending"""
    # GitHub 官方没有 Trending API
    # 用 created:>7天前 + sort=stars 近似代替
    since = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
    query = f"created:>{since}"
    
    url = (
        f"https://api.github.com/search/repositories"
        f"?q={urllib.parse.quote(query)}"
        f"&sort=stars&order=desc&per_page={limit}"
    )
    
    req = urllib.request.Request(url, headers={
        'Accept': 'application/vnd.github.v3+json',
        'User-Agent': 'content-producer/1.0'
    })
    
    with urllib.request.urlopen(req, timeout=15) as r:
        data = json.loads(r.read())
    
    repos = []
    for item in data.get('items', []):
        repos.append({
            'name': item['full_name'],
            'description': item.get('description', ''),
            'stars': item['stargazers_count'],
            'language': item.get('language', 'Unknown'),
            'url': item['html_url'],
        })
    
    return repos

注意:不带 Token 调用 GitHub API,每小时限额 60 次。日报只需调用一次,完全够用。带 Token 可以到 5000 次/小时。


趋势分析:不用 LLM 也能做分类

这是我比较得意的设计。很多人遇到"分类"问题就想到调 GPT,但其实规则 + 关键词就够了:

CATEGORY_KEYWORDS = {
    'AI/LLM': ['llm', 'gpt', 'claude', 'gemini', 'openai', 'anthropic', 
               'neural', 'model', 'inference', 'embedding', 'rag', 'agent'],
    'Security': ['vulnerability', 'cve', 'exploit', 'breach', 'hack', 
                 'malware', 'zero-day', 'patch', 'backdoor'],
    'Infrastructure': ['kubernetes', 'docker', 'terraform', 'aws', 'gcp',
                       'deployment', 'microservice', 'container', 'k8s'],
    'Web/Frontend': ['react', 'vue', 'svelte', 'typescript', 'css', 
                     'browser', 'wasm', 'webgl'],
    'Open Source': ['open source', 'github', 'release', 'fork', 'contribute'],
}

def categorize_stories(stories):
    """给每条 story 分配类别(可多分类)"""
    results = {}
    for category, keywords in CATEGORY_KEYWORDS.items():
        count = 0
        for story in stories:
            text = (story['title'] + ' ' + story.get('url', '')).lower()
            if any(kw in text for kw in keywords):
                count += 1
        if count > 0:
            results[category] = count
    
    # 按命中数排序
    return sorted(results.items(), key=lambda x: x[1], reverse=True)

输出样例:

{
  "hot_categories": [["AI/LLM", 6], ["Security", 4], ["Infrastructure", 3]],
  "top_keywords": [["rust", 5], ["llm", 4], ["docker", 3]],
  "signal": "AI/LLM dominates this cycle — 40% of top stories"
}

零 token 消耗,延迟 0ms,准确率对日报场景来说完全够用。


踩过的坑

坑 1:GitHub Actions 时区

schedule:
  - cron: '0 23 * * *'   # UTC 23:00 = 北京次日 07:00

这个我想了 10 分钟才搞清楚。GitHub Actions 的 cron 用 UTC,我想让它在北京时间每天早 7 点跑,就要减 8 小时 = UTC 前一天 23:00。

坑 2:GitHub Pages 仓库的 remote 判断

我本地用 SSH 方式克隆(git@github.com:...),但脚本里最初用了 https://github.com 来判断 remote:

# 这样不行
if 'github.com/citriac/citriac.github.io' in remote_url:
    ...

# 改成这样才对
if 'citriac.github.io' in remote_url:
    ...

SSH 格式是 git@github.com:citriac/citriac.github.io.git,字符串匹配要包容两种格式。

坑 3:HN API 偶尔返回 null

Firebase API 偶尔会对某个 item 返回 null(已删除或被标记):

with urllib.request.urlopen(item_url, timeout=5) as r:
    item = json.loads(r.read())

# 一定要判空
if item and item.get('type') == 'story':
    ...

效果

跑了一个多月,每天准时出报告:

  • HN Top 15:真实的高分 story,基本没噪音
  • GitHub Trending 12:新兴项目,经常发现没关注过的好库
  • 趋势分类:一眼看出今天是 AI 多、还是 Security 多、还是开源工具多

👉 Live 效果citriac.github.io/daily.html(… UTC 23:00 自动更新)


完整源码

开源在 GitHub:github.com/citriac/con…

不想自己搭的:我打包了一个开箱即用的版本,包含完整配置、GitHub Actions workflow、setup 文档,30 分钟内可以跑起来自己的日报站:

🚀 Daily Tech Digest Automation Kit — $15


相关工具

如果你是自由职业者或独立开发者,这些工具可能也用得上:

  • Ghost Guard — 防跑单工具包(报价单 + 催款邮件 + 验收记录)
  • Invoice Generator — 免费在线发票生成器,支持 10 种货币
  • Contract Diff — 合同对比 + 风险条款自动识别
  • Prompt Lab — 多模型 Prompt 调试工具

*代码跑起来了?欢迎 Star 一下 GitHub 仓库,或者留言聊聊你的日报方案。*PythonPython