资讯详情

厌倦工具链?我用200行Python打造零依赖静态网站生成器caveman

📅 2026/10/29 3:52:09 | 华诺云谱 👁 阅读
厌倦工具链?我用200行Python打造零依赖静态网站生成器caveman
1. 被现代工具链逼疯之后我写了一个叫caveman的东西我这两年一直有个毛病项目一旦引入超过三个依赖就开始浑身难受。尤其是写个人博客、文档、落地页这类静态页面明明内容才是主角结果 Node_modules 体积比文章正文大了几百倍每次构建还要等半天升级一次依赖就能踩出一串 breaking change。我受够了这种为了建一座小木屋先造一座钢厂的开发方式。所以当caveman这个念头冒出来的时候我几乎没犹豫——我要做一个像穴居人一样纯粹的工具。没有框架没有脚手架没有插件生态甚至没有一个多余的文件。它只做一件事把 Markdown 变成静态 HTML完事。风格上刻意回到了 web 开发的石器时代但稳定性和可维护性比我用过的大部分现代工具都好。caveman 是我的个人实验项目验证一个反直觉的结论越是简单到极致的工具越能长期陪伴你。这篇文章会把整个项目的来龙去脉、核心设计、实操流程还有踩过的坑全部摊开给那些也被工具链折腾到疲惫的开发者一条新的思路。如果你只是想在周末搭建一个属于自己的小站或者厌倦了为了用某个功能必须引入整个框架的现状这篇内容应该能给你一点启发。2. 为什么选择穴居人路线核心设计与取舍逻辑先说清楚caveman 不是什么高大上的发明。它本质上就是一个单文件脚本用起来像极了一台手摇发电机你摇它转电能就出来了。没有储能电池没有电压转换但恰恰因为结构简单它几乎不会坏。2.1 零依赖不是炫技是外科手术式的需求分析我做功能规划时对自己提了一个要求如果砍掉这个功能我是否还能正常发布文章砍得越多越好。第一版删掉了所有前端框架的引入第二版去掉了构建工具的配置第三版连语法高亮都不想用 JavaScript 实现了——我直接在构建期把代码块的样式类名写死配合一份纯 CSS 就够用。caveman 的核心依赖为零这件事在技术上很有意义。它可以运行在任何有 Python 3 的机器上不需要 npm install不需要 pip install不需要 lock 文件不需要 CI 里的缓存策略。你把这个 200KB 的脚本扔到服务器上它就能干活这种确定性带给我的掌控感是之前任何工具都没有给的。2.2 构建模型输入目录到输出目录的幂等映射caveman 的工作流程很像一条流水线但这条流水线只有三个环节读取 source 目录下的所有 .md 文件解析 Markdown 中的标题、日期、正文套用 template 目录下的 HTML 骨架生成 target 目录关键点是幂等。无论你运行一次还是运行一百次只要输入不变输出永远一致。这个特性帮我规避了增量构建、缓存失效、文件监听这一系列复杂问题。我不需要一个守护进程常驻监听文件变化每次写完文章手动跑一次脚本的代价远比处理 watch 进程崩溃要低。2.3 文件名即路由内容文件即数据库caveman 没有数据库没有 CMS 后台没有可视化编辑器。每篇博客就是磁盘上的一个 .md 文件文件名就是 URL 路径。比如你写了一个2025-06-01-hello-world.md生成的页面就是/2025-06-01-hello-world.html。这个设计直接砍掉了一大半路由配置工作。迁移网站的时候把文件搬到新服务器就行导出、备份这些事通通消失。对我这种常年写技术笔记的人文件即内容的理念最能贴合实际工作流——我本来就一直用 Markdown 做笔记现在博客也只是笔记系统的扩展而已。2.4 模板引擎的选择越原始越安全很多人会纠结模板引擎用 Jinja2 还是 Mustachecaveman 直接绕开了这个问题。我连模板引擎都自己写了一个极简版本核心只有两个标签{{ title }}和{{ content }}。如果你想加个导航栏就在模板里手动写死 HTML不需要条件判断、循环之类的高级语法。这个选择在功能上算是退了一大步但在安全性上进了两大步。极简模板没有执行任意代码的能力也就没有 XSS 注入的寄生土壤对静态博客来说这是最理想的攻击面无攻击面。3. 亲手跑通 caveman 的完整流程光聊理念不够接下来把整个实操过程原原本本走一遍从环境准备到部署上线尽量把细节都写出来。3.1 环境准备与目录规划环境只需要一台装有 Python 3.6 的机器我自己实测的环境是 Ubuntu 22.04 和 macOS 12两个系统上跑出来的结果一致。Windows 用户建议用 WSL 或者等一会儿后面我会单独说 Windows 上的一个坑。目录结构我建议这样规划这也是我整理了三个博客之后摸索出来最顺手的一种caveman.py source/ posts/ 2025-06-01-hello-world.md 2025-06-02-caveman-guide.md pages/ about.md template/ base.html style.css target/source/posts放博客文章source/pages放独立页面比如关于页、项目页。template存放皮肤和样式。target是生成目录可以随手删除因为它可以由脚本完全复现。这样的好处很直接整个站点没有什么是不能被重建的。3.2 核心脚本的分步解析caveman.py 这个文件分为四个函数职责分得很清楚我将每个函数都放在了下面方便你直接参考。第一个是读取函数它负责遍历 source 目录收集所有 Markdown 文件的路径。这里有个细节我用了递归遍历而不是简单的glob.glob因为我的草稿都藏在source/_drafts子目录里不希望它们被构建出去。递归代码相对长但过滤逻辑更灵活比如你可以在路径里加一个_前缀来跳过草稿文件。第二个是解析函数每篇 Markdown 都通过正则提取元信息。我用的是^---$块来标记标题和日期比如--- title: caveman 实战指南 date: 2025-06-02 ---这个模板跟 Hugo 的 front matter 类似但我们没引入 YAML 解析库用的是正则直接抓。为什么敢这么干因为格式完全由我自己定义字段就标题和日期两个YAML 的大多数特性根本用不上。如果你的文章里有日期格式不规范、标题里有引号之类的情况只要你在写作时遵守约等于经过了校验提规则正则这条路完全够用。第三个是转换函数马克西姆·马克尔的首页。我一开始用的是第三方库markdown2测试下来解析效果不错但在某些极端情况下比如 Markdown 嵌套代码块时会返回奇怪的 HTML 结构加上新环境装包也不方便后来我干脆写了个支持三级标题、链接、图片、无序列表、代码块的迷你解析器。这里得提醒一句我写解析器纯粹是为了项目自洽不建议你去复刻。如果你只是想要一个发布工具老老实实用markdown库或commonmark-py是更好的选择。迷你解析器省了依赖但代价是丢掉了一些 Markdown 特性比如表格、脚注、任务列表。我在自己的站里把这些功能全部砍掉因为它是一个博客而不是一个文档中心。第四个是渲染函数把转换后的 HTML 拼进模板骨架。拼接方式就是字符串替换先读 base.html然后把{{ title }}替换成解析出的标题把{{ content }}替换成正文 HTML。这个过程没有任何缓存的参与每次生成都是全量构建。3.3 第一篇文章的生产流水线运行一次 caveman 之后应该看到这样的输出[BUILD] Scanning source directory... [BUILD] Found 3 markdown files [BUILD] Rendered 2025-06-01-hello-world.md - target/2025-06-01-hello-world.html [BUILD] Rendered 2025-06-02-caveman-guide.md - target/2025-06-02-caveman-guide.html [BUILD] Rendered about.md - target/about.html [BUILD] Copying template/style.css to target/style.css [BUILD] Done. 3 pages generated.构建完成后用浏览器打开target/2025-06-01-hello-world.html或者运行python3 -m http.server 8000 -d target启动本地服务在http://localhost:8000就能看到页面。这个流程跑通之后整个发布环节其实就只剩下两件事写 Markdown执行脚本。3.4 部署细节与多环境迁移部署 caveman 生成的站点有两种方式。第一种最简单直接把target目录丢到任何支持静态托管的服务器上比如 Nginx 的 root 路径、对象存储的静态网站托管或者任何你常用的 CDN。不需要服务器端环境不需要进程常驻不需要数据库因为这个目录里全是纯静态文件。第二种方式适合你有多台设备的情况。我自己就在笔记本和工作电脑之间同步source目录写完推一下然后在任意一台机器上执行构建推送到服务器。整个流程加起来不超过十秒远超我之前用 VPS 加 Node 服务那套方案的体验。迁移的时候有个隐蔽的关联点如果文章较多建议把图片等资源单独放一个目录并让模板目录结构与 target 对齐否则构建时容易漏文件。我第一版脚本没有复制非 md 资源文件导致文章里的图片在本地看得到部署上线后全是 404。这个问题在后面的踩坑章节会专门展开排查链路。4. 实测遇到的坑与完整排查链路现在进入这篇文章最有价值的部分我在使用 caveman 过程中遇到的实际问题和排查全过程。最好的学习方式就是看一个人怎么从现象推到底层原因。4.1 坑口一Windows 路径分隔符导致的生成地址错乱现象我在 Windows 上克隆了自己的博客源码仓库运行构建脚本后所有页面的 URL 变成2025-06-01-hello-world\\window.md这种带反斜杠的文件名而且网页里链接全部失效。理论上2025-06-01-hello-world.md路径得到的是2025-06-01-hello-world.html才对。排查链路我先确认脚本在 Linux 和 macOS 上运行正常问题只在 Windows 复现基本锚定是操作系统差异引发的问题。然后打开 caveman.py 的读取函数查看路径拼接逻辑。问题找到根因了os.path.join(source, posts, 2025.md)在 Windows 上生成source\\posts\\2025.md而这个带反斜杠的字符串被当作路由处理最终 URL 里出现了\\。这个坑如果写成时用os.path.join拼路径构建时用/拼路由就会凸显得很明显。看到反斜杠的那一刻我就反应过来了核心原因是路径用于文件 IO 时反斜杠确实是 Windows 的合法分隔符但路径被重新当作 URL 片段拼接后反斜杠不属于 URL 合法字符浏览器和服务器都会整段误解。修复方案构建时不用os.path.join改成手动拼接/。虽然文件系统层面两种都能读但 URL 统一用正斜杠。具体换成了这样的写法relative_path os.path.relpath(filepath, source_root) route relative_path.replace(os.sep, /)同时把所有生成的\\统一替换成/配合路径规范化函数做一层兜底。这个问题的通用教训是不要把操作系统路径分隔符当成全局通用符号作为 URL 或路由时一定要做一层标准化处理。4.2 坑口二文章内代码块被主题 CSS 拦截现象有一篇文章的代码块配上文档主题风格后样式完全错乱缩进全部消失pre和code标签里的空格和换行被浏览器折叠了。排查链路我先用浏览器开发者工具检查元素发现pre标签的默认样式被模板里的全局重置规则覆盖了具体是white-space: normal。这就是我写的极简模板原本带了一个 reset 文件里面把white-space设成normal这会影响所有元素包括代码块。然后我用远程环境查看生成的 HTML 里那个代码块的源码发现给代码包裹的结构是precode理论上浏览器默认的pre应该保留空白。但我在模板的 CSS 里写了一行* { white-space: normal; }这行代码本来是为了让所有元素换行更自然结果把 pre 的空白保留功能给废了于是出现样式错乱。直接定位到这个规则后把选择器改窄改成只对段落生效。同时补上针对 pre 的明确规则pre, code { white-space: pre; overflow-x: auto; }一下就好了。排查的体会是当你遇到代码样式诡异先看 CSS reset 是不是写了通配符通常比检查 syntax highlighter 配置更快命中根因。4.3 坑口三草稿目录被意外发布现象某天把一篇躺在_drafts里没写完的文章误发布到了线上心里一紧删掉之后就开始查原因。排查链路这个坑非常隐蔽虽然我在读取函数里没用递归方式遍历所有文件但我忽略了某些遍历路径可能仍然把_drafts算进去。实际去看代码发现自己深度优先递归时没做过滤而过滤逻辑只在顶层做了判断没有下探到子目录根路径导致草稿文件也被收集进了构建列表。修复方式是在递归函数收集文件的全过程中目录名以_开头的直接跳过一步到位包括顶层和任意深度的子目录。这里是修改前后的对比# 修复前 for root, dirs, files in os.walk(source_root): for file in files: if file.endswith(.md): build_file(os.path.join(root, file)) # 修复后 for root, dirs, files in os.walk(source_root): # 关键跳过滤逻辑 dirs[:] [d for d in dirs if not d.startswith(_)] for file in files: if file.endswith(.md) and not file.startswith(_): build_file(os.path.join(root, file))顺带把临时文件也过滤掉了凡是文件名以_或.开头的都不参与构建。这一条经验让我对草稿和已发布内容之间必须有一道硬隔离有了非常深的认同。4.4 坑口四模板缓存导致的过时样式现象改了style.css重新构建后页面样式没有跟着变。线上还是旧版本。排查链路我很确定构建命令执行了输出也显示复制文件成功那问题大概率在浏览器端。排查过程很快浏览器开发者工具里看 Network发现 html 文档引用的 CSS 路径没有变化同时 CSS 文件返回的响应头里带了强缓存标记旧 CSS 直接从本地缓存取出来用了。这个坑的本质是条件缓存一直存在但静态站点往往没有版本号概念。我以前用各种现代前端框架时自带 hash 文件名完全不感知缓存问题到了 caveman 这种原始工具我必须手动处理。修复方式很原始但也有效在模板里给 CSS 链接加上版本参数构建时用时间戳生成版本字符串link relstylesheet href/style.css?v{{ build_version }}每次构建都会生成新的版本号浏览器缓存识别到 URL 变化就会重新拉取。重新构建一次刷新页面样式立刻恢复。这个坑让我对现代框架的缓存策略解决的是真实问题有了切身体会但 caveman 的替代方案同样够用。4.5 坑口五文件名中的中文与空格导致 HTML 转义问题现象有一篇笔记叫论 2025 年的技术选型.md构建完以后浏览器打开页面 404文件名里的中文和空格在 URL 里全部被改了编码。排查链路第一直觉是文件名编码问题于是查了生成出的 HTML 链接。看到链接里写的还是原始的中文文件名浏览器对非 ASCII 的 URL 会自动编码空间 URL 编码是%20中文是 UTF-8 百分号编码但如果服务器层面没有做好同规则处理就会找不到文件。由于 caveman 生成的页面是纯静态文件URL 和磁盘文件名直接对应所以最简单的修复方式是生成链接时把文件名做一次 URL 编码。Python 里直接用urllib.parse.quote处理一下构建路径字符串但注意不能对整个 URL 做只对路径部分做处理完后链接就变成标准编码格式。from urllib.parse import quote link quote(route.replace(.md, .html))这个坑提醒我在静态站点里文件名就是天然的路由所以规范的文件命名策略能帮你躲开 80% 的坑。尽量用全小写字母、连字符、数字少用空格和中文。这个问题的通用教训极其实用如果你决定做一个静态站点从第一篇文章开始就定好命名规范能省下大量迁移和排查时间。4.6 坑口六本地正常、上线后页面样式全丢现象本地python3 -m http.server打开一切正常部署到 Nginx 之后所有页面有内容但没有任何样式。排查链路浏览器开发工具看 NetworkCSS 文件请求返回 404。我第一反应是构建输出的路径不对但检查 target 目录发现style.css确实存在。随后想到可能是 Nginx 路径配置问题打开 Nginx 配置发现 root 路径是/var/www/blog而我又把target目录整个复制成了/var/www/blog/target。于是页面访问/style.css实际查找的是/var/www/blog/style.css而不是/var/www/blog/target/style.css。修复方案有两种一是把target里的文件直接复制到/var/www/blog根目录二是 Nginx 配置上 root 指向/var/www/blog/target。我选择了后者因为它保证了以后重新构建后复制目录的动作不变。这个坑的核心教训是静态站点目录和服务器 root 之间的映射必须严格对齐。部署时多想想我复制的是目录还是目录里的文件,这一类问题在手动部署流程里本来就最容易出幺蛾子。5. 扩展玩法与适用边界caveman 做成了以后我陆续给这个原始人添了一些不太复杂的能力同时也想清楚了它到底该用在哪些地方生成、什么时候应该被其他工具替代。5.1 简单实用的扩展一自动生成文章索引页所有博客都需要一个首页来展示全部文章列表手写超链接太不穴居人了。我在构建脚本里加了一个generate_index()函数读取所有已发布的文章并按日期倒序排列然后直接用列表 HTML 拼接首页优先内容。这个功能的实现一点也不复杂甚至不需要引入数据层就是遍历文件名、提取标题、拼接超链接几个循环就能做到。但它的开发模式非常符合整体哲学——没有数据库没有额外文件完全由源目录自己在构建期推导出索引。5.2 简单实用的扩展二RSS 订阅源同样是构建期推导罗列出所有文章的完整 URL 和时间戳拼一个 XML 文件放到 target 根目录。没有依赖库的手写 RSS 生成需要稍微注意 XML 的合法转义规则但字段就 title、link、pubDate、description很容易写完。我实际跑完以后对信息量刚好够用就好这句话有了更深理解caveman 从核心到扩展都贯穿了这一主线。5.3 什么时候不要用 caveman我现在会明确区分两类情况一是个人博客、文档快照、产品落地页这类内容和表现形式都不太复杂的场景caveman 的简单恰好转化成长期维护的刚需。二是企业级 CMS、电商网站、需要权限系统、需要可视化编辑后台的场景绝对不要用 caveman那个原始连基本需求都覆盖不了。我自己在项目里也犯过把简单工具硬往复杂场景里套的错后来想明白工具的简单是一种优点但只有当需求足够简单时才是。选择工具的第一标准不是工具本身多优雅而是它能不能在需求的复杂度和你的维护能力之间找到合适的坐标。5.4 社区化的轻量演进在打算把 caveman 开源的同时我也给了自己一个约束不添插件系统不开放 Hooks 扩展甚至不接收希望能支持站点地图自动生成之类的新需求。因为只要你放开一个口子它就会变成另一个庞大的框架。这个项目给我最大的认知转变在于维护一个工具的成本不是由功能数量决定的而是由你必须记住自己之前在多个约定之间如何取舍的记忆负担决定的。caveman 砍掉了所有约定所以维护它只需要读完那 200 行代码这对单兵作战、个人站点这种场景来说是完美的。6. 写在最后的经验沉淀折腾完这个项目我最大的感受是有时候退步比进步更难也更必要。现代开发一直在做加法加框架、加抽象、加自动化但每加一层你自己直接掌控的确定性就少一分。caveman 不带任何政治色彩地向所有现代工具链喊了个话我可以过得更好更简单。如果你也想尝试这条路我的建议是从一个周末的小项目开始先写一个单文件脚本输入是几个 Markdown 文件输出是能看的 HTML。别急着加分类页、标签系统、站内搜索就让它保持原始。等你能完全理解自己工具的每一条规则时那种所有代码都是自己写的、任何问题都能定位的掌控感真的会让人上瘾。我后来又用这套思路重构了自己的笔记系统和两三个一次性活动页面每次完成都比用重型框架时更快更安心。caveman 的脚本至今仍然只有 200 来行但它承载了我所有公开表达的基础设施从这一点看它比绝大多数智能工具都要称职。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑