1. 为什么我选择 WorkBuddy + Flask + SQLite 这套组合
1.1 从零建站的真实需求拆解
很多人一提到建站,第一反应就是 WordPress,或者干脆上 Shopify 这类托管方案。我一开始也是这么想的,但实际折腾下来发现,如果你只是想做一个轻量的、自己能完全掌控的内容站点,尤其是那种需要日更、需要自己写点逻辑、又不想被各种插件和主题绑架的场景,WordPress 反而会变成负担。数据库要单独配、PHP 环境要调、插件冲突三天两头出问题,光是维护这套东西就够喝一壶的。
我的核心诉求其实很朴素:一个能每天更新内容的站点,后台逻辑我自己说了算,数据存本地,部署简单,最好一台最低配的机器就能跑起来。基于这个诉求,我把技术栈锁定在了Flask + SQLite上。Flask 是 Python 的轻量级 Web 框架,没有 Django 那么多约定俗成的东西,你想怎么组织就怎么组织;SQLite 更是一个文件就是一个数据库,不需要单独起服务,备份就是复制一个文件,对于日更这种写入频率不高、读取为主的场景简直完美。
那 WorkBuddy 在这里扮演什么角色?它是我用来加速整个建站流程的辅助工具。你可以把它理解成一个能帮你把零散想法快速落地成可运行代码的搭档。从项目结构初始化,到 Flask 路由的骨架生成,再到 SQLite 建表语句的草拟,它都能帮我省掉大量查文档和敲重复代码的时间。这套组合下来,我从零到站点上线,实际只花了不到两天,后面就是每天花十几分钟更新内容。
1.2 三种建站路线对比:为什么自建站更适合日更场景
在动手之前,我把市面上主流的三种路线拉出来做了个对比,这个对比直接决定了我的选型。
| 对比维度 | Shopify 类托管 | WordPress 自托管 | Flask + SQLite 自建 |
|---|---|---|---|
| 上手速度 | 极快,注册即用 | 中等,需配环境 | 较慢,需写代码 |
| 数据掌控 | 平台方 | 自己 | 完全自己 |
| 日更成本 | 低但受限于平台规则 | 中,插件多了会卡 | 低,写入逻辑自己定 |
| 定制自由度 | 低 | 中,受主题插件限制 | 极高 |
| 长期维护 | 平台负责 | 自己负责,升级易崩 | 自己负责,但依赖少 |
| 适合人群 | 纯电商小白 | 内容运营者 | 有基础编程能力者 |
Shopify 这类方案适合纯电商、不想碰技术的人,但它的数据在别人手里,你想做个自定义的数据分析或者特殊的内容展示逻辑,基本没戏。WordPress 灵活一些,但它的灵活性建立在庞大的插件生态上,插件一多,站点就变重,日更的时候后台响应会明显变慢。而 Flask + SQLite 这套,虽然前期要写点代码,但一旦跑起来,整个站点的行为完全由我控制,日更就是往数据库里插一条记录的事,快得飞起。
提示:如果你完全没有编程基础,又急着上线,WordPress 仍然是更稳妥的选择。但如果你愿意花几天学一下 Python 基础,Flask + SQLite 的长期收益会高得多。
1.3 WorkBuddy 在流程中的定位与价值
WorkBuddy 不是建站框架,它更像是一个“流程加速器”。我在整个项目里用它做了三件事:第一,生成 Flask 项目的初始目录结构和基础路由代码;第二,根据我的描述生成 SQLite 的建表语句和常用的增删改查函数;第三,在我遇到报错时,帮我快速定位问题并给出修改建议。
举个具体的例子,我一开始对 Flask 的蓝图(Blueprint)结构不太熟,手动拆分模块的时候总是导入出错。我把需求描述给 WorkBuddy,它直接给我生成了一套按功能划分的蓝图结构,包括__init__.py里的注册逻辑、各个蓝图文件的路由定义,我只需要把业务逻辑填进去就行。这省掉了我至少半天查文档和试错的时间。
但要注意,WorkBuddy 生成的东西不能无脑照搬。它给的代码有时候会假设一些不存在的依赖,或者用了较新版本的语法,而你的环境可能是旧版本。所以我的习惯是:先生成,再理解,最后按自己的环境调整。这个习惯让我避开了很多“代码看起来对但就是跑不起来”的坑。
2. 环境搭建与项目初始化实操
2.1 Python 环境配置与虚拟环境隔离
第一步永远是 Python 环境。我用的 Python 3.10,这个版本在 Flask 和 SQLite 的兼容性上都很稳。安装过程不复杂,去官网下载对应系统的安装包,Windows 记得勾选“Add Python to PATH”,macOS 和 Linux 一般自带或者用包管理器装就行。
装完之后,我强烈建议用虚拟环境隔离项目依赖。原因很简单:你不可能只做一个项目,不同项目依赖的库版本可能冲突,全局安装迟早出事。创建虚拟环境的命令如下:
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS/Linux) source venv/bin/activate激活之后,命令行前面会出现(venv)标识,说明你已经在隔离环境里了。接下来安装 Flask:
pip install flask这里有个细节:Flask 本身不包含数据库驱动,SQLite 是 Python 标准库自带的,所以不需要额外安装sqlite3模块,直接import sqlite3就能用。这一点比 MySQL 省事太多,不用装驱动、不用配连接池。
注意:如果你在 Windows 上遇到
python命令找不到的情况,试试py命令,这是 Windows 的 Python 启动器。另外,虚拟环境不要提交到 Git,在.gitignore里加上venv/就行。
2.2 用 WorkBuddy 生成项目骨架
环境好了之后,我让 WorkBuddy 帮我生成项目骨架。我给它的描述是:“一个 Flask 项目,包含首页、文章列表页、文章详情页,用 SQLite 存文章数据,需要蓝图结构,模板用 Jinja2。”
它生成的目录结构大致是这样的:
myblog/ ├── app/ │ ├── __init__.py │ ├── routes/ │ │ ├── __init__.py │ │ ├── main.py │ │ └── article.py │ ├── models/ │ │ ├── __init__.py │ │ └── db.py │ ├── templates/ │ │ ├── base.html │ │ ├── index.html │ │ ├── article_list.html │ │ └── article_detail.html │ └── static/ │ └── style.css ├── instance/ │ └── blog.db ├── config.py └── run.py这个结构清晰地把路由、数据模型、模板分开了。instance文件夹放数据库文件,这是 Flask 的惯例,因为这个文件夹不会被纳入版本控制,适合放本地数据。config.py放配置项,比如数据库路径、密钥等。run.py是启动入口。
我拿到这个骨架后,做了一件事:逐个文件读一遍,确认每个文件的职责。这一步不能省,因为 WorkBuddy 生成的代码是通用模板,你需要把它变成你自己的。比如config.py里它给了一个默认的SECRET_KEY,我改成了自己生成的一串随机字符,这个密钥用于会话加密,不能泄露。
2.3 SQLite 数据库文件的位置与初始化
SQLite 的数据库就是一个文件,位置很关键。我把它放在instance/blog.db,然后在config.py里这样配置:
import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) class Config: SECRET_KEY = '你生成的一串随机字符' DATABASE = os.path.join(BASE_DIR, 'instance', 'blog.db')然后在app/models/db.py里写获取数据库连接的函数:
import sqlite3 from flask import current_app, g def get_db(): if 'db' not in g: g.db = sqlite3.connect( current_app.config['DATABASE'], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close()这里用了 Flask 的g对象,它是在一次请求周期内共享的。row_factory = sqlite3.Row这一行很重要,它让查询结果可以像字典一样用列名访问,而不是只能用索引。detect_types参数让 SQLite 能自动把时间戳字段转成 Python 的datetime对象,省去手动转换的麻烦。
初始化数据库的脚本我单独写了一个init_db.py:
import sqlite3 from config import Config def init_db(): conn = sqlite3.connect(Config.DATABASE) with open('schema.sql', 'r', encoding='utf-8') as f: conn.executescript(f.read()) conn.commit() conn.close() if __name__ == '__main__': init_db()schema.sql里放建表语句,这个我让 WorkBuddy 根据我的字段需求生成,然后自己调整。文章表的结构大概是这样:
CREATE TABLE IF NOT EXISTS article ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, summary TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );AUTOINCREMENT保证 id 单调递增,DEFAULT CURRENT_TIMESTAMP让插入时自动填时间,省去手动传时间戳。summary字段用于列表页展示摘要,避免每次都把全文查出来。
3. 核心功能开发与日更流程落地
3.1 文章发布与展示的完整链路
日更的核心就是“发布”和“展示”两个动作。发布是往数据库写,展示是从数据库读。我先说发布。
发布功能我一开始想做个后台管理页面,但后来发现对于日更场景,直接在本地跑一个脚本更快。我写了一个publish.py,运行的时候从命令行参数或者交互式输入里拿标题和内容,然后插入数据库:
import sqlite3 from datetime import datetime from config import Config def publish_article(title, content, summary=''): conn = sqlite3.connect(Config.DATABASE) cursor = conn.cursor() cursor.execute( 'INSERT INTO article (title, content, summary, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', (title, content, summary, datetime.now(), datetime.now()) ) conn.commit() conn.close() print(f'文章《{title}》发布成功') if __name__ == '__main__': title = input('标题:') content = input('内容:') summary = input('摘要(可留空):') publish_article(title, content, summary)这里用了参数化查询(?占位符),这是防止 SQL 注入的基本操作,绝对不能把用户输入直接拼接到 SQL 字符串里。datetime.now()手动传入而不是依赖数据库默认值,是因为我想在 Python 层面控制时间格式,方便后续做时区处理。
展示部分就是 Flask 路由加模板渲染。文章列表页的路由:
from flask import Blueprint, render_template from app.models.db import get_db bp = Blueprint('article', __name__) @bp.route('/articles') def article_list(): db = get_db() articles = db.execute( 'SELECT id, title, summary, created_at FROM article ORDER BY created_at DESC' ).fetchall() return render_template('article_list.html', articles=articles) @bp.route('/article/<int:id>') def article_detail(id): db = get_db() article = db.execute( 'SELECT * FROM article WHERE id = ?', (id,) ).fetchone() if article is None: abort(404) return render_template('article_detail.html', article=article)列表页只查需要的字段,不查content,这样数据量大了之后列表页依然很快。详情页用fetchone()拿单条,拿不到就返回 404。模板里用 Jinja2 的循环和变量替换,这部分 WorkBuddy 生成的模板基本能用,我主要调整了样式和布局。
3.2 用 DB Browser for SQLite 做数据核对
虽然我可以用命令行查数据库,但日更的时候经常需要快速核对数据,比如确认某篇文章有没有写进去、时间对不对。这时候DB Browser for SQLite就派上用场了。它是一个图形化的 SQLite 客户端,打开数据库文件就能看到所有表和数据,还能直接执行 SQL。
我的习惯是每天发布完文章后,用 DB Browser 打开blog.db,看一眼article表的最新几条记录,确认标题、时间、摘要都正确。这个动作花不了十秒钟,但能避免“以为发出去了其实没发成功”的尴尬。
提示:DB Browser for SQLite 在 Windows、macOS、Linux 上都有,官网直接下载安装包就行。打开数据库文件的时候记得先关掉正在运行的 Flask 服务,虽然 SQLite 支持并发读,但写入的时候可能会锁文件。
另外,DB Browser 还能用来做数据导出。比如我想把文章数据导出成 CSV 做备份,直接用它自带的导出功能就行,不用写代码。这个工具对于不熟悉命令行的朋友特别友好,强烈建议装一个。
3.3 日更节奏下的自动化小技巧
日更最怕的就是流程太繁琐,繁琐就会拖延,拖延就会断更。所以我把发布流程尽量简化。除了上面说的publish.py脚本,我还加了一个小功能:从 Markdown 文件批量导入。因为我平时写草稿习惯用 Markdown,写完之后直接跑一个脚本把文件内容读出来插进数据库,连复制粘贴都省了。
import sqlite3 import os from datetime import datetime from config import Config def import_from_markdown(filepath): with open(filepath, 'r', encoding='utf-8') as f: content = f.read() filename = os.path.basename(filepath) title = os.path.splitext(filename)[0] summary = content[:100].replace('\n', ' ') conn = sqlite3.connect(Config.DATABASE) cursor = conn.cursor() cursor.execute( 'INSERT INTO article (title, content, summary, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', (title, content, summary, datetime.now(), datetime.now()) ) conn.commit() conn.close() print(f'已导入:{title}') if __name__ == '__main__': import_from_markdown('drafts/today.md')这个脚本把文件名当标题,把内容前 100 个字符当摘要,一键导入。我每天写完草稿,保存成drafts/日期.md,然后跑一下脚本,文章就进库了。整个过程不到一分钟。
还有一个技巧是给文章加一个“草稿”状态字段。有时候文章没写完但想先存着,就可以先插入一条status='draft'的记录,列表页查询的时候加个WHERE status='published'过滤掉草稿。等写完了再更新状态。这个字段我后来加上了,确实方便。
4. 部署上线与常见问题排查
4.1 从本地到服务器的部署路径
本地跑通之后,下一步就是部署到服务器上,让站点能被外部访问。我用的是一台最低配的云服务器,系统是 Ubuntu。部署 Flask 应用不能直接用flask run,那个是开发服务器,性能和稳定性都不够。生产环境我用的是Gunicorn + Nginx的组合。
Gunicorn 是 Python 的 WSGI 服务器,负责跑 Flask 应用;Nginx 作为反向代理,负责处理静态文件、转发请求给 Gunicorn。安装很简单:
pip install gunicorn sudo apt install nginx启动 Gunicorn 的命令:
gunicorn -w 2 -b 127.0.0.1:8000 run:app-w 2表示两个 worker 进程,对于低配机器足够了。run:app表示从run.py里导入app对象。然后配置 Nginx,把 80 端口的请求转发到 8000 端口:
server { listen 80; server_name your_domain.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/your/app/static/; } }静态文件交给 Nginx 直接处理,不经过 Flask,这样能减轻应用服务器的压力。配置完之后sudo nginx -s reload重载配置。
注意:SQLite 数据库文件在部署后要注意权限问题。Gunicorn 运行的用户必须对
instance/blog.db有读写权限,否则会报“unable to open database file”。我一开始就踩了这个坑,后来把数据库文件的所有者改成运行 Gunicorn 的用户就好了。
4.2 常见报错与排查速查表
建站过程中我遇到了不少报错,这里整理成一张速查表,方便你遇到类似问题时快速定位。
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
ModuleNotFoundError: No module named 'flask' | 虚拟环境没激活或没装 Flask | 激活虚拟环境后pip install flask |
sqlite3.OperationalError: unable to open database file | 数据库路径错误或权限不足 | 检查config.py里的路径,确认文件权限 |
jinja2.exceptions.TemplateNotFound | 模板文件不在templates目录 | 确认模板路径和文件名大小写 |
sqlite3.IntegrityError: NOT NULL constraint failed | 插入时必填字段为空 | 检查插入语句是否漏了字段 |
Address already in use | 端口被占用 | 换端口或杀掉占用进程 |
502 Bad Gateway | Gunicorn 没启动或崩溃 | 检查 Gunicorn 日志,确认应用能正常导入 |
| 页面样式丢失 | Nginx 静态文件路径配错 | 检查location /static/的alias路径 |
这张表里的每一条都是我实际遇到过的。其中502 Bad Gateway最让人头疼,因为 Nginx 只告诉你网关错误,具体原因要看 Gunicorn 的日志。我的习惯是先用gunicorn run:app直接在前台跑一下,看有没有报错,确认应用本身没问题再放到后台。
4.3 数据备份与迁移的实操经验
SQLite 最大的优势就是备份简单,复制文件就行。但复制的时候要注意,如果数据库正在被写入,直接复制可能会得到一个损坏的文件。正确的做法是先停掉应用,或者用 SQLite 自带的备份命令:
sqlite3 blog.db ".backup backup.db"这个命令会在数据库运行时创建一个一致性快照,不会因为并发写入导致备份文件损坏。我设置了一个定时任务,每天凌晨跑一次备份,备份文件按日期命名,保留最近 30 天。
迁移就更简单了,把blog.db文件拷到新服务器上,配好环境,启动应用就行。不需要导出 SQL、不需要重建表结构,一个文件搞定。这也是我当初选 SQLite 的重要原因之一。
提示:如果你的站点数据量增长到几万篇文章,SQLite 的读取性能依然没问题,但写入可能会开始变慢。这时候可以考虑加索引,比如在
created_at字段上建索引,加速列表页的排序查询。建索引的语句是CREATE INDEX idx_created_at ON article(created_at DESC);。
5. 我踩过的坑和几条实在建议
5.1 关于 WorkBuddy 生成代码的使用心得
WorkBuddy 生成的代码质量整体不错,但有几个地方需要特别注意。第一,它有时候会用一些较新的 Python 语法,比如海象运算符:=或者 f-string 的嵌套,如果你的 Python 版本比较旧,就会报语法错误。我的做法是生成之后先扫一眼,看到不熟悉的语法就查一下版本要求。
第二,它生成的 SQL 语句有时候没有加IF NOT EXISTS,重复执行会报错。建表语句我一般会手动加上IF NOT EXISTS,这样初始化脚本可以反复跑,不会因为表已存在而中断。
第三,它生成的 Flask 路由有时候缺少错误处理。比如查询单条记录的时候没有判断None,直接拿结果去渲染模板,数据不存在就会报错。我养成了一个习惯:凡是fetchone()的地方,后面必跟一个if xxx is None的判断。这个习惯帮我避免了很多 500 错误。
5.2 日更站点的内容管理策略
日更最难的不是技术,是坚持。技术上的顺畅能降低坚持的难度,但内容本身的规划也很重要。我的策略是提前攒一批草稿,状态设为draft,每天从草稿里挑一篇发布。这样即使某天特别忙,也不会断更。
另外,我给文章加了一个tags字段,用逗号分隔存多个标签。列表页可以按标签筛选,详情页展示相关文章。这个功能不复杂,但能提升站点的可浏览性。实现方式就是在查询的时候加一个WHERE tags LIKE '%xxx%'的条件,虽然不够优雅,但对于小规模数据完全够用。
还有一点,文章的updated_at字段我一开始没怎么用,后来发现有时候会修改已发布的文章,这时候更新updated_at能让读者知道内容有更新。所以每次更新文章内容的时候,记得把updated_at也一起更新。
5.3 性能与安全上的几个关键注意点
性能方面,SQLite 在读取为主的场景下表现很好,但有几个地方要注意。第一,每次请求都创建新的数据库连接是有开销的,我用 Flask 的g对象做了请求级别的连接复用,请求结束自动关闭。第二,列表页一定要分页,不能一次性把所有文章查出来。我用的LIMIT和OFFSET做分页,每页 20 条。
安全方面,除了前面说的参数化查询防注入,还有几点。第一,SECRET_KEY绝对不能硬编码在代码里提交到公开仓库,我是通过环境变量读取的。第二,用户输入的内容在渲染到模板之前,Jinja2 默认会转义 HTML,所以不用担心 XSS,但如果你用了|safe过滤器,就要确保内容是你自己可控的。第三,生产环境一定要关掉 Flask 的调试模式,debug=True会暴露源代码和堆栈信息,非常危险。
最后说一个我个人的体会:这套技术栈最大的价值在于“可控”。从数据库文件到应用代码,每一层我都能看懂、能改、能迁移。不像用托管平台,出了问题只能等客服,想加个功能要看平台支不支持。如果你也想做一个自己能完全掌控的日更站点,Flask + SQLite 这条路值得走一遍。