资讯动态

Caveman:用Python脚本构建极简静态博客,夺回内容主权

发布时间:2026/10/29 22:21:07 来源:尧图企业网站定制
Caveman 这个名字是我给一个用了六个月的极端项目起的代号。它不指穴居人也不代表什么原始主义运动而是我的一次反抗——在同时被十几个在线工具、三个笔记软件和两套博客框架折腾到几乎崩溃之后我决定往回走用最土、最笨、最原生的办法重新构建一个属于自己的个人站点和知识库。这个项目没有用任何现代前端框架没有依赖任何在线笔记平台没有复杂的自动化流水线。核心就是一台能跑 Python 的电脑、一个文本编辑器、一堆 Markdown 文件以及一个不到两百行的构建脚本。听起来像考古现场但它确实解决了我的核心问题内容永远属于自己、系统永远不会因为厂商停服而报废、写东西的流程短到打开文件就能开始。这篇文章会把整个项目的思路、选型、实操步骤和踩坑记录完整拆开如果你也在被复杂工具折磨或者想找回那种一块木头也能生火的掌控感应该能从这里拿走一套可以直接上手的方法。1. Caveman项目一次对抗工具焦虑的复古实践1.1 项目的缘起被工具绑架之后的反向实验事情的起因很普通。我在过去几年里换过四款笔记软件从最早的本地纯文本到后来的云同步笔记再到带数据库和关系图谱的第二大脑工具。每一款软件刚上手时都让人兴奋但时间一长问题就出来了有的同步开始出错有的收费策略突然变更有的笔记数据导出格式越来越封闭。最让我崩溃的一次是我发现自己依赖的一款在线写作平台的导入导出功能变得异常复杂我花了整整一个周末才把几百篇文章折腾出来格式还丢了一半。那之后我意识到工具越智能、越庞大用户被绑架得越深。我需要一个反方向的方案做一个穴居人级别的系统——像原始人一样用最基础的东西把复杂现代生活里真正重要的内容重新攥在自己手里。于是 Caveman 诞生了。1.2 Caveman不是拒绝科技而是重新认识科技这个项目叫 Caveman容易让人误解为反技术崇拜。其实恰恰相反我用了很多现代工具尤其是 Git 版本控制、Python 脚本和命令行工具它们都是这个项目的骨干。真正的区别在于我选择了低依赖、高掌控的路线尽量让系统由普通的文本文件和简单脚本组成而不是由黑盒服务组成。这种思路和很多极简主义的数字生活实验一脉相承在海外社区里有个相近的概念叫数字园艺和手写博客很多人都在尝试用最少的工具、最透明的流程来维护自己的内容。Caveman 就是这类探索的务实版本不追求回归原始的形式感只求每一步都可理解、可修复、可迁移。1.3 这个项目适合哪些人如果你符合下面任何一条Caveman 的思路会非常适合你。写过博客但被 Hexo、Hugo、WordPress 这些系统折腾过想要更轻量的写作环境。依赖 Notion、印象笔记等平台但担心数据被锁定、编辑体验过于笨重。对命令行和脚本有兴趣想搭一个属于自己的极简工具链。纯粹是厌倦了被各种通知、图表和 AI 助手包围想回到打开文件就写作的安静状态。说到底Caveman 不是一套软件而是一条思路你完全可以不依赖大型工具生态用最朴素的元件打造一个稳定、高效的内容系统。接下来的部分我会从设计原则到具体代码把这条思路完整还原出来。2. 整体设计与方案选型的底层逻辑2.1 三条设计原则最小依赖、纯文本优先、可迁移Caveman 在设计阶段就定死了三条原则之后的每一个选型都围绕它们展开。第一条原则是最小依赖。这里的依赖包括运行依赖、维护依赖和心理依赖。运行依赖是指系统运行需要什么环境——我只要求一台装了 Python 3 的电脑连数据库都不需要。维护依赖是指日后升级和修复的成本——尽量不用第三方库和复杂配置出问题一眼就能看穿。心理依赖是指我对工具的信任——我要求自己对系统的每一个零件都心里有数哪怕有一天开发者生态变了我也能自己维护个十年八年。第二条原则是纯文本优先。所有内容正文都以 Markdown 格式存储不塞进数据库不搞私有格式。纯文本的好处多得说不完任何编辑器都能打开任何系统都能处理Git 可以完美地追踪每一行变更而且永远不会因为软件版本过新打不开老文件。很多人在几年前用 Word 写文档现在打不开了但二十年前用纯文本写的日记现在仍然清晰可读。这就是纯文本的力量。第三条原则是可迁移。我在选型时反复问自己如果这个系统必须搬家我需要多久答案永远是十分钟——因为所有内容都在几十个 .md 文件里构建脚本用 Python 标准库就能跑模板是几个孤立的 HTML 片段。我可以很轻松地把整套东西搬到一个新服务器甚至可以彻底放弃构建脚本直接用文本编辑器手动生成页面。这种随时可以走人的安全感是任何在线服务都给不了的。2.2 技术选型为什么是脚本模板而非静态站点生成器很多人一听要搭个人网站第一反应是去装一个静态站点生成器比如 Hugo、Hexo、Jekyll或者更现代的 Astro。这些工具本身很好但在 Caveman 的项目里我刻意没有使用它们。原因分三方面。其一静态站点生成器本身就是一层抽象而且是一层不小的抽象。以 Hugo 为例它的目录结构、模板语法、shortcode 机制加起来需要学习很多内容。一旦某个主题不满足需求你就要深入源码去找问题。这和Caveman背道而驰——它把简单的事情重新弄得复杂了。其二生成器有固定的数据模型比如一些工具要求使用分段、分类、权重等配置字段这会约束内容的组织方式。而用自写脚本我可以决定文章就是文件路径正文其他什么都不需要。其三我自己的技术栈足够写好一个构建脚本这比去学习一个庞大框架更符合项目的精神——保持掌控。我最终采用的方案本质上是一个约 200 行的 Python 脚本读取 content 目录里的 Markdown 文件解析头部信息和正文套用模板输出静态 HTML 到 public 目录。它没有任何插件系统也没有配置语言却完全够用。2.3 放弃动态网站和在线笔记的理由在早期设计时我也做过参与式讨论是不是应该用 WordPress或者用 Next.js 搭一个带后台的博客最后我全盘否掉了。动态网站的代价是服务器、数据库、安全补丁和漫长的页面加载时间。我写博客只是想发布内容不想每半年担心一次 PHP 版本升级或数据库备份失败。在线笔记平台的代价则是数据所有权和灵活性。如果我把文章存在别人的数据库里即便能导出也始终隔着一层许可。Caveman 的做法是彻底本地化文件和生成结果都留在自己手里发布到服务器时也只是把静态文件丢上去服务器上根本不需要运行任何应用代码自然也不存在被攻击的大面积风险。这套方案的应用效果我六个月跑下来的感受是它不只是一个博客系统更是一个所有内容资产的本地仓库。无论是一篇文章、一张会议纪要还是一个临时记录都统一落到纯文本里统一的、可搜索的、永久的。3. 核心细节解析Caveman的运转方式3.1 目录结构与文件组织Caveman 的项目目录非常简单一眼就能看明白caveman/ ├── content/ │ ├── posts/ │ │ ├── 2024-05-01-hello-world.md │ │ └── 2024-05-15-caveman-notes.md │ ├── pages/ │ │ └── about.md │ └── drafts/ │ └── unfinished-thought.md ├── templates/ │ ├── base.html │ ├── post.html │ └── list.html ├── scripts/ │ └── build.py ├── public/ │ └── (构建后生成的静态文件) ├── assets/ │ ├── style.css │ └── favicon.ico └── README.mdcontent 是内容的唯一来源posts 放文章pages 放固定页面drafts 放草稿。templates 存放 HTML 模板scripts 里是构建脚本public 是构建产物。assets 放样式表等静态资源。这种布局的优点是职责单一、路径即分类。想发一篇新文章只要往 posts 目录里放一个新的 Markdown 文件即可不需要在数据库里插入记录也不需要注册路由。目录本身就是数据库文件系统的层级天然表达了内容的归属。3.2 核心构建脚本把Markdown变成网页构建脚本是全项目的心脏我把它的核心逻辑分成四块遍历文件、解析元信息、转换格式、渲染模板。遍历文件部分采用 Python 标准库的pathlib遍历目录按文件名排序。解析元信息部分不需要引入复杂的 frontmatter 解析库我约定文件头部的三行固定字段title:、date:、desc:用纯手工解析即可两百行以内就能搞定。转换格式部分我使用 Python 的markdown库把 Markdown 文本转换成 HTML。这个库本身是唯一的外部依赖但它是纯 Python 的、广泛使用且常年稳定的。渲染模板部分用 Python 标准库的string.Template做简单的变量替换同样不引入 Jinja2 之类的重型模板引擎。我理解很多读者这会想问为什么不直接用现有的库把这几步组合起来原因很简单如果我用现成的静态生成器就不会真正理解这个系统。自己写过一遍之后我才知道每一步发生了什么以后无论它出什么问题我都能在最底层找到答案。这是穴居人式的固执也是它最稳固的原因。3.3 写作与发布流程设计在 Caveman 里写作和发布是两个完全分离的环节。写作时我只需要用任何文本编辑器打开content/drafts/下的文件沉浸式地写。没有后台编辑器没有富文本工具栏没有自动保存冲突提示。写完后把文件移动到content/posts/目录顺手把文件名改成带有日期的格式然后在命令行运行一条命令python scripts/build.py。这条命令会把所有草稿以外的 Markdown 文件处理成 HTML生成全新的 public 目录。之后用一条 rsync 命令把 public 目录同步到云服务器发布就完成了。整个流程从开始写作到上线最短只要一分钟。这个流程的价值在于它的短。我不需要打开浏览器转悠、不需要登录后台、不需要考虑插件更新。写作和发布之间的摩擦小到接近于零这直接决定了我更新内容的频率——当你不需要面对繁琐流程时会愿意写更多的东西。3.4 搜索与归档的低科技替代方案现代内容系统通常自带全文搜索和标签功能Caveman 里我选择了更笨的方案。搜索方面站点提供的是一个简单的本地搜索脚本利用 Python 的grep逻辑扫描 content 目录返回匹配的文件列表部署后的服务器上也有一个静态索引在构建时自动生成访客可以在网页里用浏览器自带的查找功能做关键词定位。说实话对一个几百篇文章的个人博客来说这个方案已经完全够用。归档方面我不使用数据库式的动态归档页而是按年份生成静态列表。构建脚本读取每篇文章的日期字段生成一个按时间倒序排列的文章目录页。标签页的思路也一样从文章头部的desc和正文里提取关键词生成一个简单的文本清单。这样就没有标签云和关联文章那些花哨功能但胜在页面加载飞快且没有任何交互成本。很多朋友问我为什么不加一个真正的搜索框我反问你觉得你自己博客上的搜索框真正用到的机会有多少大多数来访者是通过搜索引擎点进来又在几分钟内离开的。为一个几乎没人用的功能增加维护负担不值得。4. 实操过程从零搭建属于你的Caveman4.1 第一步初始化目录与文件我建议你从现在开始照着下面的步骤搭一套自己的 Caveman。首先在本地创建一个项目目录并生成最基础的文件夹结构mkdir caveman cd caveman mkdir -p content/posts content/pages content/drafts templates scripts public assets创建完成后在templates/base.html里写一个最简的页面骨架包含 HTML 头、一个导航栏区域、一个内容占位符和页脚。我的第一个模板长这样!doctype html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title$title/title meta namedescription content$desc link relstylesheet href/assets/style.css /head body header a href/$site_title/a /header main $content /main footer p由 Caveman 构建/p /footer /body /html这里$title、$desc、$content等会在构建时被替换成实际内容。为什么用string.Template因为它不会自动执行任何逻辑只做纯粹的字符串替换这让我确信模板不会成为攻击面或意外行为的温床。4.2 第二步编写构建脚本构建脚本build.py是全项目的核心下面这个精简版本是我实际运行过的你完全可以照抄后修改。#!/usr/bin/env python3 Caveman - 极简静态站点构建脚本 import re import shutil from pathlib import Path from datetime import datetime from string import Template import markdown BASE_DIR Path(__file__).resolve().parent.parent CONTENT_DIR BASE_DIR / content TEMPLATE_DIR BASE_DIR / templates PUBLIC_DIR BASE_DIR / public SITE_TITLE 我的Caveman def parse_meta(text: str) - tuple: 解析 Markdown 文件头部元信息 lines text.split(\n) title 未命名 date datetime.now().strftime(%Y-%m-%d) desc content_start 0 for i, line in enumerate(lines): if line.startswith(title:): title line.split(:, 1)[1].strip() elif line.startswith(date:): date line.split(:, 1)[1].strip() elif line.startswith(desc:): desc line.split(:, 1)[1].strip() elif line ---: content_start i 1 break body \n.join(lines[content_start:]) return title, date, desc, body def render_template(template_name: str, variables: dict) - str: path TEMPLATE_DIR / template_name tmpl Template(path.read_text(encodingutf-8)) return tmpl.safe_substitute(**variables) def build_post(path: Path, base_html: str): raw path.read_text(encodingutf-8) title, date, desc, body parse_meta(raw) html_body markdown.markdown(body, extensions[extra, sane_lists, nl2br]) rendered render_template(base.html, { title: title, desc: desc, content: html_body, site_title: SITE_TITLE, }) dest PUBLIC_DIR / posts / path.stem dest.mkdir(parentsTrue, exist_okTrue) (dest / index.html).write_text(rendered, encodingutf-8) def build_index(): ... def build_page(): ... def main(): if PUBLIC_DIR.exists(): shutil.rmtree(PUBLIC_DIR) (PUBLIC_DIR / assets).mkdir(parentsTrue, exist_okTrue) shutil.copy(BASE_DIR / assets / style.css, PUBLIC_DIR / assets / style.css) (PUBLIC_DIR / posts).mkdir(parentsTrue, exist_okTrue) for post in sorted((CONTENT_DIR / posts).glob(*.md)): build_post(post, None) print(f构建完成: {post.stem}) if __name__ __main__: main()需要注意的是上面代码里我留了build_index和build_page的空函数你看着就能明白这是在哪里生成首页和固定页面的。这个脚本里其实暗含一个取舍构建时直接清空 public 目录再重建保证线上永远不会有多余的旧文件。好处是绝对干净、不会留僵尸页面坏处是如果你在服务器上直接构建一瞬间会有一篇请求不到的窗口但我通常是在本地构建好再上传所以这个缺点完全不存在。4.3 第三步设计极简页面模板Caveman 的页面模板走的路线是尽量少、尽量窄一个用于文章页的base.html就够用列表页可以单独用list.html来展示所有文章的标题和摘要。我贴一下自己实际用的列表页片段main h1文章列表/h1 $post_list /main这个$post_list是在构建脚本里生成的 HTML 片段逻辑特别简单遍历内容目录输出一个带链接的列表。每个条目就两样东西日期和标题点击进去就是正文页。这里我没有做分页也没有做分类筛选因为对个人博客来说全文罗列反而更尊重信息本身——任何一篇文章都在列表里不会有文章因被精选而沉底。样式方面我写了一份不到 80 行的style.css参考了瑞士国际主义排版的基本路数系统字体栈、白底黑字、留白充足、链接下方画下划线。为了让它在手机上也能舒服地看我加了一条max-width: 720px; margin: auto;的处理这样整个页面不管在什么设备上都能保持一栏的阅读宽度。我建议你也把样式做得尽量克制因为 Caveman 的意义不在视觉炫技而在内容持久。4.4 第四步部署与更新部署用的是最直接的 rsync。我有一台位于普通机房的小型云服务器不需要任何额外的软件环境因为线上只需要一个静态文件服务用 Nginx 就能承载。构建完成后我在项目目录里运行rsync -avz --delete public/ userserver:/var/www/caveman/这条命令会把整个 public 目录同步到服务器--delete参数保证在本地删掉的文件线上也不会残留。更关键的是服务器上没有任何应用程序进程没有数据库连接没有可以注入的漏洞点。运行三个月的统计显示我的 Nginx 访问日志里几乎没有异常扫描尝试不像过去跑 WordPress 时每天都能看到几十次攻击探测。这就是把动态逻辑全部挪到本地构建端的最大好处——公网只需要展示静态文件不需要计算任何东西。4.5 第五步日常写作与维护日常写作流程已经被我练成了肌肉记忆。打开终端进入 Caveman 目录在content/drafts/里新建一个带当日日期的 Markdown 文件正文第一行写title:空一行后开始打字。写完后移动文件到posts/目录运行python scripts/build.py再执行那条 rsync 命令。全程不超过两分钟没有任何打断心流的环节。我甚至会为了无打扰故意不用 GUI 编辑器直接在本地的 Sublime Text 里打开 Markdown 文件编辑一屏之内搞定所有操作。有些朋友会问那图片怎么办很简单——把图片放在内容旁边的一个images目录里文中用相对路径引用构建时同步复制到 public。这也是最原始而可靠的资产管理方式图片永远和你文章同在一个仓库里永远不会丢失。5. 常见问题与避坑指南5.1 常见问题速查表在我把 Caveman 分享给十多个朋友之后大家实际跑起来遇到的最集中问题我整理成了一张速查表你直接按图索骥就行。问题现象原因解决方案构建后页面全是$title没被替换模板里使用了Template但没调用safe_substitute检查渲染函数必须用safe_substitute而非substituteMarkdown 代码块里出现错位Python 的markdown库需要开启extra扩展markdown.markdown(text, extensions[extra, sane_lists])中文文件名在 rsync 后乱码本地与服务器文件系统编码不一致所有内容文件名统一用英文加日期中文只出现在正文里本地能打开页面线上 404相对路径写成了绝对路径链接统一使用根路径/posts/xxx/格式草稿误发布文件放在了posts/而不是drafts/构建脚本里跳过drafts路径发布前检查文件位置这份速查表里的每一条都是我或是朋友实际踩过的。特别是第一条很多人刚上手时用substitute就会因为模板里有未定义变量直接抛异常而safe_substitute则不会它会安静地保留占位符这更容易暴露问题。5.2 我踩过的坑和总结的窍门第一个真正的坑是我早期试图给构建脚本加标签系统。我天真地在 Markdown 头部加了一个tags:字段然后在构建脚本里写了一大段逻辑来生成标签页。结果标签页反复出问题而且写着写着发现自己在解决一个根本不存在的问题——我的文章量不到一百篇标签的意义实在有限。花了一个下午做完后第二天就把它从代码里咔嚓掉了。不要为了功能而功能需求要跟着自己的真实使用习惯走。第二个坑是备份。我一开始觉得反正文件都在本地没什么可丢的直到有一次我误删了一个content/posts/目录幸好 Git 提交历史里还有记录才救回来。从那以后我给 Caveman 定了三重备份规则Git 仓库推到代码托管平台、外接硬盘每周冷备份一次、重要文章还额外导出成一份纯文本放在网盘里。任何系统都需要备份纯文本的好处是备份成本低到可以忽略。第三个窍门是我在写博客列表页时总结出来的列表页只显示标题加日期不要显示摘要。很多系统默认显示摘要反而逼着读者做额外判断。在 Caveman 里一段极简的列表让用户能纯粹地按时间浏览想看哪篇点哪篇回归了目录的本质属性。这个方法的手段极其简单但阅读体验比那些堆满了摘要和标签的列表好了非常多。最后说说对这个项目的后续想法。我现在正在给 Caveman 加一个简单到极致的评论机制不是接入任何现成论坛框架而是让每篇页面底部提供一个相对路径的投稿链接。访客通过这个链接发送的信息会被一个极轻的小程序收集后转成邮件发给我我再手动选择是否将它追加到文章底部的回应列表里。这个方案完全不依赖第三方服务隐私关系干净也保留了对话的克制感和筛选权。这样一篇原始博客反而比很多现代系统的交流更加真实和有态度。我在实际使用中发现Caveman 解决的不只是技术问题更重要的是它给了我一种内容主权的心理安全。六个月前我开始做这件事时只是想摆脱工具瘫痪的焦躁现在回头看这个极简系统成了我最稳定的数字基础设施。它让我愿意重新开始写长文愿意把很多想法落在文本里而不是散落在各种软件的碎片记录中。如果你也被工具压得喘不过气不妨试着亲手搭一套自己的 Caveman不需要多复杂从一篇 Markdown 和一个小脚本开始就行。

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

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

免费获取报价 →
↑