☰
游戏开发资料大全:用Git仓库做索引、元数据校验与死链检测
2026/11/4 3:45:29 网站建设 项目流程

游戏开发这个圈子里,学习资料从来不是稀缺品,稀缺的是"我现在要用的时候能立刻找到它"这件事。我自己做过几年独立项目,也带过几个刚入行的新人,收藏夹里躺着上千条链接,硬盘里散着三个版本的教程压缩包,笔记软件里还有一堆没头没尾的截图。真正要动手做一个原型的时候,光是把这些东西翻出来对齐,半小时就过去了。后来我干脆把这些东西全部收进一个仓库,做成一份自己的游戏开发资源大全:一个仓库搞定所有学习资料,按方向分目录、按统一字段写元数据、用脚本自动体检死链,新人拉下来十分钟就知道该看什么。这套东西跑了大半年,最大的变化不是资料变多了,而是找东西的时间从十几分钟压到几十秒。

这篇东西讲的就是这个仓库怎么设计、怎么搭、怎么维护。它不是一份"资源推荐清单",而是一套收纳资料的工程方法:目录怎么分层、每条资源要记哪些字段、命名怎么定、死链怎么自动扫、大文件怎么不把仓库撑爆、半年后怎么保证它还没烂掉。适合三类人看:正在自学、资料已经乱成一锅粥的;小团队里负责带新人、需要一份统一入口的;以及单纯喜欢把知识体系整理成可检索结构的强迫症患者。不需要你会写复杂脚本,会几条基本命令、能看懂最简单的 Python,就足够把这套东西落地。

1. 为什么我决定用"一个仓库"来管所有学习资料

1.1 收藏夹式学习的三笔隐形账

大部分人整理资料的方式是"收藏即拥有"。看到一篇讲碰撞检测的文章,点个收藏;刷到一个讲着色器的视频,存进播放列表;下载了一个素材包,扔进"待整理"文件夹。当时的心理感受是"我把它抓住了",但实际发生的是一次延迟支付的负债。

第一笔账是上下文丢失。半年后你翻到一条链接,标题写着《空间划分入门》,你已经想不起来当初为什么要存它——是为了优化碰撞检测,还是为了解决场景剔除?没有一条备注把它和你的具体问题绑在一起,这条资源就等于失效了。第二笔账是链接腐烂。个人博客的平均存活周期远比想象中短,我统计过自己早期收藏的一批技术博客,两年后能正常打开的不到六成。第三笔账是无法协作。你可以把收藏夹分享给别人,但那只是一串链接,没有任何筛选信息和难度标注,对方拿到手等于拿到一堆噪音。

A 同学是我带过的一个转行自学者,他的做法很典型:把教程视频按"第几周"编号存进网盘,然后用一个表格记录进度。三个月后他想回头查"角色控制器怎么做",发现自己当初把这块内容拆在了四个不同的视频里,而表格里只写了"第三周:看完"。这三笔账加在一起,最直接的后果是重复劳动——同一类知识你会学两到三遍,每次都是从零开始找。

1.2 仓库、网盘、笔记软件到底差在哪

我不认为仓库是万能的,它只是在我的使用场景里恰好合适。把几种常见载体摆在一起对比,区别会非常清楚。

载体全文检索版本历史多人协作离线可用大文件维护成本
浏览器收藏夹弱(只有标题)无无部分不支持极低,但会烂
本地文件夹依赖系统搜索无靠拷贝是支持中
网盘同步盘弱有限一般需缓存支持中
笔记软件强有强视产品弱中高
版本仓库强(纯文本)完整强是需额外处理前期高,后期低

关键差异在版本历史和纯文本可检索这两条。仓库里的每条资源都是一行 Markdown 或者一行结构化数据,这意味着你可以用 grep 做全文搜索,可以 diff 出"这周新增了什么",可以回滚到上个月的状态,也可以让别人的提交通过合并请求进来。笔记软件也能做到其中一部分,但它的数据格式通常是私有结构,导出一次要掉一层皮,而且很难做"提交即校验"这种自动化。

网盘的问题不在能力,而在心智。网盘里没有"这条资源已被验证可用"的概念,你永远不知道那个 2019 年的压缩包解压出来还能不能用。仓库里我可以加一个字段叫"最后验证日期",用脚本定期扫描,过期的高亮出来。这个机制听起来很小,但它把"资料整理"从一次性动作变成了持续运营。

1.3 这套结构适合谁,不适合谁

先说适合的。第一类是正在自学的个人,尤其是从别的行业转过来的,你的知识是碎片化拼起来的,特别需要一张自己的地图。第二类是三人以内的小团队,没有专门的文档流程,一份共享仓库就是最低成本的团队知识库。第三类是带新人的角色,你不需要每次从零讲一遍"渲染管线怎么理解",直接给一个目录让他按顺序看。

不适合的也很明确。如果你目前的阶段是"先做完一个能跑的小 Demo",那我建议你先别整理,先做完——整理资料这件事本身有很强的拖延属性,很容易变成"我在为学习做准备"的自我安慰。另外,如果你的资料主体是几十 GB 的视频课程,仓库不是好的托管方案,仓库只该存索引,视频本身放别处,用链接和校验值引用。还有一类是已经有一套成熟笔记体系的人,硬迁移过来的收益不大,你可以只借鉴元数据字段设计那部分。

注意:这套东西的第一原则是"索引优先于存储"。仓库负责回答"东西在哪、值不值得看、什么时候验证过",不负责当硬盘。搞混这一点,仓库会迅速膨胀成一个没人愿意克隆的怪物。

2. 仓库的整体骨架设计

2.1 顶层目录按"产出物"分,不按"资料类型"分

这是我踩过最大的一个坑。最早我的目录是这样的:视频/、文章/、PDF/、代码/、素材/。看起来很整洁,实际用起来极其痛苦,因为你从来不会带着"我要找一个 PDF"的念头去检索。你的念头是"我要做一个能跳能爬的角色",或者"我要让水面反射看起来不那么假"。按载体分类等于按文件的物理属性分类,和你的检索意图完全正交。

改成按产出物分之后,一切顺了。现在的顶层结构大概是这样:

game-dev-vault/ ├── 00-index/ # 总索引、标签表、阅读路线 ├── 10-engine/ # 引擎与语言:上手路径、API 速查、项目模板 ├── 20-math-graphics/ # 数学地基、渲染管线、着色器 ├── 30-gameplay/ # 玩法、关卡、数值、手感调参 ├── 40-art-audio/ # 美术流程、素材授权、音频处理 ├── 50-engineering/ # 版本管理、构建、性能剖析、崩溃上报 ├── 60-release/ # 打包、多平台、商店页素材规格 ├── 70-cases/ # 案例拆解、复盘笔记 ├── 90-toolbox/ # 脚本、模板、配置文件 └── README.md

数字前缀是我特意加的。文件管理器默认按字典序排列,不加前缀的话art会排在engine前面,和逻辑顺序对不上。用两位数或者三位数编号,还有一个隐性好处:中间留了空位。你以后想在20和30之间插一个"物理与动画",可以用25,不用把后面全部重命名。这个技巧我是从印刷排版的行距里学来的,很土但很管用。

每一层内部我建议再分两层:_links/放纯链接和元数据,_notes/放我自己的理解和笔记。分开的原因很实际——链接是可能失效的、可以批量替换的,笔记是原创的、不能丢的。混在一起的时候,做一次全量链接迁移会把笔记也搅进去。

2.2 每条资源的统一字段:控制在 11 个以内

字段设计是整个仓库的灵魂。字段太少,检索不了;字段太多,填写成本高到你坚持不下去。我前后改了三版,最后稳定在 11 个字段,填一条大约 40 秒,这个成本是可以长期承受的。

字段是否必填作用填写建议
id是唯一标识,用于交叉引用方向缩写+序号,如gp-0012
title是标题,用于快速扫读保留原文标题,别自己改写
topic是二级分类从固定词表里选,别自由发挥
type是载体类型文章/视频/书/代码/工具/课程
level是难度入门/进阶/专题,三档足够
lang是语言只标非母语,减少填写量
time否预计耗时视频必填,文章可空
url是主链接用短链或原始链接,别用带追踪参数的
mirror否备用位置存档地址或镜像说明
license否授权素材类必填,其余可空
verified是最后验证日期格式统一YYYY-MM-DD

topic用固定词表这条我要单独强调。人性天然喜欢自由发挥,第一次整理的时候你会写出"渲染""图形学""画面表现"三个其实是一个意思的标签,两个月后你自己都搜不全。做法很简单:在00-index/topics.md里维护一份允许值的清单,脚本校验时发现不在清单里的直接报错。这个约束前三天会觉得烦,一周后你会感谢自己。

verified字段是这套体系里投入产出比最高的一个。它让你可以问出"哪些资源已经两年没验证过了"这种问题,然后批量处理。我一般把超过 18 个月的标记为"待复核",超过 30 个月的直接降级到归档区。

2.3 命名与路径的几条硬规矩

路径规范看着是小事,但凡涉及到脚本处理就会变成大事。我定了几条不留余地的规矩。

目录名和文件名全部用小写英文加短横线,不用中文、不用空格、不用下划线。原因有三个:跨平台时中文路径在某些压缩工具里会乱码;空格在 shell 脚本里需要额外转义,写脚本时容易漏;下划线和小写字母在等宽字体下容易看混,短横线更清晰。这条在你自己电脑上可能无所谓,但只要你想在自动化流程里跑一遍遍历,就知道有多省事。

资源条目的文件名我采用id-简写.md的形式,比如gp-0012-coyote-time.md。带 id 是为了和索引表对应,带简写是为了在文件列表里能看懂。同一目录下条目不要超过 60 个,超过就再分子目录——这不是技术限制,是人的浏览习惯,一屏能扫完的量大概是 20 到 30 条,再多就需要二次筛选。

还有一条是链接要去掉追踪参数。你从搜索结果里复制出来的地址,尾巴上经常挂着一串?utm_source=...&ref=...。这些参数会让同一个资源在去重时被识别成两条,也会在你分享给别人时暴露来源。写个小函数在提交前统一清洗,成本很低。

3. 资源分类与选型:每个方向该收什么、不该收什么

3.1 引擎与语言入口:先定一个,其余只做索引

新手最常犯的错是同时开两个引擎。我见过太多人一边看 C# 那套商业引擎的教程,一边又去试 C++ 那套的重型引擎,三个月后两个都停在"能创建项目、能放个方块"。引擎本质上是工具链的组合,你学的是它背后的编辑器工作流、资源管线、脚本生命周期,这些东西在不同引擎之间是要重新建立的肌肉记忆,并行学习等于把建立肌肉记忆的次数翻倍。

选引擎我给一个粗糙但有效的决策顺序。第一步看目标平台和包体要求:要做小体积的移动端或者网页端,重型引擎直接排除。第二步看视觉目标:写实风格的三维项目,重型引擎的现成管线能省你半年;二维或者风格化的,轻量引擎反而更顺手。第三步看你现有语言基础:熟悉 C# 就走对应的路线,熟悉 C++ 就走另一条,熟悉 JavaScript 就直接从网页游戏框架起步。第四步才看社区和教程数量——这一条重要,但不该排前面,因为教程多的引擎你不一定用得上。

仓库里我是这么处理的:10-engine/下面按引擎各建一个子目录,但只有一个标_primary,其余标_reference。_primary目录里放完整的上手路线、常用 API 速查、项目模板;_reference目录只放"迁移时需要注意的差异点"和"这个引擎的独有特性"。这个标记不是永久的,做新项目时可以换,但同一时间只能有一个 primary。

场景引擎形态该重点收集的资料
二维小体量、快速出原型轻量开源引擎或网页框架场景组织、精灵动画、输入处理
三维写实、中大型项目重型商业引擎材质系统、光照、地形、优化
极致包体、自定义渲染底层图形接口 + 自研图形接口文档、内存管理、构建系统
上架小游戏平台平台官方工具链平台 SDK、广告与支付接入、审核规范

收集资料的时候有个反直觉的建议:少收"从零做一个完整游戏"的长系列教程,多收"某个具体问题怎么解决"的专题文章。长系列看着有安全感,但它最大的问题是和你自己的项目不同步,你跟着做完一个示例,换到自己的题材上依然不会。专题文章短、针对性强,遇到问题时能立刻用上。

3.2 数学与图形学:最容易囤积、最难消化的部分

这个方向是重灾区。几乎每个人的仓库里都躺着几本图形学教材的 PDF 和一堆收藏了没看的线性代数课程。原因是这类知识看起来很重要但没有即时反馈,你收藏它的那一刻获得的是安全感,不是能力。

我的处理办法是把这个方向拆成"必须能徒手推"和"知道去哪查"两类。必须能徒手推的其实不多:向量加减与点乘叉乘的几何意义、矩阵乘法的顺序含义、坐标系变换的链路(模型空间到世界空间到观察空间到裁剪空间)、四元数为什么能避免万向锁。其余的东西,比如具体某个光照模型的 BRDF 公式、某种空间加速结构的构建细节,都是查得到就行。

四元数这块我用一个生活类比讲给新人听,效果不错:想象你手里拿着一个手机做全景拍摄,俯仰、偏航、翻滚三个轴分别转,当俯仰转到 90 度时,偏航和翻滚在物理上变成了同一个动作,你丢掉了一个自由度——这就是万向锁。用三个角度存姿态,本质上是用三个数字去描述一个三维旋转,但三维旋转的自由度只有三个而参数空间是环形的,某些点上必然退化。四元数用四个数加一个约束条件来描述,避开了这个奇点。这个解释不严谨,但足够让人明白"为什么需要它",剩下的严谨推导可以等真正用到时再看。

渲染管线这部分,仓库里我建议只放两类资料:一张清晰的管线阶段示意图(自己画的更好),和一份"每个阶段能做什么、不能做什么"的对照。很多新手的问题是不知道某个效果该在哪一步做,比如透明混合为什么必须在最后,深度测试为什么在片元阶段之前。这类"位置感"比记住 API 有用得多。

3.3 玩法与手感:最容易被当成玄学,其实全是参数

玩法设计里最能量化的部分是手感(game feel)。这部分我强烈建议在仓库里单独建一个文件,专门记录你调过的参数,因为它是唯一一类你调完就忘、下次还得重调的东西。

拿最经典的平台跳跃举例。假设你的角色起跳初速度是 12 米每秒,重力加速度是 30 米每二次方秒,那么跳跃高度和滞空时间可以直接算出来:

  • 起跳高度h = v² / (2g) = 144 / 60 = 2.4米
  • 上升到顶点的时间t_up = v / g = 12 / 30 = 0.4秒
  • 完整滞空时间t_total = 2 × 0.4 = 0.8秒

如果玩家反馈"跳得太飘、像月球漫步",把重力改成 60,速度提到 17,此时高度h = 289 / 120 ≈ 2.41米,几乎不变,但滞空时间变成2 × 17 / 60 ≈ 0.57秒。同样的跳跃高度,通过提高重力和初速度可以让手感变"脆"。这个结论用文字描述很难传达,但用两个数字一算就非常清楚,这就是为什么参数要记进仓库。

除了这两个基础量,还有几个"隐形参数"是商业手感的秘密,我全部记在30-gameplay/feel-params.md里:

参数典型值作用
土狼时间0.08 ~ 0.15 秒离开平台后仍允许起跳的宽限
输入缓冲0.10 ~ 0.20 秒落地前按下跳跃仍然生效
可变跳跃高度松键后重力 ×2 ~ ×3短按小跳、长按大跳
转向加速度0.05 ~ 0.12 秒到满速控制起步和急停的"重量感"
顶点悬停顶点附近重力 ×0.5让滞空更有"漂浮"节奏

这些数值不是标准答案,是起点。不同类型游戏差得很远,硬核动作和休闲平台跳跃的土狼时间能差三倍。但有了起点,你调整的时候就不是瞎试,而是在一个有坐标的空间里移动。仓库里我会为每个项目单独存一份参数快照,附一句"这个手感对应的是什么类型",下次开新项目可以直接拿来当基线。

3.4 美术与音频:只收"能立刻上手"的,重点盯授权

美术和音频方向的资料,我的筛选标准比技术方向严得多,因为这一类东西的"看起来有用但用不上"比例极高。一个两小时的厚涂教学视频,对你做一个二维小体量项目几乎零帮助。所以我只收三类:能在半小时内产出可用素材的流程、素材资源库、授权说明。

二维方向我建议先啃像素画和矢量剪影这两条线,它们对基本功要求低、出图快、风格容错高。三维方向先学低模加手绘贴图,别一上来就碰写实材质。动画方面,二维骨骼动画和逐帧动画的取舍很实际:逐帧表现力强但成本高,骨骼动画修改方便但容易僵。这个选择要在项目开始前定,中途换代价很大。

音频这块很多人直接忽略,其实是性价比最高的一环。一个游戏的音效做得对,玩家感受到的"完成度"会明显上升。仓库里我分了两个目录:sfx/放音效素材和来源,music/放音乐和授权。音频中间件这类工具只在项目需要动态混音、实时参数控制的时候才收,小项目直接用引擎内置的音频源就够了。

授权是必须单独说的。素材的授权类型直接决定你能不能用,我把常见的几类做成了一张常驻对照表:

授权类型能否商用是否需署名能否修改常见坑
完全放弃权利可以不需要可以少数要求不得暗示作者背书
署名型可以必须可以署名格式和位置有要求
相同方式共享可以必须可以,但衍生作品需同协议商业项目里很难满足
仅非商业不可以视条款视条款广告变现也算商业
自定义授权逐条读逐条读逐条读最容易踩雷的一类

我的做法是:每一条素材记录必须填license字段,填不出来的一律不收。这个规则很粗暴,但它避免了我见过的最惨的一种情况——项目做完准备上架,发现核心美术素材的授权不允许商用,重做来不及,换素材要重画三个月。

3.5 工程化与发布:绝大多数教程清单都缺这段

这是我最想强调的一节。你去搜"游戏开发学习路线",十份里有九份会告诉你学引擎、学编程、学美术,几乎没有人告诉你版本管理怎么做、构建流水线怎么配、崩溃怎么上报、包体怎么压。但这些恰恰是从"能跑"到"能发布"之间最大的鸿沟。

版本管理这块,游戏项目和普通软件项目最大的区别是二进制资产多。一张贴图、一个模型、一段音频,都是无法做行级 diff 的大文件。直接提交会让仓库体积迅速失控。我做过的计算是:一个中等体量的三维项目,原始资产加中间文件,很容易到 8 到 15 GB。如果在 10 Mbps 的上行带宽下克隆,理论最短时间是15 × 1024 × 8 / 10 ≈ 12288秒,也就是三个多小时。这个体验足以劝退任何一个想参与的人。

解决办法是用大文件版本管理扩展把二进制资产单独处理,同时把"可再生的中间文件"排除掉。判断标准很简单:这个文件能不能由别的文件重新生成?能生成的一律不进版本库,包括烘焙后的光照数据、导入产生的缓存、编译产物。这条规则能砍掉一半以上的体积。详细的配置我在 4.5 节给出来。

构建和发布这块要收的资料包括:各平台的打包参数含义、包体压缩的取舍(纹理压缩格式、音频压缩格式)、符号文件的保留方式、商店页的素材规格(图标尺寸、截图比例、宣传片时长限制)。最后这条特别容易漏,我见过项目做完卡在商店页素材不合格上,返工了整整一周。这些规格要求会变,所以每一条都要带verified日期。

4. 落地实操:从零把这个仓库跑起来

4.1 初始化与目录创建

前面讲的都是设计,现在动手。先建仓库、建目录,一条命令搞定。

mkdir -p game-dev-vault && cd game-dev-vault git init mkdir -p 00-index for d in 10-engine 20-math-graphics 30-gameplay 40-art-audio \ 50-engineering 60-release 70-cases 90-toolbox; do mkdir -p "$d/_links" "$d/_notes" done touch 00-index/topics.md 00-index/reading-path.md touch README.md .gitignore .gitattributes

顺手把.gitignore写好,这一步比多数人想的更重要:

# 编译与缓存 build/ dist/ obj/ *.cache *.tmp # 大体积中间文件 *.fbx.autosave *.blend1 *.psd~ # 本地环境 .env .DS_Store Thumbs.db

00-index/reading-path.md这个文件是用来放"阅读路线"的,它的作用是把散落的资源串成有先后的路径。比如"零基础到能做出一个可玩原型"这条路线,大概包含 8 到 12 条资源,每条标注预计耗时。这个文件我建议手写,别自动生成,因为顺序本身就是你的判断,而判断才是这个仓库最有价值的部分。

4.2 索引表与条目模板

每个方向目录下的_links/index.md是一个表格,字段和 2.2 节对齐。为了让填写成本尽可能低,我在90-toolbox/里放了一个条目模板,新增资源时复制粘贴改几处就行。

<!-- 90-toolbox/templates/entry.md --> ### gp-0012 平台跳跃的土狼时间是怎么实现的 - topic: 手感调参 - type: 文章 - level: 进阶 - lang: 中文 - time: 15 分钟 - url: https://example.invalid/coyote-time - mirror: 本地存档 2025-03 快照 - license: 未标注 - verified: 2025-03-14 **为什么收它**:用一个极小的示例说明了"离开平台后仍可起跳"的判定窗口, 并给出了不同游戏类型下的取值区间。 **要点摘录**: 1. 判定依据是"离地后的帧计数",不是距离。 2. 窗口过长会让玩家感觉"能凭空二段跳",过短则几乎无感。

这里有个细节值得说:"为什么收它"这一栏必须写。它是整个仓库里唯一无法被搜索引擎替代的东西,因为它是你的判断。我给自己定的下限是三十个字,少于三十个字说明我其实没想清楚为什么收,那就先别收。执行了几个月之后,这一栏成了我回看时最有价值的部分——很多条目的链接已经失效了,但那句判断还在。

00-index/topics.md里维护允许的分类词表,格式很朴素,一行一个:

# 允许的 topic 取值 - 引擎上手 - 脚本与生命周期 - 资源管线 - 线性代数 - 渲染管线 - 着色器 - 物理与碰撞 - 手感调参 - 关卡设计 - 数值与经济 - 二维美术 - 三维美术 - 动画 - 音效与音乐 - 版本管理 - 构建与打包 - 性能剖析 - 授权与合规

4.3 元数据校验脚本:把"填错了"挡在提交之前

仓库条目一多,手写的错误必然出现:日期格式不统一、id 重复、topic 写了个词表外的词、链接少了协议头。这些错误肉眼很难全查,但脚本一秒就能扫完。下面这个脚本只依赖标准库,复制就能跑。

#!/usr/bin/env python3 """校验资源条目的元数据完整性与格式。""" import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent INDEX_DIR = ROOT / "00-index" REQUIRED = ["topic", "type", "level", "url", "verified"] DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") FIELD_RE = re.compile(r"^-\s+([a-z]+):\s*(.+)$") HEAD_RE = re.compile(r"^###\s+([a-z]{2}-\d{4})\s+(.+)$") def load_topics(): path = INDEX_DIR / "topics.md" if not path.exists(): return set() topics = set() for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith("- "): topics.add(line[2:].strip()) return topics def parse_entry(path: Path): text = path.read_text(encoding="utf-8") lines = text.splitlines() entry_id, title, fields = None, None, {} for line in lines: m = HEAD_RE.match(line) if m and entry_id is None: entry_id, title = m.group(1), m.group(2) continue m = FIELD_RE.match(line) if m: fields[m.group(1)] = m.group(2).strip() return entry_id, title, fields def main(): topics = load_topics() problems, seen_ids, count = [], {}, 0 for path in sorted(ROOT.rglob("*.md")): if "_notes" in path.parts or path.name == "topics.md": continue if not re.match(r"^[a-z]{2}-\d{4}-", path.name): continue count += 1 entry_id, title, fields = parse_entry(path) if not entry_id: problems.append(f"{path}: 缺少 ### id 标题行") continue if entry_id in seen_ids: problems.append(f"{path}: id 重复,已存在于 {seen_ids[entry_id]}") seen_ids[entry_id] = path for key in REQUIRED: if not fields.get(key): problems.append(f"{path}: 缺少必填字段 {key}") if fields.get("verified") and not DATE_RE.match(fields["verified"]): problems.append(f"{path}: verified 日期格式应为 YYYY-MM-DD") url = fields.get("url", "") if url and not url.startswith(("http://", "https://")): problems.append(f"{path}: url 缺少协议头") topic = fields.get("topic") if topic and topics and topic not in topics: problems.append(f"{path}: topic '{topic}' 不在词表中") print(f"扫描条目 {count} 条,发现问题 {len(problems)} 个") for p in problems: print(" -", p) return 1 if problems else 0 if __name__ == "__main__": sys.exit(main())

这个脚本里有个设计选择值得解释:我为什么用rglob全盘扫描而不是维护一份文件清单?因为清单会过期。人手动维护的清单,加文件时忘了更新,校验就漏了。全盘扫的代价是慢一点,但在这个体量下(几千个文件)完全不是问题。相反,我特意给_notes目录开了豁免,因为笔记是我自己的自由文本,不该被元数据规范约束。

4.4 死链检测:并发数是要算的,不是拍脑袋的

这是维护环节最核心的自动化。没有它,仓库会在半年内退化成一半是死链的遗迹。

先算一笔账。假设仓库有 1500 条链接,单条请求的平均耗时(含 DNS、握手、响应)按 0.6 秒估。串行跑完需要1500 × 0.6 = 900秒,也就是 15 分钟。这个时长会超过大多数自动化环境的单任务时间上限,而且失败重跑的成本很高。改成 16 个并发,理论耗时约900 / 16 ≈ 56秒,实际因为网络抖动大概 70 到 90 秒,完全可以接受。

那为什么不干脆开 200 个并发?两个原因。一是多数站点的友好阈值在每秒个位数请求,打太猛会被限流甚至临时封禁,结果反而更慢。二是本地文件描述符和连接数有上限,开太大会出现莫名的连接重置。16 到 32 是一个稳妥区间,我最终定在 16。

#!/usr/bin/env python3 """并发检测资源链接可用性,输出疑似死链清单。""" import re import urllib.error import urllib.request from concurrent.futures import ThreadPoolExecutor from pathlib import Path ROOT = Path(__file__).resolve().parent.parent URL_RE = re.compile(r"^-\s+url:\s*(\S+)$", re.M) TIMEOUT = 8 WORKERS = 16 UA = "Mozilla/5.0 (compatible; link-check/1.0)" def collect_urls(): urls = {} for path in ROOT.rglob("*.md"): if "_notes" in path.parts: continue text = path.read_text(encoding="utf-8") for u in URL_RE.findall(text): urls.setdefault(u, []).append(str(path.relative_to(ROOT))) return urls def check(url): req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": UA}) try: with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: return url, resp.status, "" except urllib.error.HTTPError as e: # 有些站点不支持 HEAD,回退到 GET if e.code in (403, 405, 501): try: req = urllib.request.Request(url, headers={"User-Agent": UA}) with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: return url, resp.status, "" except Exception as e2: return url, None, repr(e2) return url, e.code, "" except Exception as e: return url, None, repr(e) def main(): urls = collect_urls() print(f"待检测链接 {len(urls)} 条,并发 {WORKERS}") bad = [] with ThreadPoolExecutor(max_workers=WORKERS) as pool: for url, status, err in pool.map(check, urls): ok = status is not None and 200 <= status < 400 if not ok: bad.append((url, status, err, urls[url])) print(f"疑似异常 {len(bad)} 条") for url, status, err, refs in bad: print(f"\n[{status or 'ERR'}] {url}") print(f" 引用: {', '.join(refs)}") if err: print(f" 详情: {err}") if __name__ == "__main__": main()

注意:脚本报出来的"异常"不等于死链。403 通常是站点拒绝脚本访问,429 是限流,5xx 是对方服务器临时故障。这几种都要人工点进去确认一遍再决定是否删除,我见过不止一次因为批量清理把好链接误删的情况。

保守的处置策略是这样的:第一次异常标记为"待确认",连续三次(间隔一周)都异常才降级到_archive/目录,并且保留条目但注释掉链接,而不是直接删。删除是不可逆的,注释是可逆的。这个小习惯救过我不下五次。

4.5 大文件与二进制资产的处理

回到 3.5 节留下的问题:怎么让仓库不至于膨胀到几个 GB。答案是两条规则加一份配置。

第一条规则:可再生的一律不进库。烘焙数据、导入缓存、编译产物、自动生成的缩略图,全部写进.gitignore。第二条规则:必须进库的二进制资产走大文件版本管理扩展,配置写在.gitattributes里。

# .gitattributes *.psd filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.tga filter=lfs diff=lfs merge=lfs -text *.fbx filter=lfs diff=lfs merge=lfs -text *.blend filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text *.mp3 filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text

即便如此,我还是要强调一个克制原则:这个仓库的定位是资料索引,不是资产仓库。如果你要存的是自己项目的美术源文件,那应该另开一个资产仓库,和索引仓库分开。索引仓库的体积目标是纯文本、克隆在十秒以内。我目前的索引仓库大概 2000 多条资源,纯文本加起来不到 1.5 MB,克隆几乎是瞬间完成的。这个体验差异,决定了别人愿不愿意参与进来。

顺便说一下体积的估算方法,方便你给自己设一条红线:纯文本条目平均约 600 字节(含标题、字段、摘要、要点),2000 条就是2000 × 600 / 1024 ≈ 1172KB,约 1.1 MB。加上索引文件和脚本,控制在 3 MB 以内是完全可行的。超过 20 MB 就该停下来检查是不是混进了不该进库的东西。

4.6 自动化:提交即校验

脚本写好了放在本地,靠自觉去跑,最终还是会被忘记。所以最后一步是把校验和死链检测挂到提交环节上。下面是一份通用的流水线配置,具体路径按你所用的托管平台的约定调整。

name: vault-check on: push: branches: [main] schedule: - cron: "0 3 * * 1" # 每周一凌晨跑一次全量死链检测 jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 元数据校验 run: python 90-toolbox/scripts/validate.py - name: 死链检测(仅定时任务执行) if: github.event_name == 'schedule' run: python 90-toolbox/scripts/check_links.py continue-on-error: true

这里有三个细节我调过好几轮。第一,死链检测只在定时任务里跑,不在每次提交时跑。因为网络检测受对方站点影响,把它挂在提交环节会让你的提交通道变得极不稳定,一次对方站点超时就把你的正常提交挡了。第二,加了continue-on-error,让死链检测失败不阻塞主流程,结果只作为报告。第三,元数据校验是硬性阻塞的,因为它是纯本地判断,结果确定,没有误报风险。

5. 维护机制:让仓库半年后还活着

5.1 季度体检与"三次未用即降级"

整理类项目几乎都死在同一件事上:建的时候很兴奋,一个月后不再打开。对抗这件事的办法不是靠意志力,是靠机制。我给自己定了一个很轻的季度流程,每次不超过一小时。

第一步跑一遍元数据校验和死链检测,把异常清单过一遍。第二步看verified超过 18 个月的条目,挑十条抽检,能打开且内容还有效的更新日期,失效的处理掉。第三步做一次减法:把过去三个月里引用次数为零、且我自己判断"大概不会再用"的条目移到_archive/。移动而不是删除,因为人的需求会变。

减法这一步我用一个粗暴的规则:连续三次季度体检都没被引用、也没被打开的条目,降级到归档区。这个规则的合理性在于,资料的真正价值不取决于它本身多好,而取决于它在你实际工作中的调用频率。一条讲得很透彻但你从没用过的文章,占用的其实是你的注意力预算。

提示:给仓库加一个usage.log,用极简格式记录"日期 + id + 一句话用途",比如2025-03-14 gp-0012 调跳跃手感翻了一下。写这一行只要五秒,但它是你判断"哪条资源真正有用"的唯一客观依据。三个月后回头看,你会惊讶地发现高频引用的可能只有二十来条。

5.2 版本钉死与快照策略

资料的有效性是有时效的。同一个地址,今年讲的是某个引擎版本的工作流,明年接口一改,内容就过时了。应对方式有两种,我两种都用。

第一种是在条目里写清适用版本。比如"适用于某引擎 2022 及之后的版本",这句话很关键,它让半年后的你知道该怎么判断这条资源还能不能信。第二种是对关键资源做快照。核心的 API 速查、版本迁移说明、授权条款,我会保存一份纯文本快照放进_notes/snapshots/,文件名带日期。快照的意义不是防死链,而是防"内容被悄悄改掉"——有些文档会静默更新,你按旧版写的代码突然对不上,有了快照就能对照出差异在哪。

快照的取舍也要算成本。一份纯文本快照大概几十 KB,存一百份也就几 MB,完全可以接受。但网页截图就没必要存了,图片体积大、检索不了、还容易过期。我在仓库里只存两类快照:文本内容和配置片段,不存截图。

5.3 版权与引用:只做索引,不做分发

这一条必须说清楚。资源大全这个形态天然带着版权风险,而风险几乎全部来自一个动作:把别人的内容复制进自己的仓库。

我的原则很简单:只存链接、标题、自己的判断和摘要,不存原文全文。摘要要短,并且明确标注"这是自己的理解,不是原文摘录"。素材类的资源,license字段填不出来的一律不收。教程类资源,如果原作者明确写了"禁止转载",那我就只存链接,连摘要都写得极简。

另一个容易忽略的点是引用格式。当你把别人的观点写进自己的笔记时,至少保留"来源 id"这一条线索。回看的时候你可以顺着 id 找到原链接,判断这个说法是否被断章取义了。这不是形式主义,我确实遇到过自己几个月前的笔记,因为当时摘得太随意,回看时完全误解了原意的情况。

6. 踩坑与排查实录

6.1 常见问题速查表

下面这些问题都是我在实际维护中真实遇到过的,按出现频率排序。

现象大概率原因排查动作处理方式
校验脚本报大量缺字段模板改动后旧条目没同步看报错文件的创建时间批量补字段,或对旧条目降级豁免
死链检测耗时异常长并发被限流,触发了退避重试看日志里的状态码分布并发降到 8,加请求间隔
仓库克隆很慢混入了大文件或历史提交里有大对象查目录体积和提交历史清理历史或迁移到独立资产库
同一资源出现两条链接带不同追踪参数对比 url 去掉参数后的主体保留信息更全的一条,另一条注释掉
搜索结果太杂topic 词表被自由发挥污染统计 topic 取值分布合并同义项,更新词表并重跑校验
中文路径在某些工具里乱码目录或文件名含中文或空格用find列出非 ASCII 路径批量重命名,脚本处理
多人提交频繁冲突都改同一个 index 文件看冲突集中在哪些行拆分索引文件,按方向分文件
定时任务里检测结果不稳定对方站点响应波动对比多次运行结果三次异常才降级,别立即删

最后一条我要单独展开,因为它是唯一一条"看起来是技术问题、其实是流程问题"的。早期我设的是"检测失败即标记失效",结果有一次对方站点维护了六个小时,我一次批量运行把四十多条好链接全标成了死的。改成"连续三次才降级"之后,这类误伤基本消失了。自动化越强,越要给它留缓冲。

6.2 只有踩过才知道的几个细节

第一个细节:先写阅读路线,再往里加资源。我最初的做法是疯狂往里塞,想着"以后再整理",结果三个月后仓库里有四百多条资源,但没有一条路径,我自己都不知道该从哪开始看。后来我改成反过来的顺序:先定一条"从零到能做出可玩原型"的路线,只写 12 条,然后围绕这条路线去补资源。这个顺序的差别在于,路线会自然淘汰掉大量不相关的资源,你的收集效率反而更高。

第二个细节:把"没看懂"也记下来。我有个习惯,遇到看不懂的内容,就在条目的要点摘录里写一句"这里没懂:为什么深度预通道能减少过度绘制"。这句话的价值远超我的预期,因为它把模糊的困惑变成了一个具体的问题,半年后我可能就在另一篇文章里找到了答案,然后回来补一句"已解决:原因是……"。仓库因此不只是一个书架,还变成了一个待办清单。

第三个细节:别追求全。我见过太多人卡在"还差一点就整理完了"的状态里,然后永远没开始用。资料整理本身是无限游戏,永远有更好的资源。我的做法是给自己定一个"够用即停"的阈值:每个方向有 8 到 15 条、覆盖入门和进阶两档、跑通一次元数据校验,就算这一版完成,直接开始用。用起来之后,缺口会自己暴露出来,那时候补的才是真正需要的。

第四个细节:把最难自证的部分外包给脚本。元数据格式、重复检测、死链、路径规范,这些全是机器能判的,别浪费注意力。人应该只做机器做不了的两件事:判断这条资源值不值得收,以及把资源串成有先后顺序的路径。认清这条分工,仓库的维护成本会下降一个数量级,这也是这套体系能持续半年以上的真正原因。

如果非要说一句个人体会,那就是:这个仓库最大的产出不是那两千多条资源,而是我在写"为什么收它"这一栏时被迫想清楚的那些东西。很多条资源在写完那一栏之后我就删了,因为我发现自己根本说不出它对我的价值。这个筛选过程本身,比仓库里的任何一条链接都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询