定时检查 chinesehuazhou/python-weekly 是否发布了新一期周刊,把其中的文章和开源项目标题补上真实链接, 去掉订阅广告等无关内容,再通过 Telegram 机器人推送给你。
原始周刊只有标题文本、没有链接,所以链接由脚本自动检索补全: 项目走 GitHub API,文章同时问 Hacker News 和 serper.dev,Tavily 只在前两者都不够可靠时才出场。
samples/2026-08-01-weekly-resolved.md 是第 161 期人工核对过的结果,可以对照参考。
📰 Python 潮流周刊 #161
Claude Code 工作原理
2026-08-01 · 12 篇文章 · 12 个开源项目
🦄 文章 & 教程
1. PyPI 界面更新计划 ← 超链接
2. 用 bisect 在 Python 中做二分查找
...
🐿 项目 & 资源
1. vnpy
开源量化交易平台
...
置信度低的链接后面会带 ⚠️,完全没找到的条目会附一个 🔍 搜索链接,方便你自己确认。
- 用 GitHub Contents API 列出
docs/en(或docs)下的所有YYYY-MM-DD-weekly.md,取最新一期。 - 与上次推送记录比对,没有更新就直接退出。
- 下载 Markdown,解析出 ①②③… 编号的条目,按 🦄 / 🐿 两个小标题分成文章与项目; 列表结束后的第一段普通文字之后的内容(订阅广告、往期合集链接等)全部丢弃。
- 逐条补链接,结果按期缓存,避免重试时重复消耗检索配额。
- 拼成 HTML 消息,超过长度自动分条发送,最后记录本期已推送。
-
在 val.town 新建一个 Val,按下面的结构创建文件:
main.ts src/config.ts src/errors.ts src/github.ts src/http.ts src/parser.ts src/resolver.ts src/schedule.ts src/store.ts src/telegram.ts src/types.ts -
在
main.ts的编辑器右上角点+→ 选择 CRON,把调度设为0 */3 * * *(每 3 小时,UTC)。 实际发起请求的次数远小于这个频率,原因见下面的「检查频率」。 -
在 Val 左侧栏的 Environment Variables 里填入下面的变量。
-
先把
DRY_RUN设为1点一次 Run,在日志里确认消息内容正确,再删掉这个变量正式启用。
一共只涉及三个平台的凭据:Telegram(必填)、GitHub(建议)、serper.dev(建议)。 剩下的都是开关和调参,不用注册任何账号。
| 变量 | 必填 | 说明 |
|---|---|---|
TELEGRAM_BOT_TOKEN | 是 | 找 @BotFather 发 /newbot 创建机器人后得到 |
TELEGRAM_CHAT_ID | 是 | 先给机器人发一条消息,再访问 https://api.telegram.org/bot<TOKEN>/getUpdates,取 message.chat.id |
GITHUB_TOKEN | 强烈建议 | 任意 classic token(无需勾选权限)。有 token 时所有项目用一次 GraphQL 查完,否则要按 6.5 秒/次限速走 REST |
SERPER_API_KEY | 建议 | serper.dev 注册即得,送 2500 次、不用绑卡。主力搜索渠道 |
TAVILY_API_KEY | 否 | tavily.com 注册即得,每月 1000 次、不用绑卡。只在 Serper 没结果时才用 |
LINK_OVERRIDES | 否 | JSON 形式的人工链接,见下文「实在搜不到的条目」 |
WEEKLY_LANG | 否 | zh(默认)或 en。中文版展示中文标题,链接仍用英文标题检索 |
ERROR_NOTIFY | 否 | 默认开启;设为 0 关闭出错时的 Telegram 告警 |
DRY_RUN | 否 | 设为 1 时只打印消息、不发送、不记录已推送 |
FORCE_RESEND | 否 | 设为 1 时忽略「已推送」记录和链接缓存,把最新一期从头查一遍再推 |
RESOLVE_BUDGET_MS | 否 | 单次运行用于检索链接的毫秒预算,默认 35000 |
QUIET_DAYS | 否 | 收到一期后静默多少天再开始查下一期,默认 6 |
文章链接分两波:Hacker News 和 Serper 同时查,取更好的那个; 只有这一波都不够可靠时才问
Tavily。PEP 标题仍直接拼 peps.python.org,不走搜索。
| 渠道 | 是否需要 Key | 特点 |
|---|---|---|
| PEP 直链 | 否 | 标题里出现 PEP 842 就直接拼 peps.python.org,零成本且必定正确 |
| Hacker News(Algolia) | 否 | 免费无限制,返回的就是原文 URL,准确率最高;缺点是只覆盖上过 HN 的内容 |
| serper.dev | 是(免费送 2500 次,不绑卡) | 拿到的就是 Google 的结果,覆盖最全,主力就是它 |
| tavily.com | 是(每月 1000 次,不绑卡)可选 | 只在前面都没结果时才调用,用来兜 Serper 失效的情况 |
两个 Key 都不配也能跑,但那样只剩 Hacker News,没上过 HN 的文章会直接查不到链接 (日志里会写明
No SERPER_API_KEY or TAVILY_API_KEY: ...)。 如果你在意每期都尽量补全,SERPER_API_KEY
基本是必配的;TAVILY_API_KEY 纯属保险, 不想多注册一个账号就别配,配了也不会改变正常情况下的行为。
用量其实很小:每期文章约 12 次 Serper 查询(和 HN 并行,不再等 HN 先失败), 项目那 12 条走 GitHub API。一年撑死几百次,注册送的 2500 次够用好几年。
原先还有一路抓 https://html.duckduckgo.com/html/ 的无 Key 兜底,已经删掉了。
它现在对机器人一律返回 202 + 验证码页面而不是搜索结果,云服务器的 IP 必中,
实测连查三个词全部被挡。它最糟的地方不是没用,而是失败得很安静—— 验证码页面也是
2xx,看起来就像「这篇文章哪儿都搜不到」。
替代品挑下来只有 Tavily 合适:免费额度每月 1000 次、每月 1 号重置、注册不用绑卡, 而且是正经 API,配额用尽会返回 429 而不是假装没搜到,脚本能识别并告警。 其他几个都被排除了:Brave 官方 FAQ 明说免费档也要绑卡(网上不少文章写「不用绑卡」,是错的); Exa 只送一次性 $10 额度,用完就断;自建 SearXNG / UnSearch 这类要自己维护服务, 为一个每周跑一次的脚本不划算。
顺带说一句,我也查过有没有「不用搜索引擎」的路子,结论是没有: 中文版周刊同样只有标题、没有文章链接;作者的 Substack 里 20 期只有 1 期带外链,其余都被剥掉了; Telegram 频道只有图片;PyCoders Weekly 存档虽然有链接,但选题只跟周刊部分重合, 这次出问题的两篇恰好都不在里面。所以搜索是绕不开的。
- 打开 serper.dev,用 Google 账号或邮箱注册(不需要信用卡)。
- 登录后在 Dashboard 的 API Key 一栏直接就能看到 key,复制出来。
- 在 Val Town 的 Environment Variables 里加
SERPER_API_KEY。 - 把
DRY_RUN设成1手动 Run 一次,日志里看到链接补全就说明生效了。
想同时配上兜底的话,tavily.com 流程一样:邮箱注册、 Dashboard 复制
Key、存成 TAVILY_API_KEY。它只在 Serper 没给出结果时才会被调用。
如果你在 Google Cloud 控制台里翻遍了也找不到 Custom Search API,不是你的问题: Google 在 2025 年就关闭了 Custom Search JSON API 的新用户注册, 官方文档上写着 "This API is not available for new customers",并且整个服务在 2027-01-01 停服。 配套的 Programmable Search Engine 也在收紧,免费引擎被限制到 50 个域名、 「搜索整个网络」选项正在取消。官方给的替代品 Vertex AI Search 是面向企业的产品, 起步门槛和计费方式都不适合这个脚本。
Brave Search API 原来的 2000 次/月免费档在 2025 年底取消了,现在改成每月送 $5 额度(约 1000 次), 且注册必须绑卡——这一点以官方 FAQ 为准("the card is only used to confirm your identity"), 网上不少对比文章写「Brave 免费档不用绑卡」是过时的。额度对我们够用, 但为一个每周跑一次的脚本多绑一张卡不划算,所以兜底选了 Tavily。
这两家的代码和环境变量都已经从仓库里删掉了。真要加回来的话, 照着 src/resolver.ts 里的
serperSearch 写一个同样形状的函数、 在 providers() 里插一行就行,每家约十行。
LINK_OVERRIDES 接受一个 JSON,键是条目标题(或标题里任意一段能唯一区分的文字),值是链接。
命中的条目会跳过全部检索直接用你给的地址:
{ "How Claude Code Works": "https://nem035.com/thoughts/how-claude-code-works", "Wheels, Bottles, and Images": "https://nesbitt.io/2026/07/30/wheels-bottles-images.html", "SQLite vs DuckDB": "https://tracewayapp.com/blog/sqlite-vs-duckdb" }
改完环境变量再手动 Run 一次即可,不用改代码,也不用清缓存。
想在部署前先看看某个标题能不能查到,本地跑:
deno task live-check "Wheels, Bottles, and Images"
它会走完整的渠道链并打印命中的渠道、置信度和链接,方便判断是该配 Key 还是该加 override。
周刊每周六更新,真正需要盯着的只有周六前后那一天多。所以脚本没有靠 cron 表达式去 猜发布时间,而是自己判断「现在该不该查」:
- 记住上一期的发布日期(文件名里的
2026-08-01)。 - 距离这个日期不满
QUIET_DAYS(默认 6 天)时,直接返回,一个网络请求都不发。 - 到第 6 天(也就是周五)起进入「等待期」,此后每次定时器触发都会去 GitHub 看一眼。
按每 3 小时一次算,一周 56 次触发里约有 48 次是毫秒级空转,只有周五到周六的 8 次左右会真的查 GitHub。新一期发布后最迟 3 小时内送达。
这样做的另一个好处是自动适应变化:某期拖到周日才发,静默期就从那个周日重新开始算, 不用改 cron;连着停更两周也只是维持每 3 小时看一眼的节奏。
想更快可以把 cron 改成 0 */2 * * *,想更省就调大 QUIET_DAYS——两者互不影响。
失败分两层处理。一次运行内,所有对外请求(GitHub、各搜索渠道、Telegram)都走 src/http.ts 的
fetchWithRetry:遇到超时、网络异常、429、5xx 会退避重试,服务端给了 Retry-After
就按它说的等。三类请求的重试次数和超时按重要性区分:
| 请求 | 重试次数 | 单次超时 | 理由 |
|---|---|---|---|
| Telegram 发送 | 4 | 15s | 内容都备好了,这一步失败最可惜 |
| GitHub | 3 | 15s | 拿不到原文就没有后续 |
| 搜索渠道 | 2 | 6s | 一个渠道卡住不如早点换下一个 |
跨运行这层靠的是「查完才记录已推送」:只要没成功发出去,last-sent 就不变,
下一次定时运行会重新走一遍。链接检索的中间结果存在 Blob 里,重来时不会重复查已经查到的条目;
单个条目连续查不到会累计失败次数,超过上限就放弃它,不拖累整期。
配合上面的静默窗口,失败后的重试节奏正好是每 3 小时一次,直到成功或你收到告警。
缓存是「查到了就不再查」,这在正常情况下省时省配额,但也意味着一个查错的链接会一直错下去: 条目已经有 URL(或连续失败达到上限)就算「定案」,后续运行直接跳过它。所以有两个失效机制:
- 缓存键里带了
src/../main.ts的RESOLVER_VERSION。改动打分逻辑时把它加一, 旧结果自动作废,已经推过的那期也会用新规则重查一遍。 FORCE_RESEND=1会同时忽略「已推送」记录和链接缓存,等于手动重来。
换句话说,如果某期的链接不对,光改代码不够,还得让缓存失效,否则跑多少次都是同一个结果。
Val Town 免费版单次运行上限是 1 分钟,这一分钟基本决定了脚本的所有设计取舍。 时间都花在等网络上,所以优化的方向是别让请求排队等:
- 条目并发检索:24 个条目分 6 路同时查(
src/resolver.ts的CONCURRENCY), 每条内部再把 Hacker News 和 Serper 同时发出去,墙钟时间是最慢的那一次而不是相加。 - 项目批量查:配了
GITHUB_TOKEN时,12 个项目用一次 GraphQL 全部查完, 而不是 12 次 REST 请求。 - REST 单独一条道:没有 token 时项目只能走 REST,而 REST 搜索被限速到每 6.5 秒一次。 这类请求被放进独立的串行队列,不占并发池的位置,免得几个慢请求把文章检索堵死。
- Tavily 等第一波结束:Hacker News 和 Serper 已经给出可靠结果时不消耗 Tavily 配额。
- 每个请求都有超时:
fetchWithRetry给每次尝试挂了AbortSignal.timeout(搜索 6 秒、其余 15 秒)。fetch本身没有超时,少了这层,一个不响应的站点 就能把整分钟耗光然后被平台强杀。 - 检索预算:默认 35 秒(
RESOLVE_BUDGET_MS),前后要留给 GitHub 请求、 Telegram 发送和重试。预算不仅在每个条目开始前检查,渠道之间也检查, 这样单个条目再慢也不会独自把整个运行拖爆。
预算用完时脚本会保存已查到的链接、本次不发送,由下一次定时运行接着查,全部查完才推送。 配好
GITHUB_TOKEN 和 SERPER_API_KEY 的情况下,一次运行通常十几秒就跑完了。
万一没跑完,会等到下一次触发(最多 3 小时)才续上,介意的话可以把 cron 调密一点。
deno task test # 跑解析与分条的单元测试 deno task dry-run # 走完整流程但只打印消息 deno task send # 真正发送一次
本地运行时状态写到 ./.state/;部署在 Val Town 时自动改用 Blob 存储。
- 文章:Hacker News(全文 + 缩短查询)和 Serper(标题里没有 Python 时自动补上)同时发出,
取打分最高的结果。只有这一波都不够可靠时才问 Tavily,并补一次不带
python的原文标题。 判为「可靠」要同时满足:相似度 ≥ 0.6,且覆盖了周刊标题里至少 60% 的关键词。只满足前者标 ⚠️, 都不满足就不给链接只留 🔍。 Hacker News 讨论页、Lobsters、Reddit、Pinterest、GitHub 的 PR/Issue/Commit 页会从结果里排除(HN 渠道用的是 story 里的原文 URL,不是讨论页)。 - 相似度用的是两个标题分词后的 F1(同时看「查询词被覆盖了多少」和「候选标题里有多少是多余的」)。 只看覆盖率是不够的:一条很长的无关标题很容易凑齐短标题里的大部分词, 比如 "Show HN: I made a cheaper alternative to Claude Code or Codex CLI" 对 "How Claude Code Works, From Tokens to Agents" 的覆盖率有 0.4,用 F1 算只有 0.29。
- 但只看 F1 也不够,它会偏袒「标题短而干净」的候选:
DuckDB vs. SQLite对SQLite vs DuckDB on the Same $16 Box能拿到 0.67,可它丢掉了same / 16 / box这几个真正区分文章的词,其实是另一篇。 所以额外加了覆盖率门槛:这类候选最多算 ⚠️。 - URL 必须为标题作证,否则直接丢掉:搬运站会逐字抄标题,但 URL 是不透明 ID。 第 162 期 ⑪ 就被 Pinterest pin 骗到过。原文的 slug 或域名几乎总会带上标题里的词; 对不上的结果不会进入候选,也不会以 ⚠️ 发出去。slug 命中会乘 0.95,弱于真正的标题匹配。
- 缩短查询只给 Hacker News,且和全文同时发出:Algolia 要求所有词都命中,周刊标题一旦比原文长
就会返回空。
sqlite vs duckdb不再等全文先空跑一轮。 - 项目:用仓库名搜 GitHub,仓库名完全一致得满分,再用「描述与周刊简介的重合度」和 star 数做加权, 同名仓库很多时也能选对。搜不到才退回网页搜索。
任何一次运行失败都会往 Telegram 推一条告警,内容包含错误类型、原始错误信息和针对性的处理建议,
不用去翻 Val Town 的日志。为了避免刷屏,同一类错误 12 小时内只提醒一次;
问题消失后的第一次成功运行会推一条「已恢复」。不想要告警就把 ERROR_NOTIFY 设成 0。
诊断规则写在 src/errors.ts,常见情况和处理方式:
| 现象 | 原因 | 怎么解决 |
|---|---|---|
| Telegram 401 | bot token 错了 | 去 @BotFather 用 /mybots 核对,必要时 /revoke 重新生成 |
| Telegram 400 chat not found | chat_id 不对 | 先主动给机器人发条消息;群聊的 chat_id 是负数,别漏减号 |
| Telegram can't parse entities | 标题里有未转义字符 | 检查 telegram.ts 的 escapeHtml;应急可先去掉 parse_mode |
| GitHub 403 / rate limit | 匿名调用只有 60 次/小时 | 配 GITHUB_TOKEN,顺带让项目检索走 GraphQL 提速 |
| Parsed 0 items | 周刊排版改了 | 改 src/parser.ts 的 ITEM_PREFIX / SECTION_HEADER,跑 deno task test 验证 |
| Serper 401 / 403 | key 写错或 2500 次用完了 | 去 serper.dev 后台核对 key、看余额,充值或删掉这个变量 |
| 超出运行时长 | 一分钟没跑完 | 调小 RESOLVE_BUDGET_MS,让它保存进度交给下一次运行 |
下面几点是刻意设计的,出问题时不会造成更坏的后果:
- 解析失败(0 条)时不会把这一期记成「已推送」,修好脚本后下一次运行会自动补发。
- 单个条目检索失败只会让那一条没有链接,不会中断整期推送。
- 告警发送本身失败时只写日志,不会因此再抛一次错。
- 失败后「已推送」记录不变,静默窗口也就一直是打开的,下一次触发自动重试,不需要手动干预。
- 周刊标题偶尔与原文标题不完全一致(比如加了副标题),这类条目会被标成 ⚠️ 或没有链接,需要人工确认。 第 161 期里的 "How Claude Code Works, From Tokens to Agents" 就属于这种情况。
- 不配
SERPER_API_KEY就只剩 Hacker News 一条渠道,覆盖率会掉得很厉害,且没有免费替代品可切。 - 解析依赖周刊现有的 ①②③ 编号排版;如果作者改版,脚本会因为解析出 0 条而报错退出, 不会误把广告内容当成正文推送。