资讯中心

放弃WordPress,用Flask+SQLite手搓日更站实操记录

📅 2026/9/26 7:31:45
放弃WordPress,用Flask+SQLite手搓日更站实操记录
1. 为什么我放弃了 WordPress转头用 Flask SQLite 手搓了一个日更站去年年底我给自己定了个目标每天写一篇行业观察坚持一年。最开始我图省事直接上了 WordPress主题一装、插件一堆看着挺美。结果第三周就出问题了——插件更新把页面搞崩了数据库被某个统计插件拖慢到打开一篇文章要等四秒。我那时候才意识到对于一个只想安安静静日更的个人站来说WordPress 这套重型装备其实是负担。后来我把目光转向了 Flask SQLite 这套组合。原因很直接Flask 足够轻一个app.py就能跑起来SQLite 是单文件数据库备份就是复制一个.db文件迁移服务器的时候直接拖走就行。整套东西没有后台管理面板的臃肿也没有插件生态的不可控每一行代码我都知道它在干什么。这篇文章就是我把这套站从零搭起来、并且真的做到日更的完整实操记录包括踩过的坑、参数怎么调、数据怎么管以及日更这件事在技术层面到底需要哪些支撑。如果你也是那种不想被平台绑架、想自己掌控内容的人或者你正在学 Flask 想找个真实项目练手那这篇内容应该对你有用。我会把每一步的选择理由讲清楚而不是只丢一堆代码让你抄。毕竟建站这件事抄代码只能跑通一次理解逻辑才能跑通一年。2. 技术选型Flask、SQLite 和 WorkBuddy 各自扮演什么角色2.1 为什么是 Flask 而不是 Django 或 FastAPI很多人一提到 Python 建站就想到 Django但 Django 的 ORM、Admin、中间件这套东西对个人日更站来说太重了。我算过一笔账Django 项目初始化后光是自带的表就有十张而我整个站真正需要的表只有三张——文章表、标签表、访问记录表。用 Django 就像开卡车去菜市场买菜能拉但没必要。FastAPI 我也试过它的异步性能确实好但它的强项在 API 服务模板渲染这块生态不如 Flask 成熟。我要的是一个能直接返回 HTML 页面的传统网站Flask 的 Jinja2 模板加上render_template就是最顺手的方案。而且 Flask 的扩展机制很克制需要什么装什么不需要就不装这种按需加载的哲学特别适合个人项目。具体到版本我用的是 Flask 3.0.x。这个版本对 Python 3.8 以上都支持我本机是 Python 3.11装的时候直接pip install flask就行。这里有个小细节如果你用的是 Windows建议在虚拟环境里装别直接装到全局不然以后多个项目依赖冲突会很头疼。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install flask2.2 SQLite 在日更场景下的真实表现SQLite 经常被误解成玩具数据库但实际上它在读多写少的场景下表现非常稳。我的站每天写一篇文章写入操作一天就一次但读取操作可能有几百上千次包括我自己预览、爬虫抓取、读者访问。这种读写比例下SQLite 完全够用甚至比 MySQL 还快因为它没有网络开销直接读本地文件。不过有几个参数必须调不然并发一上来就会遇到database is locked的错误。我在app.py里加了这段配置import sqlite3 def get_db(): conn sqlite3.connect(blog.db, timeout10) conn.execute(PRAGMA journal_modeWAL) conn.execute(PRAGMA synchronousNORMAL) conn.row_factory sqlite3.Row return connjournal_modeWAL是关键它把写操作和读操作分离到不同的文件读的时候不会被写阻塞。timeout10是等锁的超时时间默认 5 秒有时候不够。row_factory设成sqlite3.Row之后查询结果可以像字典一样用列名访问写模板的时候方便很多。2.3 WorkBuddy 在这个流程里到底帮我做了什么WorkBuddy 这类工具的核心价值不是替你写代码而是帮你把重复性的操作流程固化下来。我日更的流程是这样的早上想到一个选题打开编辑器写 Markdown然后需要把 Markdown 转成 HTML、插入数据库、生成标签、更新首页列表。这一套动作如果手动做每天要花十五分钟。用 WorkBuddy 把这些步骤串成一个自定义指令之后我只需要把 Markdown 文件丢进去剩下的它自动跑完。我配置的指令大概长这样读取指定目录下的.md文件解析 front matter 里的标题和标签调用 Python 脚本写入 SQLite然后触发一次静态页面重新生成。整个过程不需要我打开终端敲命令。这就是工具的意义——把我知道怎么做但懒得每天做的事情自动化掉。3. 从空目录到能访问的站点环境搭建的完整链路3.1 Python 环境与依赖安装的坑Python 安装本身没什么好说的官网下载安装包一路下一步就行。但有两个地方容易出问题一是 Windows 上安装时记得勾选 Add Python to PATH不然后面在命令行里敲python会提示找不到命令二是如果你电脑里已经有好几个 Python 版本建议用py -3.11这种带版本号的方式调用避免装错地方。依赖方面我整个项目只用了三个包Flask、Markdown把 Markdown 转 HTML、python-frontmatter解析文章头部的元信息。用requirements.txt管理Flask3.0.0 Markdown3.5.1 python-frontmatter1.0.0安装命令就是pip install -r requirements.txt。这里提醒一句如果你在国内网络环境下装包慢可以换用国内镜像源加上-i https://pypi.tuna.tsinghua.edu.cn/simple参数速度会快很多。3.2 项目目录结构怎么设计才不乱我见过很多人建站建到一半文件到处乱放最后自己都找不到东西。我的目录结构是这样的myblog/ ├── app.py # 主程序 ├── blog.db # SQLite 数据库 ├── requirements.txt ├── content/ # Markdown 源文件 │ ├── 2024-01-01.md │ └── 2024-01-02.md ├── templates/ # Jinja2 模板 │ ├── base.html │ ├── index.html │ └── post.html ├── static/ # CSS、JS、图片 │ └── style.css └── scripts/ # 辅助脚本 └── import_posts.pycontent目录放原始 Markdown这是内容的真相来源。数据库只是索引和缓存万一数据库坏了我随时可以从 Markdown 重新生成。这种设计的好处是内容永远不会丢而且 Markdown 文件可以直接用 Git 管理每次更新都有记录。3.3 数据库表结构三张表撑起整个站我的数据库只有三张表设计得很克制CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT UNIQUE NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, summary TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); CREATE TABLE post_tags ( post_id INTEGER, tag_id INTEGER, PRIMARY KEY (post_id, tag_id), FOREIGN KEY (post_id) REFERENCES posts(id), FOREIGN KEY (tag_id) REFERENCES tags(id) );slug是文章的 URL 标识我用日期加拼音生成比如2024-01-01-workbuddy-build-site。用 slug 而不是 id 做 URL一是对搜索引擎友好二是以后换数据库的时候链接不会失效。post_tags是多对多关系表一篇文章可以有多个标签一个标签也可以对应多篇文章。注意SQLite 默认不强制外键约束需要在连接时执行PRAGMA foreign_keysON才会生效。这个坑我踩过删文章的时候标签关联没清掉导致标签页出现空链接。4. 日更流程的自动化从写 Markdown 到页面更新4.1 Markdown 文件的格式约定为了让脚本能自动解析我给每篇 Markdown 定了统一的头部格式--- title: 用 WorkBuddy 从零建站并日更实操记录 date: 2024-01-01 tags: [Flask, SQLite, 建站] summary: 记录我用 Flask 和 SQLite 搭建个人站并坚持日更的完整过程 --- 正文内容从这里开始...这个头部就是 front matter用---包起来。python-frontmatter这个库可以直接把它解析成字典我拿title和tags就能用不需要自己写正则去匹配。日期用YYYY-MM-DD格式排序的时候直接字符串比较就行不用转成时间对象。4.2 导入脚本的核心逻辑导入脚本做的事情很线性遍历content目录解析每个 Markdown 文件检查数据库里是否已存在同 slug 的记录存在就更新不存在就插入。核心代码大概三十行import frontmatter import markdown import sqlite3 import os from datetime import datetime def import_posts(content_dircontent, db_pathblog.db): conn sqlite3.connect(db_path) conn.execute(PRAGMA foreign_keysON) for filename in sorted(os.listdir(content_dir)): if not filename.endswith(.md): continue filepath os.path.join(content_dir, filename) post frontmatter.load(filepath) slug filename.replace(.md, ) title post.get(title, Untitled) tags post.get(tags, []) summary post.get(summary, ) html_content markdown.markdown(post.content) # 插入或更新文章 conn.execute( INSERT INTO posts (slug, title, content, summary) VALUES (?, ?, ?, ?) ON CONFLICT(slug) DO UPDATE SET titleexcluded.title, contentexcluded.content, summaryexcluded.summary, updated_atCURRENT_TIMESTAMP , (slug, title, html_content, summary)) # 处理标签 post_id conn.execute( SELECT id FROM posts WHERE slug?, (slug,) ).fetchone()[0] for tag_name in tags: conn.execute( INSERT OR IGNORE INTO tags (name) VALUES (?), (tag_name,) ) tag_id conn.execute( SELECT id FROM tags WHERE name?, (tag_name,) ).fetchone()[0] conn.execute( INSERT OR IGNORE INTO post_tags (post_id, tag_id) VALUES (?, ?), (post_id, tag_id) ) conn.commit() conn.close()ON CONFLICT(slug) DO UPDATE是 SQLite 3.24 以上支持的语法相当于有则更新无则插入比先查再判断要简洁。标签用INSERT OR IGNORE避免重复插入报错。4.3 WorkBuddy 自定义指令的配置思路WorkBuddy 的指令配置本质上是把一串操作定义成一个可复用的动作。我配的指令包含三个步骤第一步执行python scripts/import_posts.py第二步检查执行结果有没有报错第三步如果有新文章就输出提示。这样我每天写完 Markdown 保存后只需要触发一次指令数据库就更新好了。这里有个经验指令里最好加上错误处理。比如脚本执行失败的时候WorkBuddy 应该把错误信息完整显示出来而不是只报一个执行失败。我在脚本里加了 try-except把异常信息打印到标准输出这样在 WorkBuddy 的日志里就能看到具体是哪一行出的问题。5. 页面渲染与前端交互让 Flask 把数据吐到网页上5.1 Jinja2 模板的继承机制Flask 用的是 Jinja2 模板引擎它的继承机制能省掉大量重复代码。我建了一个base.html作为所有页面的骨架!DOCTYPE html html head meta charsetUTF-8 title{% block title %}我的日更站{% endblock %}/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body header h1a href/我的日更站/a/h1 /header main {% block content %}{% endblock %} /main footer p坚持日更的第 {{ day_count }} 天/p /footer /body /html然后index.html和post.html只需要继承这个骨架填充content块就行。url_for(static, filenamestyle.css)是 Flask 生成静态文件 URL 的标准方式这样即使以后改了静态文件路径模板也不用动。5.2 首页列表与文章详情页的数据查询首页需要展示文章列表按时间倒序排列每篇显示标题、摘要和日期。查询语句很简单app.route(/) def index(): conn get_db() posts conn.execute( SELECT slug, title, summary, created_at FROM posts ORDER BY created_at DESC LIMIT 20 ).fetchall() conn.close() return render_template(index.html, postsposts)文章详情页需要根据 slug 查单篇同时把标签也查出来app.route(/post/slug) def post_detail(slug): conn get_db() post conn.execute( SELECT * FROM posts WHERE slug?, (slug,) ).fetchone() if post is None: abort(404) tags conn.execute( SELECT t.name FROM tags t JOIN post_tags pt ON t.id pt.tag_id WHERE pt.post_id ? , (post[id],)).fetchall() conn.close() return render_template(post.html, postpost, tagstags)abort(404)是 Flask 内置的当文章不存在时直接返回 404 页面比手动构造响应要规范。5.3 标签页和归档页的扩展标签页的逻辑是先列出所有标签点击某个标签后展示该标签下的所有文章。归档页则是按月份分组展示。这两个页面我一开始没做后来发现读者需要按主题找文章才补上的。实现方式就是多写两个路由和对应的查询语句没有太复杂的地方。这里分享一个 SQLite 的小技巧查标签下文章数量的时候用GROUP BY加COUNT一次查出来比循环里逐条查要快得多SELECT t.name, COUNT(pt.post_id) as count FROM tags t LEFT JOIN post_tags pt ON t.id pt.tag_id GROUP BY t.id ORDER BY count DESC6. 部署上线让站点真正能被别人访问6.1 开发服务器不能直接用于生产Flask 自带的app.run()是开发服务器单线程、性能差、没有安全防护绝对不能直接暴露到公网。我一开始图省事用开发服务器跑了两天结果有次访问量稍微大一点就卡死了。后来换成 Gunicorn 才稳定下来。Gunicorn 的启动命令gunicorn -w 4 -b 127.0.0.1:8000 app:app-w 4是开四个工作进程一般设成 CPU 核心数的两倍。-b指定绑定地址这里绑到本地 8000 端口前面再用 Nginx 做反向代理。6.2 Nginx 反向代理配置Nginx 负责处理静态文件、转发动态请求、做 HTTPS 终止。配置大概是这样server { listen 80; server_name yourdomain.com; location /static/ { alias /path/to/myblog/static/; expires 30d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }静态文件交给 Nginx 直接返回不走 Flask这样能省下不少进程资源。expires 30d让浏览器缓存静态文件三十天减少重复请求。6.3 数据库备份与迁移的实操SQLite 的备份简单到令人发指——直接复制.db文件就行。但要注意如果站点正在运行直接复制可能会拿到不一致的状态。正确做法是用 SQLite 自带的备份命令sqlite3 blog.db .backup backup/blog_$(date %Y%m%d).db这个命令会在事务层面保证备份的一致性。我设了个定时任务每天凌晨三点自动备份一次保留最近三十天的备份文件。迁移服务器的时候把.db文件和content目录一起打包带走新服务器上装好依赖、配好 Nginx十分钟就能恢复。7. 日更一年后我总结出的几条硬经验7.1 内容与展示分离是长期维护的关键我最大的体会是Markdown 文件是内容的唯一真相来源数据库和网页都只是它的投影。这个原则让我在一年里换了两次服务器、改了三版模板但内容从来没有丢过。每次迁移只需要把content目录和数据库文件搬过去重新跑一遍导入脚本所有文章就都回来了。如果你把内容直接存在数据库里那数据库一坏就全完了。而 Markdown 文件是纯文本用任何编辑器都能打开用 Git 能追踪每一次修改这种安全感是数据库给不了的。7.2 自动化要适度别为了自动化而自动化我一开始想把所有事情都自动化包括自动抓取热点、自动生成摘要、自动配图。后来发现这些智能功能带来的维护成本远大于收益。自动摘要经常抓错重点自动配图出来的东西跟内容不搭最后我还得手动改一遍反而更费时间。现在我保留的自动化只有两件事Markdown 导入数据库、静态文件缓存。其他的都手动做。手动写摘要虽然花两分钟但质量可控手动选配图虽然花五分钟但至少不会出错。自动化的边界应该是重复且确定的操作而不是需要判断的操作。7.3 性能优化先看查询再看缓存站点跑起来之后我做过一次性能排查。用 Chrome 的开发者工具看首页加载要 800 毫秒其中 600 毫秒花在数据库查询上。我一开始想加 Redis 缓存后来发现真正的问题是首页查询没有加索引。posts表的created_at字段没有索引每次排序都要全表扫描。加了一个索引之后查询时间从 600 毫秒降到 20 毫秒。CREATE INDEX idx_posts_created_at ON posts(created_at DESC);这个经历告诉我优化要先定位瓶颈别一上来就上重型方案。SQLite 的EXPLAIN QUERY PLAN命令可以看查询执行计划能清楚看到有没有走索引。7.4 日更的技术支撑其实是低摩擦坚持日更一年我发现最大的敌人不是没灵感而是写完之后还要做一堆操作才能发布这件事带来的心理摩擦。如果发布流程要花十五分钟那我下班累了就很容易拖到明天。但如果发布流程只需要点一下那顺手就做了。所以技术上的所有优化最终目标都是降低发布摩擦。Markdown 导入自动化、WorkBuddy 一键触发、Nginx 静态缓存这些加起来把发布流程压缩到了两分钟以内。摩擦小了坚持就容易了。这可能是建站这件事里最不技术、但最重要的一条经验。7.5 关于 WorkBuddy 和类似工具的定位最后说一句工具选择。WorkBuddy 这类工具适合的是你已经知道怎么做只是不想每天重复做的场景。它不能替你决定写什么也不能替你判断文章质量但它能把那些机械性的操作打包成一个按钮。如果你还在探索阶段流程本身还没定型那先别急着上工具手动跑通几遍再说。等流程稳定了再把重复的部分交给工具这时候收益才最大。我见过有人一上来就配一堆自动化指令结果流程改了三次指令全废了。工具是给稳定流程加速的不是给混乱流程擦屁股的。这个顺序搞反了工具越多越乱。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案