- 文档
- 教程
【免费下载链接】project-based-learning
Curated list of project-based tutorials
CONTRIBUTING.md 是 project-based-learning 仓库唯一的贡献入口文档,它定义了向这份"项目制教程清单"提交内容的全套规则:提交前必须逐条核对的质量清单、README 条目的标准格式、多部分系列的组织方式,以及提交 PR 前必须在本地跑通的校验命令。本文以该文档为主线,结合仓库内 scripts/check_readme.py(约 1178 行的 README 解析器与链接巡检器)的源码实现,逐条解读每条规则背后的自动化逻辑、错误码含义与 CI 行为,帮助你写出的每个 Pull Request 都能一次通过校验,并理解"某些链接显示 could not verify"为何是预期现象而非构建失败。
仓库定位与贡献入口
project-based-learning 的核心载体是根目录下的 README.md:一份按主语言分节组织的编程教程清单,收录标准是"读者跟随教程从零构建出一个完整、可运行的应用"(README 原文:a list of programming tutorials in which aspiring software developers learn how to build an application from scratch, divided into different primary programming languages)。因此,它的"代码资产"本质上就是 README 这份数据文件,而 CONTRIBUTING.md 就是维护这份数据的操作手册。
贡献流程的第一步是 fork 仓库(README.md 明确要求"To get started, simply fork this repo"),随后所有改动以 Pull Request 形式提交,并严格遵循 CONTRIBUTING.md 中的约定。
提交 PR 前的质量清单:十四条规则全解读
CONTRIBUTING.md 在"Before making a pull request"下列出了一整套检查项。这些规则可以分为五组:查重与定位、内容准入、作者与 PR 管理、条目格式、文字与链接卫生。
查重与定位(规则 1–2)
- 先查重:想要添加的教程不能已经存在——需要在 README.md 中同时搜索其URL和标题。这条规则在源码层面有对应的自动化实现(详见下文"重复 URL 的判定与白名单机制"):解析器会对每个条目的 URL 做归一化去重,命中重复会触发 E005 错误;但标题查重属于人工核对项,因为解析器只能解析结构,无法判断语义重复。
- 归位正确:教程必须放在合适的语言/技术章节下。README.md 以
##二级标题划分主语言(C#、C/C++、Clojure、Dart、Elixir、Erlang、F#、Go、Haskell、HTML/CSS、Java、JavaScript、Kotlin、Lua、OCaml、PHP、Python、R、Ruby、Rust、Scala、Swift,外加 Additional Resources),部分语言内部还有###三级子节(如 C/C++ 下的 Network programming、OpenGL,JavaScript 下的 React、Next.js、Angular、Node、Vue 等)。解析器会记录每条条目所属的 section,链接巡检报告也会按 section 汇总,归位错误会直接造成后续巡检报告的可读性下降。
内容准入标准(规则 3–4)
- 免费开放:教程必须免费且开放——不能有付费墙(paywall)、登录墙(login wall)或强制订阅 newsletter。这是一条人工评审项,自动化工具无法探测页面背后的付费逻辑;这正是 CI 中"某些站点显示 could not verify 而不是构建失败"的背景(详见下文 CI 链接巡检一节)。
- 项目制:教程必须是 project-based 的——读者跟随教程能构建出完整、可运行的作品,而非单纯的概念讲解。这是本清单的收录哲学,也是 README.md 对整份清单的定义性标准。是否"构建出完整作品"需要人工判断,机器只负责校验条目结构。
作者披露与 PR 管理(规则 5–8)
- 作者披露:如果你是教程作者、或与作者/网站有关联,必须在 PR 中明示。这是社区透明度要求,避免清单被变相用于推广。
- 描述性标题:PR 必须有一个能描述改动内容的标题(descriptive title)。
- 新语言可开新章节:如果教程的语言/技术栈在清单中尚不存在,可以在目录(Table of Contents)中新建条目。注意这条与 ToC 锚点校验直接相关——新增
##章节后必须同步在 ToC 中登记,否则校验器会报 E003/E004(详见下文)。 - 一个 PR 只放一个教程:每个教程单独一个 Pull Request。从源码结构看,这一约定与 check-diff 子命令 的设计互相呼应——CI 的 diff 检查只针对本次新增的行做语法与 URL 校验,粒度越小,评审和自动检查就越聚焦。
条目格式与多部分系列(规则 9–10)
单条教程使用以下精确格式:
- Title如果教程是多部分系列(multi-part series),必须使用缩进嵌套格式:
- Title - Part 1 - Part 2这两个格式不是随便约定的——它们是 check_readme.py 中正则文法的一部分(详见下一节)。系列标题行本身没有链接,但必须有比它更深一级缩进的子条目,否则会触发 E002 错误。
文字与链接卫生(规则 11–13)
- 检查拼写与语法:人工检查项,校验器不检查英文拼写。
- 去除行尾空白:样式约定,解析器的正则在语法上容忍行尾空白(例如
HEADER_RE的\s*$),但清单要求保持文件整洁。 - 链接直指教程本体,禁用短链:链接必须直接指向教程页面,禁止使用 URL 缩短服务。这一条在源码中有强校验:
URL_SHORTENER_DOMAINS集合(check_readme.py)列出了 bit.ly、tinyurl.com、goo.gl、t.co、ow.ly、buff.ly、is.gd、rebrand.ly、cutt.ly、shorturl.at、tiny.cc、rb.gy、lnkd.in、t.ly、bitly.com、po.st、adf.ly、tr.im、x.co 等二十余个短链域名,任何条目命中该集合都会触发 E102 错误,lint 和 check-diff 两个场景都会检查。
README 条目语法:解析器眼中的"合法行"
理解格式规则最可靠的方式是看解析器如何逐行分类。classify_line(check_readme.py)把 README 的每一行归入以下类型:
| 行类型 | 触发条件 | 说明 |
|---|---|---|
blank | 空行 | 被跳过 |
header | ^(#{2,4})\s+... | 标题行,##级确定章节归属 |
entry | - title,缩进 0/2/4 空格 | 教程条目,缩进空格数 ÷ 2 = 嵌套深度 |
series | - 标题(无链接),缩进 0/2 空格 | 系列标题行 |
toc | - [label](#anchor)且位于 ToC 块内 | 目录条目 |
malformed | 形似- [但 URL/括号不完整 | 会被判为 E001/E103 错误,绝不落入 series |
unparseable | 以上都不匹配 | 触发 E001 错误 |
注意malformed的巧思:一个写坏的链接条目(缺 scheme、括号未闭合等)如果落入series分支,就会悄悄从所有检查中消失,因此解析器强制把形如- [的行单独归类,宁可报错也不静默吞掉。
条目 URL 提取自ENTRY_RE正则(check_readme.py),它还会从条目行内剩余文本(tail)中提取额外 URL 一并计入检查。多部分系列的合法性由 E002 保证:parse_readme在解析到 series 标题行时向前看,若紧随其后没有更深缩进的 entry/series 子项,就报"series title has no deeper child entry"错误(check_readme.py)。
本地校验:python3 scripts/check_readme.py lint
CONTRIBUTING.md 要求:在打开 PR 之前,在仓库根目录本地运行校验器,且必须以状态码 0 退出:
python3 scripts/check_readme.py lint这是整个贡献指南中最具操作性的部分。lint子命令(check_readme.py)做三件事:
- 语法检查:用上文介绍的逐行文法解析整个 README,任何不符合 entry/series/header/toc 语法的行都会报错;
- 目录检查:校验 ToC 条目与
##章节锚点的双向一致性(E003/E004); - 重复与短链检查:归一化 URL 去重(E005)、http/https 变体警告(W102)、短链域名拦截(E102),以及历史遗留的
http://链接统计(E101,info 级)。
出错时退出码为 1,全部通过时为 0。非 JSON 模式下,输出按行号与严重级别排序的诊断信息,格式为行号:错误码:严重级别 消息,末尾打印统计行:
stats: entries=... urls=... http=... sections=... errors=... warnings=... info=...如果需要结构化结果(供脚本或 CI 消费),可以追加--json参数,输出按 errors/warnings/info 分桶的 JSON 及统计信息。
lint 的错误码全景:E001–E005 与警告 W101/W102
把 CONTRIBUTING.md 中"checks the grammar above, the Table of Contents, and for duplicate/shortened URLs"一句话落实到源码,得到如下错误码清单:
| 错误码 | 严重级别 | 含义 | 触发场景 |
|---|---|---|---|
| E001 | error | 行无法解析 / ToC 块内出现非法行 / ToC 风格行出现在正文 | 格式错误、漏了]或)、ToC 块外出现- [label](#anchor) |
| E002 | error | 系列标题没有更深层子条目 | - Title后没有缩进子项 |
| E003 | error | ToC 锚点没有对应的##标题 | ToC 写了#go但正文没有## Go:标题 |
| E004 | error | ##标题没有登记进 ToC | 新增语言章节后忘了更新目录 |
| E005 | error | 归一化后 URL 重复 | 同一 URL 出现两次以上(白名单除外) |
| E101 | info(lint)/ error(check-diff) | 使用http://链接 | lint 中属历史遗留仅提示;新增行中属错误 |
| E102 | error | 命中短链域名 | URL 主机在URL_SHORTENER_DOMAINS中 |
| E103 | error | 新增行不符合 entry/series/header 文法 | 仅 check-diff 场景 |
| W101 | warning | 新增youtu.be链接 | 建议改用完整 youtube.com URL(仅 check-diff) |
| W102 | warning | 同一 URL 存在 http/https 变体 | 两个条目仅 scheme 不同 |
这里有一个值得注意的设计:E101 在整库 lint 中是 info 级别(历史遗留的http://链接被"grandfathered"保留),但在 check-diff 校验新增行时升级为 error——存量可以容忍,增量必须合规。
重复 URL 的判定与白名单机制
规则 1(先查重)的自动化实现是 E005。normalize_url(check_readme.py)会把 URL 拆分为(scheme, netloc, path, query)四元组并去掉尾部/和空 path,因此https://a.com/x/与https://a.com/x会被判为同一个 URL。同一归一化 key 出现多次且不在白名单内,就触发 E005。
少数"合法重复"通过 scripts/lint-allow-duplicates.txt 放行,该文件是 E005 的显式例外清单,每行一个归一化 URL 并附注释说明原因,仓库里目前有两个典型场景:
craftinginterpreters.com教程的第 1–13 章用 Java 编写、第 14 章起用 C 编写,因此有意同时收录在 C/C++ 与 Java 两个章节下;llvm.org/docs/tutorial/的 Kaleidoscope 教程有 C++ 与 OCaml 两个语言变体(同一物理页面、不同#fragment),去重逻辑会剥离 fragment,因此需要 path 级白名单。
这说明:重复检查是"默认从严、例外开白"的模型,任何绕过 E005 的需求都要有说得通的理由。
ToC 与章节锚点:新增语言章节的正确姿势
规则 7 允许为不存在的语言新建目录条目,但这绝不意味着可以只加一个##标题了事。parse_readme在解析完成后会做双向交叉校验(check_readme.py):
- 每个 ToC 锚点必须能匹配一个
##标题(否则 E003); - 每个
##标题必须出现在 ToC 中(否则 E004)。
锚点匹配依赖github_slug(check_readme.py),它把标题转小写、去掉非字母数字字符、空白转连字符——所以 README 中的## C/C++:对应 ToC 锚点#cc,## HTML/CSS:对应#html-and-css。新增语言章节时,必须同时完成三件事:写## 语言:标题、在 ToC 中登记- [语言](#slug)、在正文中按- Title格式填入教程条目,三者缺一都会让 lint 失败。
CI 链接巡检:可验证性、域名策略与 2-strike 机制
CONTRIBUTING.md 最后一段说明了 CI 的另一项职责:检查你添加的每个链接是否可达。这对应check-links子命令(check_readme.py),它默认用 8 个 worker 并发检查 URL,并针对每个注册域名做限速(每个域名最多 2 个并发、最小间隔 1 秒,见DomainLimiter,check_readme.py)。
每个链接的检查结果是四分类之一(_classify_result,check_readme.py):
| 分类 | 含义 | 典型触发 |
|---|---|---|
OK | 可达 | HTTP 2xx,且页面标题不含软 404 特征 |
HARD_DEAD | 确证失效 | HTTP 404/410、DNS 解析失败、TLS 证书不匹配、连接被拒 |
SUSPECT | 可疑,需人工确认 | 页面标题含软 404 短语(如 "page not found"、"video unavailable")、重定向到站点根目录、意外状态码 |
BLOCKED | 无法验证,不算失效 | 命中域名策略、HTTP 401/403/429、网络错误重试后仍失败 |
软 404 检测值得一提:有些站点对失效页面仍返回 200,但<title>中含有 "404"、"page not found"、"no longer available"、"video unavailable" 等短语(SOFT_404_PHRASES,check_readme.py),这类结果会被标记为SUSPECT而不是OK。重定向处理上,最多跟随 5 次跳转;对 429/500/502/503/504 会按Retry-After(上限 30 秒)退避重试;HEAD 请求遇到 400/403/405/501 时会自动降级为 GET。
"could not verify"为什么是预期行为:这是 CONTRIBUTING.md 明确说明的边界——Medium、Reddit、LinkedIn、Udemy 等站点会拦截自动化 User-Agent。仓库用 scripts/linkcheck-domains.txt 维护了一份域名策略:这些域名的检查失败一律被归为BLOCKED,永远不会误报为失效链接。该文件目前收录了 medium.com、*.medium.com、towardsdatascience.com、reddit.com、twitter.com、x.com、linkedin.com、udemy.com、quora.com,并注明*.前缀可匹配任意子域名。所以如果你的链接指向这些站点而 CI 显示 "could not verify",请直接忽略——这是设计行为,不是构建失败,也不需要修复。
从 2-strike 状态机到自动修复 PR
链接巡检还配套了完整的状态机与修复流水线:
- 2-strike 规则:每次巡检结果写入状态文件(默认
.github/link-rot-state.json)。一个 URL 需要连续两次被判定为HARD_DEAD或SUSPECT才会进入周报的 Dead/Suspect 列表(consecutive_failures >= 2);BLOCKED不影响计数。 - 周报生成:
report子命令(check_readme.py)把巡检结果渲染成 Markdown 问题模板,分为 Dead links、Moved links、Suspect links、Blocked/unverifiable 四部分。README 顶部的 "Link rot sweep" 徽章以及报告落款(Regenerated weekly by.github/workflows/link-rot.yml)都指向这条每周自动流水线。 - 安全自动修复:
prune子命令(check_readme.py)只做两类无歧义修改——删除连续 2 周确证死亡且无重定向的条目、把发生同站重定向的条目标题为新 URL(原地更新,而不是换成 archive.org 链接);同时自动清理因此变成孤儿的多部分系列标题。任何SUSPECT、非主 URL 的次要链接、或一个 URL 匹配多个条目的情况都留给人工处理,且该命令的产物以 PR 形式落地、绝不会无人评审直接合入。
校验器的安全边界设计
check_readme.py 的模块文档明确了一条安全不变量,这也是理解整个工具链设计的关键:README 内容与 PR diff 都是不可信输入(该校验器会运行在 fork 出的 PR 上)。因此它只允许urllib/http.client网络调用与纯字符串/正则解析,绝不对 README 数据执行eval/exec、不传给 shell、不用它拼命令行。
这一原则在细节处处处体现:例如escape_md_cell(check_readme.py)在把页面<title>(由第三方站点控制的内容)嵌入周报表格时会转义|和\,并用零宽空格打断@的解析,防止恶意页面内容破坏表格结构或触发 GitHub 的 @mention 通知。贡献者理解这层设计后,也能明白为何条目格式必须严格、为何 URL 必须直接指向教程本体——整个数据管道都建立在对 README 文本的可解析假设之上。
结语
CONTRIBUTING.md 看似只有三十余行,但它定义的每一条规则都能在仓库的 scripts/check_readme.py 中找到对应实现:条目与系列格式对应ENTRY_RE/SERIES_RE文法,查重与短链禁令对应 E005/E102,ToC 规则对应 E003/E004,CI 链接检查对应 check-links 的四分类与 scripts/linkcheck-domains.txt 域名策略。提交贡献前,只需在仓库根目录跑通python3 scripts/check_readme.py lint(退出码 0),再核对一遍十四条人工检查项,你的 PR 就具备了一次通过的坚实基础。若对规则本身有改进建议,可通过 CONTRIBUTING.md 中给出的维护者邮箱 tuvtran97@gmail.com 联系。
- 文档
- 教程
【免费下载链接】project-based-learning
Curated list of project-based tutorials
相关推荐
minikube 文档贡献指南:`make site` 本地构建、markdownlint 校验与 Hugo ref/relref 链接规范
minikube 文档贡献指南: make site 本地构建、markdownlint 校验与 Hugo ref/relref 链接规范 minikube 的
云原生容器编排CLI开发工具SciPy 文档贡献指南:从本地 Sphinx 渲染到写作规范与 CI 校验全解析
SciPy 文档贡献指南:从本地 Sphinx 渲染到写作规范与 CI 校验全解析 本文面向希望向 SciPy 仓库提交文档修改(修复 docstring 错误
科学计算数据科学高性能计算godot-rust(gdext)贡献指南:从 PR 规范到本地开发与 CI 工具链全解析
godot rust(gdext)贡献指南:从 PR 规范到本地开发与 CI 工具链全解析 godot rust(仓库 gdext )是 Rust 语言与 Go
游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考