资讯动态

用150行Python手写零依赖静态博客生成器,告别WordPress焦虑

发布时间:2026/10/29 21:28:50 来源:尧图企业网站定制
去年冬天我把用了六年的WordPress 博客彻底关掉了换成了现在这套由一百多行 Python 脚本生成的静态站。我给这套小工具起了个名字叫caveman直译就是“穴居人”。起这个名字不是想装酷而是想给自己立个规矩写博客这件事归根结底只需要一根趁手的棍子和一块靠得住的石头不需要一个不断膨胀的工具全家桶。你可能会觉得我在小题大做但如果你也经历过从 WordPress 换到 Hexo再从 Hexo 换到 Hugo最后发现写一篇文章的时间有一大半花在“先让后台跑起来”这件事上大概就能明白我为什么要走到这一步。这篇东西不是什么大项目复盘只是想把我做caveman的完整思路、踩过的坑、以及它现在给我带来的真实体验记录下来给那些同样被博客工具折磨得喘不过气的人一个参考。1. 从“部落迁徙”说起我为什么丢掉 WordPress反而留下了 caveman先说清楚我原来的处境。我写博客断断续续写了大概六年中间用过传统动态方案也追过一轮静态站生成器。听起来好像很折腾但实际上每一次迁移都不是因为内容变多了而是因为工具先撑不住了。1.1 写这篇文章的真实导火索真正让我下决心的机缘是又一次插件升级后整个后台白屏。我花了两个多小时查原因最后发现是某个缓存插件和老主题不兼容。那一刻我忽然问自己我到底在维护一个博客还是维护一个随时可能爆炸的软件系统WordPress 那条路我走得很深数据库、插件、主题、子主题、CDN、对象缓存每一层我都能说出个一二三来。但问题恰恰在这里——你越熟悉就越容易陷入“配置它”而不是“用它写作”的状态。今天改一下侧边栏明天调一下字体后天研究怎么合并 CSS 文件等把这些折腾完写文章的热情早就凉透了。后来我也试过静态站生成器。Hexo 时代,我已经觉得挺轻了但它的更新频率、主题生态和 Node 依赖链依然会时不时给你来一个“npm install 失败”。Hugo 更夸张安装包倒是单文件速度也快但当我想改一点小逻辑时必须去读 Go 模板的语法。我渐渐意识到不是我学不会而是这些工具对“只想安静写点东西”的人来说真的太重了。于是我做了个决定自己写一个发布系统只用一种我闭着眼都能改的语言只依赖标准库任何第三包都不装。它要做的事情很简单——把一堆 Markdown 文件变成能放到服务器上的 HTML 网页没了。1.2 这个项目到底适合谁如果你符合下面几条我觉得caveman的思路可能会对你有用你写内容为主但已经厌倦了后台转圈、插件互掐、主题升级变样。你有点动手能力能接受“先写脚本再写作”这层薄薄的代价。你手头有一台最低配的云主机或者愿意用现成的静态托管服务。你希望十年后网站还能跑不依赖某个框架是否继续维护。反过来如果你需要完整的后台编辑体验、多人协作和实时预览那活该用现成的管理系统。caveman的前提是你愿意向“纯粹的文字工作流”靠拢而不是像一个编辑部那样运作。2. caveman 的设计原则拒绝依赖把心智负担砍到最低当我决定自己写这套系统时第一件事不是急着写代码而是先立规则。因为我知道如果它又变成一个充满抽象概念的大项目那和之前扔掉的那些工具没有区别。2.1 零依赖不等于零代码caveman的第一个原则是零第三方依赖。所有代码只用 Python 标准库也就是说任何一台装了 Python 3 的机器都能直接跑不需要pip install任何东西不需要维护requirements.txt更不存在“某个依赖库因为作者退坑而停更”的问题。有人可能会说Markdown 解析总得用库吧总不能自己写吧。我就是自己写的而且只写了实现最小功能子集的解析器。这不是因为我不信任第三方库而是我清楚地知道如果我引入一个功能完整的 Markdown 库就意味着我要用一个复杂度远超我需求的东西。之后文档、配置、版本兼容性都会变成新的负担。零依赖带来的好处是即时可见的换机器、重装系统、从云服务器迁移到家里那台小主机都只需要把整个目录打包拷走。没有环境变量、没有数据库迁移、没有运行环境版本锁。我在一台旧笔记本上跑在一台 2015 年的低配小主机上跑在朋友的电脑上跑结果一摸一样。2.2 全部代码控制在 150 行以内第二原则是代码量必须控制在 150 行以内。这个数字不是随便定的而是我给自己设的“可读性边界”。一个文件、150 行以内、没有任何隐藏的模块依赖这意味着我可以在任意时间把整个系统读一遍知道每一行在干什么。我见过很多人写自己的工具一开始很清晰后来为了支持更多功能不断加代码最后变成一万多行的怪物然后新写一套“精简化工具”。我把这条线画死了功能宁可少不可代码多。如果某个需求会让代码明显超过 150 行我就去精简别的写法或者干脆放弃那个需求。这带来一个很实际的好处所有逻辑都透明。我不会在某个深夜改了一处行为后就忘记它在哪也不会因为“这次升级”导致旧文章显示错乱。caveman至今没有产生过“版本兼容”的焦虑因为它的逻辑全部摊在眼前。2.3 内容与表现彻底分离caveman的内容全部是 Markdown 文件一个个.md文件躺在content/目录里。页面长相只由template.html控制。我不需要数据库不需要后台管理面板不需要评论系统。评论我暂时是关掉的。如果你想留言可以事后用一个外部脚本指向链接或者干脆让读者发邮件。这样做不是反社交而是为了保住“发布系统极简”这个大前提。每加一类功能我都要付出额外的维护成本。在没有明显需求之前把所有的口子都封住才是最舒服的状态。这种内容与表现完全分离的方式也让“搬家”变成了一件极低压的事。如果哪天我不想用自建服务器了只要把content/目录推到任何一个静态托管平台网站就照常运行。没有任何锁定效应。3. 核心实现150 行 Python把 Markdown 变成静态站点接下来是具体实现。我尽量把思路讲完整代码只贴关键片段因为完整版有点长而且很多细节要根据你自己的偏好调整。3.1 目录结构设计成一个最小副本caveman的目录最初长这样caveman/ ├── caveman.py ├── template.html ├── content/ │ ├── 2025-11-02-markdown-guide.md │ ├── 2025-12-18-caveman-project.md │ └── draft/ └── output/content/存放 Markdown 文章子目录draft/放未发布的草稿。output/是生成结果每次运行脚本都会先清空再重建。template.html是唯一的页面模板控制整站的外观。caveman.py是唯一的工具脚本。这个结构我再解释一下内容、模板、产物三块完全分开。output/里的东西是纯 HTML可以直接丢给任何静态服务器。content/里永远只有文本和图片资源备份起来就是一个目录拷贝。3.2 最小可用的 Markdown 解析器写解析器前我统计了自己以往文章里用的 Markdown 语法发现常被用到的只有几种标题、段落、代码块、链接、列表偶尔用到粗体和斜体。这就够了。解析的核心是一个简单的逐行扫描函数def md_to_html(text): lines text.splitlines() in_code False out [] for line in lines: if line.startswith(): in_code not in_code out.append(precode if in_code else /code/pre) elif in_code: out.append(html.escape(line)) elif line.startswith(# ): out.append(h1 html.escape(line[2:]) /h1) elif line.startswith(## ): out.append(h2 html.escape(line[3:]) /h2) elif line : out.append() elif line.startswith(- ) or line.startswith(* ): out.append(li html.escape(line[2:]) /li) elif line.startswith( ): out.append(pre html.escape(line[4:]) /pre) else: out.append(p html.escape(line) /p) return \n.join(out)这个解析器当然称不上严谨嵌套列表、引用块、表格都没有。但它对我足够可靠因为我会刻意按它的能力范围写文章。比如需要代码块就一定用三个反引号包起来需要列表就连行写不在代码块里放空行。先把语法面缩窄写作习惯自然跟着收敛换来的却是每篇文章的渲染效果完全可控。对于行内链接和加粗我会加一个简单的正则替换line html.escape(line) line re.sub(r\[([^\]])\]\(([^)])\), ra href\2\1/a, line) line re.sub(r\*\*([^*])\*\*, rstrong\1/strong, line)注意顺序先转义再加链接。否则链接里的之类会被吃掉。这个坑后面细说。3.3 首页与文章页的生成逻辑每篇文章的文件名里带了日期但为了保险我还是在文件头部加了极简元信息格式如下--- title: 你好洞穴 date: 2025-12-28 ---脚本扫描content/下所有.md文件解析这段元信息然后做两件事第一每篇文章生成一个独立的 HTML 页面。文件名我用日期加序号不以中文标题作为路径比如20251228-001.html。这样以后改标题链接也不会坏。第二把所有文章按日期倒序排生成一个index.html首页只显示标题和日期点击进入正文。生成正文页时模板替换大概长这样page_html template.replace({{TITLE}}, title) .replace({{DATE}}, date) .replace({{CONTENT}}, content_html)首页则是把所有文章的标题列成链接items .join(flia href{url}{title}/a span{date}/span/li for post in posts) index_html template.replace({{TITLE}}, 首页) .replace({{CONTENT}}, ful{items}/ul)就这么简单。全文生成时间在一百篇文章的场景下不到一秒肉眼根本感知不到静态站“生成”这个动作的存在。4. 部署与发布流程一台最低配云主机就能跑起来代码写完以后真正的考验来了网站怎么上线日常怎么更新。这里我选了所有人都会用的老办法但把它精简到了只要几个键。4.1 三秒发布本地生成然后 rsync我的发布流程到今天仍然是三句话cd ~/caveman python3 caveman.py rsync -av --delete output/ userserver:/var/www/caveman/解释一下这几个动作第一条进目录第二条重新生成所有页面第三条把本地output/的内容增量同步到服务器上。这里用--delete是有讲究的——它保证服务器上那些被删除或修改过的旧文件也会同步删除不会出现“文章都删了页面还在”的幻影。为什么选rsync而不是用 Git hook 或者 CI 自动构建因为我的需求是“可控的本地发布”不是“每个分支推送都自动部署”。rsync足够直接传输前会做文件对比几百个 HTML 实际只传几个新增的速度快到经常一晃就结束。而且它不依赖任何第三方服务服务器上只要有一个能 SSH 登录的账号就能用。4.2 静态服务器的 nginx 配置服务器上我跑的是 nginx配置文件短到不需要注释server { listen 80; server_name example.com; root /var/www/caveman; index index.html; location / { try_files $uri $uri/ 404; } }try_files这行在我看来是静态站最核心的配置。它首先找精确文件找不到就找目录下的index.html再找不到就返回 404不会把任何请求转交给后端程序。因为caveman产出的都是纯静态文件这样的配置已经接近理论上的最简了。静态站的好处在这里体现得最明显没有数据库进程驻留没有 PHP-FPM 需要监控没有 502 错误没有内存泄漏。一台 1 核 512M 的小主机跑一个这样的博客日常负载几乎可以忽略。如果你不想自己维护服务器也可以直接把output/推到各种静态托管平台原理完全一样。我选择自建只是因为喜欢折腾最少哪怕将来服务器到期我迁移的成本也只是把文件重新传到一个新 IP 上。4.3 外出时怎么坚持更新还有一个实际问题人不会永远坐在电脑前。我出门时也会想写东西但我的方案不是去搞一个网页后台而是在手机上写草稿同步回电脑再发布。手机端我只用一个支持 Markdown 的笔记应用写完存成.md文件通过 Git 仓库同步到本地。整个过程的目标很明确手机上只负责产生文字不负责发布。发布永远回电脑上执行那一条命令。这个看似“绕路”的方案反而帮我过滤了很多不宜发布的内容。等我回到电脑前那些草稿往往已经冷静了一晚我会在发布前再读一遍删掉废话再生成。这算是个意外收获。5. 迭代中的三个坑标题转义、中文文件名、缓存问题任何工具都是靠踩坑长大的caveman也不例外。下面这三个问题是我在实际使用中碰到的每一个都花了时间去排查把它们写出来是想让你少走弯路。5.1 标题里的特殊字符把页面搞乱了有一次我写了一篇标题叫“Python 3.10 的兼容性整理”的文章。生成页面后在浏览器里打开发现整个页面结构都歪了一部分正文变成了粗体横线一部分直接消失。我一开始以为是模板写错了检查半天才发现问题出在标题里的字符。它在进入 HTML 模板时没有被转义浏览器自作主张地把后面一长串内容当成了 HTML 标签直到碰到另一个才“关掉”。排查方法是直接查看浏览器里的“查看页面源代码”看到标题附近出现了意料之外的标签结构。修复方式就是在所有插入模板的文本上统一调用html.escape()。从那以后我给自己定了规矩不管内容来自哪里凡是放进 HTML 的变量一律先转义。5.2 中文文件名在服务器上的怪问题项目早期我为了让 URL 更好读直接用中文标题生成文件名比如你好洞穴.html。在本地跑得好好的传到 Linux 服务器上就出了一些诡异现象有的浏览器能打开有的手机会下载文件而不是打开页面某些 CDN 还会返回 403。原因很复杂不完全是服务器的问题但最简单的解决办法是别让 URL 依赖中文。现在生成的路径是纯日期加序号例如20251228-001.html。文件名本身依然可以是中文这样我在本地管理时还能一眼认出是哪篇文章但 URL 永远保持 ASCII彻底避开字符编码和服务器地区的坑。这个改动也让链接变得非常稳定。以后就算我改文章标题URL 不会变化外部引用不会失效。5.3 重新生成时遇到旧的 HTML 残留刚上线那会儿我在本地删了一篇文章重新运行caveman.py然后同步到服务器结果那篇文章的页面还能访问。我一度以为 rsync 没生效后来才发现是output/目录里还留着旧文件而 rsync 的--delete只在源端没有该文件时才会删除目标端对应文件源端里还有残留自然删不掉。修复办法很简单每次生成前先清空output/目录脚本开头加一行shutil.rmtree(OUTPUT_DIR, ignore_errorsTrue) os.makedirs(OUTPUT_DIR, exist_okTrue)这样每次生成都是从零开始上一次的产物不可能留到下次。后来我又顺手处理了浏览器缓存问题在生成的每个 HTML 页面的head里加入一段禁止缓存的小标签。不管服务器怎么更新浏览器总会强行拿到最新版本。这看起来都是细节但对一个只想安静写作的人来说稳定的“所见即所得”特别重要。我不想每次更新完还要担心读者看到的是不是上一版。6. 用下来的真实感受caveman 解决的不只是写博客的问题现在这套系统至少稳定跑了几个月。我不敢说它是史上最优雅的动态站架构但它给我的体验确实完全不一样了。6.1 写作效率回到了“打开记事本”的程度我现在从打开电脑到新文章上线大概只需要三分钟写 Markdown关掉文件输入那两行脚本命令再等一秒钟完事。没有后台等待没有自动保存冲突没有插件更新提醒。工具退到了它本该在的位置——一个不起眼但可靠的助手而不是一个需要你不断讨好、维护的大爷。这种变化最直观的体现是我的草稿箱里积压的文章开始变少了因为脑子里的想法可以直接变成成品而不是一想起“还要登录后台、排版、插入图片、选分类、填摘要”就索性搁置。6.2 这套思路还能往哪儿延伸caveman虽然诞生于写博客的需求但同样的逻辑完全可以迁移到别处团队内部知识库几十个 Markdown 文件一条命令生成静态站放到内网服务器上。产品更新日志每次发版写一个.md文件自动按日期生成页面。个人项目主页一个模板、几个文件比任何页面生成器都好维护。数字极简主义的核心不是“什么都没有”而是每一件剩下东西都必须经得起追问。我留在这套系统里的每一项都能回答“没有它会怎样”这个问题。6.3 如果你想自己做一套“穴居人工具”最后给你一个最直接的起点。别急着买域名、找服务器、写宏大需求。先把自己写过的一批文章导出成 Markdown 文件然后打开电脑上的编辑器模仿上文里的脚本让这些文件能够变成一个简单的本地网页。只要这一步通了后面的部署、优化、加功能都只是顺手的事。你不需要用我的代码也不需要完全认可我的取舍。caveman真正的价值在于逼你重新思考你以为你需要的和你真正需要的之间的距离到底有多大。想通这一点比搞定一百套工具都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑