1. 从一份榜单到一套可复现的抓取脚本
GitHub Trending 页面每天更新,但很多人只停留在“刷一眼榜单”的层面。真正有价值的是把榜单变成结构化数据,再叠加趋势指标,比如单日星标增量、增长率、语言分布、连续上榜天数。这样你才能回答“哪个项目是真的在涨,哪个只是历史存量高”这类问题。
这篇内容面向想复现 2026 年 2 月 2 日那份热门项目趋势报告的开发者。我会把整条链路拆开:用 Python 抓取 GitHub Trending 的当日、本周、本月三个榜单,解析出项目名、描述、语言、总星标、当日新增星标,再算增长率和排名变化,最后输出一份可读的 Markdown 报告。抓取脚本本身不复杂,麻烦的是解析规则会随页面结构变化,以及多个脚本、多个模型调用之间 Key 管理混乱。所以我会把 TaoToken 作为统一的 Key/API 通道接进来,让抓取、摘要、趋势解读走同一个入口,配置只维护一份。
你需要的环境很轻:Python 3.10 以上、requests、beautifulsoup4,再加一个能发 HTTP 请求的 Key 通道。下面所有配置和命令都可以直接复制,跑完你能得到一份和当日榜单对齐的 JSON 加 Markdown 输出。
2. TaoToken 前置:统一 Key 与 API 通道
TaoToken 在这里扮演的角色是“统一入口”。抓取脚本负责拿原始 HTML,解析出结构化字段;而项目描述摘要、趋势归因、报告文案生成这些需要模型能力的部分,统一走 TaoToken 的 API。这样你不需要在脚本里散落多个厂商的 Key,也不用为每个模型单独写一套鉴权逻辑。
接入前先确认两件事。第一,你已经在控制台创建了 API Key,入口在 console 页面,创建后复制保存,后面写进配置文件。第二,确认你要调用的模型名,模型列表和对话调试可以在模型对话页面直接试。如果你后面要做长期的编码或 Agent 任务,比如让脚本自动迭代解析规则,可以看 Coding Plan,它更适合持续性的开发场景。
配置上我建议分两个文件:config.toml放通道和模型参数,settings.json放抓取目标和输出选项。这样换模型只改一处,抓取逻辑不动。下面给出骨架。
2.1 config.toml 骨架
# config.toml [channel] # 统一 Key 通道,所有模型调用走这里 base_url = "https://taotoken.net/api" api_key = "sk-你的Key" timeout = 60 [model] # 用于摘要和趋势解读的模型 name = "claude-sonnet-4-5" max_tokens = 2048 temperature = 0.3 [report] language = "zh" top_n = 10 include_weekly = true include_monthly = truebase_url用 API 地址,不要带多余路径。api_key从控制台复制,别提交到 Git。temperature设低一点,趋势解读要稳定,不要每次措辞差异太大。
2.2 settings.json 骨架
{ "targets": { "daily": "https://github.com/trending?since=daily", "weekly": "https://github.com/trending?since=weekly", "monthly": "https://github.com/trending?since=monthly" }, "headers": { "User-Agent": "Mozilla/5.0 (compatible; TrendingBot/1.0)", "Accept-Language": "en-US,en;q=0.9" }, "output": { "json_path": "./out/trending_20260202.json", "markdown_path": "./out/report_20260202.md" }, "rate_limit": { "sleep_seconds": 2, "retry": 3 } }User-Agent必须带,否则 GitHub 可能返回空内容。sleep_seconds是礼貌抓取,三个榜单之间隔 2 秒,避免触发限流。retry设 3 次,网络抖动时自动重试。
3. 可复制配置:抓取与解析脚本
抓取部分的核心是解析 Trending 页面的文章卡片。每个项目是一个article.Box-row,里面包含仓库名、描述、语言、总星标、当日新增星标。下面这段脚本把三个榜单都抓下来,输出统一结构。
# fetch_trending.py import json import time import tomllib import requests from bs4 import BeautifulSoup def load_config(): with open("config.toml", "rb") as f: return tomllib.load(f) def load_settings(): with open("settings.json", "r", encoding="utf-8") as f: return json.load(f) def parse_stars(text): # "10,794" -> 10794 if not text: return 0 text = text.strip().replace(",", "") return int(text) if text.isdigit() else 0 def parse_repo(card): repo_el = card.select_one("h2 a") if not repo_el: return None repo = repo_el.get("href", "").strip("/") desc_el = card.select_one("p") desc = desc_el.get_text(strip=True) if desc_el else "" lang_el = card.select_one("[itemprop='programmingLanguage']") lang = lang_el.get_text(strip=True) if lang_el else "Unknown" star_el = card.select_one("a[href$='/stargazers']") total_stars = parse_stars(star_el.get_text(strip=True)) if star_el else 0 today_el = card.select_one("span.d-inline-block.float-sm-right") today_stars = parse_stars(today_el.get_text(strip=True).split("stars")[0]) if today_el else 0 return { "repo": repo, "description": desc, "language": lang, "total_stars": total_stars, "today_stars": today_stars, "growth_rate": round(today_stars / total_stars * 100, 2) if total_stars else 0.0 } def fetch(url, headers, retry=3, sleep=2): for i in range(retry): try: resp = requests.get(url, headers=headers, timeout=30) if resp.status_code == 200: return resp.text except requests.RequestException: pass time.sleep(sleep) raise RuntimeError(f"抓取失败: {url}") def main(): cfg = load_config() st = load_settings() result = {} for name, url in st["targets"].items(): html = fetch(url, st["headers"], st["rate_limit"]["retry"], st["rate_limit"]["sleep_seconds"]) soup = BeautifulSoup(html, "html.parser") cards = soup.select("article.Box-row") items = [parse_repo(c) for c in cards] items = [x for x in items if x] result[name] = items[: st["output"].get("top_n", 10)] if name == "daily" else items time.sleep(st["rate_limit"]["sleep_seconds"]) with open(st["output"]["json_path"], "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"已写入 {st['output']['json_path']}") if __name__ == "__main__": main()运行命令:
python fetch_trending.py跑完你会得到out/trending_20260202.json,里面 daily、weekly、monthly 三个数组。daily 只保留前 10,weekly 和 monthly 保留全部,方便后面做交叉分析。
3.1 用 TaoToken 生成趋势解读
抓取是纯解析,不需要模型。但“为什么这个项目涨得快”“语言分布说明了什么”这类归因,交给模型更省事。下面这段把 JSON 里的 daily 榜单喂给 TaoToken,让它输出一段趋势解读,再拼进最终报告。
# summarize.py import json import tomllib import requests def load_config(): with open("config.toml", "rb") as f: return tomllib.load(f) def call_model(cfg, prompt): url = f"{cfg['channel']['base_url']}/v1/messages" headers = { "x-api-key": cfg["channel"]["api_key"], "anthropic-version": "2023-06-01", "content-type": "application/json" } payload = { "model": cfg["model"]["name"], "max_tokens": cfg["model"]["max_tokens"], "temperature": cfg["model"]["temperature"], "messages": [{"role": "user", "content": prompt}] } resp = requests.post(url, headers=headers, json=payload, timeout=cfg["channel"]["timeout"]) resp.raise_for_status() data = resp.json() return data["content"][0]["text"] def main(): cfg = load_config() with open("./out/trending_20260202.json", "r", encoding="utf-8") as f: data = json.load(f) daily = data["daily"] lines = [f"{i+1}. {x['repo']} | {x['language']} | 总星标 {x['total_stars']} | 今日 +{x['today_stars']} | 增长率 {x['growth_rate']}%" for i, x in enumerate(daily)] prompt = "以下是 GitHub Trending 当日 Top 10,请用中文写一段 200 字以内的趋势解读,重点说增长最快的项目和语言分布:\n" + "\n".join(lines) text = call_model(cfg, prompt) with open("./out/summary.md", "w", encoding="utf-8") as f: f.write(text) print(text) if __name__ == "__main__": main()运行:
python summarize.py如果返回 401,检查api_key是否复制完整;如果返回 404,检查base_url是否写成了带路径的地址。模型名要和你在模型对话里确认的一致。
4. 验证请求与成功结果
跑完两步后,先验证 JSON 结构。打开out/trending_20260202.json,daily 数组第一条应该包含repo、total_stars、today_stars、growth_rate四个关键字段。以 2026 年 2 月 2 日的数据为例,第一条是openclaw/openclaw,总星标 145670,今日新增 10794,增长率约 7.41%。如果你抓到的数字和这个量级接近,说明解析规则是对的。
再验证模型调用。out/summary.md应该有一段中文解读,提到增长最快的项目和语言分布。如果文件为空,看终端有没有抛异常。常见的是KeyError: 'content',说明返回结构和你预期的不一样,打印resp.text看原始返回。
最后做一次端到端校验:把 JSON 里的 daily 和 weekly 做交集,找出同时出现在两个榜单的项目。这类项目通常不是短期爆发,而是有持续热度。你可以用下面这段快速验证。
import json with open("./out/trending_20260202.json", "r", encoding="utf-8") as f: data = json.load(f) daily = {x["repo"] for x in data["daily"]} weekly = {x["repo"] for x in data["weekly"]} print("日榜与周榜交集:", daily & weekly)如果交集里有openclaw/openclaw这类项目,说明你的数据抓取和榜单对齐没问题。
5. 本篇常见错排查
抓取脚本最容易踩的坑是页面结构变化。GitHub Trending 的卡片类名偶尔会调整,如果article.Box-row选不到,先打印len(soup.select("article"))看有没有内容。如果返回 0,说明页面没抓到,检查User-Agent和网络。
第二个坑是星标解析。总星标和今日新增星标在页面上的位置不同,今日新增有时带 “stars today” 后缀,直接int()会报错。上面的parse_stars已经做了清洗,但如果你改了选择器,记得同步改清洗逻辑。
第三个坑是模型调用超时。timeout设 60 秒,如果网络慢可以调到 120。如果频繁超时,检查base_url是否可达,以及 Key 是否有余额。控制台里能看到用量,接入文档里有完整的错误码说明。
第四个坑是编码。Windows 下写文件如果不指定encoding="utf-8",中文会乱码。上面所有open都带了编码参数,别删。
6. 把脚本变成日常工具
这套东西跑通一次不难,难的是每天稳定跑。我的做法是把fetch_trending.py和summarize.py串成一个 shell 脚本,用 cron 定时执行,输出文件按日期命名。Key 放在环境变量里,config.toml只留占位符,避免泄露。
如果你要长期维护解析规则,或者让脚本自动适配页面变化,可以走 Coding Plan,把迭代任务交给持续的编码通道。模型调用和 Key 管理统一在 TaoToken 控制台,接入文档里有完整的参数说明。需要调试模型时,模型对话页面可以直接试 prompt,确认效果再写进脚本。
最后提醒一句:抓取频率别太高,三个榜单之间隔 2 秒是底线。GitHub 对高频请求会限流,一旦被限,重试也拿不到数据。稳定比快更重要。