1. 为什么我选择 WorkBuddy + Flask + SQLite 这套组合
1.1 从零建站的真实需求拆解
去年年底我接手了一个小项目,需要给一个做农产品批发的朋友搭一个能展示每日价格行情的小站。需求听起来不复杂:能发布每日价格、能按品类筛选、能看历史走势、后台能自己改数据。但真动手的时候,问题就来了——朋友完全不懂技术,我不可能给他部署一套需要运维的复杂系统,也没必要上云数据库,更不想每个月交服务器和数据库的费用。
我当时的判断是:这个站的核心不是高并发,而是低成本、易维护、能日更。日更这件事特别关键,因为价格数据每天都要更新,如果更新流程超过五分钟,朋友自己就放弃了,最后又变成我每天帮他改,那就失去了建站的意义。
所以选型逻辑就很清晰了:后端要轻,数据库要单文件,部署要简单,最好还能有个趁手的工具帮我把重复的建站和更新动作自动化。这就是我最终落到WorkBuddy + Flask + SQLite这套组合的原因。
1.2 三个核心组件各自扮演什么角色
先把这三个东西的分工说清楚,不然后面实操容易乱。
Flask是后端框架,负责把数据从数据库取出来,渲染成网页给用户看,同时接收后台提交的新数据。它足够轻,一个文件就能跑起来一个站,不像 Django 那样自带一堆约定和目录结构。对于这种中小型展示站,Flask 的“微框架”定位刚刚好。
SQLite是数据库,整个库就是一个.db文件。这意味着备份就是复制一个文件,迁移就是把这个文件拷走,不需要单独装数据库服务,也不需要配置账号密码。对于日更量在几十到几百条的数据规模,SQLite 的读写性能完全够用,甚至比很多人想象的要强。
WorkBuddy在这里扮演的是“自动化助手”和“建站脚手架”的角色。它帮我把建站过程中那些重复的、模板化的动作固化下来,比如生成项目骨架、批量插入数据、定时触发更新任务。我不用每次手动敲一堆命令,也不用写复杂的定时脚本,把日常操作交给它来编排。
提示:这三个组件的组合适合“数据量不大、更新频繁、维护人手少”的场景。如果你的站要扛几万并发,或者数据关系极其复杂,这套组合就不合适了,该上 PostgreSQL 和更重的框架就得上。
1.3 和 WordPress、Shopify 自建站的本质区别
很多人一说到建站就想到 WordPress,或者做电商就想到 Shopify。我并不是说这些不好,而是它们和“源码自建站”解决的是不同问题。
WordPress 是内容管理系统,插件生态庞大,但你想要一个完全按自己想法来的数据展示逻辑,往往要跟主题和插件打架,改起来束手束脚。Shopify 是托管电商,省心但每个月有固定成本,而且数据不在自己手里,导出和二次加工都受限。
Flask 自建站的核心优势是数据完全自主、逻辑完全可控、边际成本几乎为零。你写的就是你要的,没有多余的东西。缺点也很明显:什么都要自己写,没有现成的后台管理界面,得自己搭。这也是为什么我要引入 WorkBuddy 来降低重复劳动——把“自己写”的成本压下来,同时保留“完全可控”的好处。
| 方案 | 成本 | 数据自主 | 灵活性 | 维护难度 |
|---|---|---|---|---|
| WordPress | 低(主机费) | 中 | 中 | 低 |
| Shopify | 中(月费) | 低 | 低 | 极低 |
| Flask 自建 | 极低 | 高 | 高 | 中 |
这张表是我自己踩过坑之后总结的,不是绝对结论。选哪个取决于你更看重哪一列。
2. 环境搭建:Python、SQLite 与 WorkBuddy 的安装细节
2.1 Python 安装与虚拟环境配置
Python 安装这一步看似简单,但坑不少。我建议直接去官网下 3.10 或 3.11 的稳定版,不要追最新的 3.13,因为部分第三方库的兼容性还没跟上。安装的时候务必勾选“Add Python to PATH”,否则后面在命令行里敲python会提示找不到命令。
装完之后验证一下:
python --version pip --version两条命令都能正常输出版本号,说明基础环境没问题。接下来是虚拟环境,这一步很多人会跳过,但我强烈建议不要省。虚拟环境能把项目的依赖和系统全局的包隔离开,避免不同项目之间互相污染。
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活之后命令行前面会出现(venv)前缀,说明你已经进入虚拟环境。之后所有pip install装的包都只在这个环境里生效。
注意:如果你用的是 VSCode,记得在右下角把 Python 解释器切换成刚创建的虚拟环境里的那个,否则编辑器提示的库和实际运行用的库会对不上,调试的时候特别容易懵。
2.2 SQLite 的安装与可视化工具选择
SQLite 本身不需要“安装服务”,Python 标准库自带sqlite3模块,直接就能用。但你需要一个可视化工具来查看和编辑数据,不然全靠命令行查数据太痛苦。
我试过好几个工具,最后常驻的是DB Browser for SQLite。它免费、跨平台、界面直观,能直接打开.db文件,像 Excel 一样浏览表数据,也能执行 SQL 语句。对于日更场景,我经常用它快速核对当天数据有没有插进去。
安装方式很简单,去官网下对应系统的安装包,一路下一步就行。装完之后打开你的.db文件,左侧会列出所有表,点进去就能看数据。
如果你习惯用 Android Studio 或者 C# 开发环境,它们也都有各自的 SQLite 可视化插件,但对我这种纯 Python 栈来说,DB Browser 是最顺手的。
2.3 WorkBuddy 安装与初始化配置
WorkBuddy 的安装我走的是官方推荐的流程。它支持多个平台,Windows、Linux、Ubuntu 都有对应的包。我主力环境是 Ubuntu,所以用的是 Linux 版本。
安装完成后第一步是初始化工作区。WorkBuddy 的核心概念是“工作台”,你可以理解成一个项目容器,里面放你的建站任务、数据更新任务、定时触发规则。初始化命令大致是这样:
workbuddy init my-site cd my-site初始化之后会生成一个配置文件,里面定义了任务类型、执行周期、依赖环境等。我建议一开始就把 Python 解释器路径、项目根目录、数据库文件路径这几个关键配置填好,后面所有任务都引用这些变量,改起来只改一处。
WorkBuddy 还有个很实用的能力是“自定义指令”,你可以把常用的操作封装成一条指令,比如“拉取今日价格并入库”“重新生成静态页面”。这样日更的时候我只需要触发一条指令,剩下的它自己跑完。
提示:WorkBuddy 有国际版和国内版的区分,功能上大同小异,配置项的命名可能略有差异。安装前先确认你下的是哪个版本,照着对应版本的文档配,别混着看,容易对不上。
3. 数据库设计:SQLite 表结构与日更数据模型
3.1 核心表结构设计思路
农产品价格数据的特点是:每天每个品类有一条记录,字段包括日期、品类、价格、单位、备注。听起来简单,但设计的时候要考虑几个问题。
第一,品类是固定的还是可扩展的?我朋友那边品类会随季节变,所以品类不能写死在代码里,得单独一张表管理。第二,历史数据要不要保留原始记录?要,因为价格走势分析依赖历史,不能覆盖更新。第三,需不需要记录数据来源?需要,方便追溯是哪天谁录的。
基于这三点,我设计了两张表:一张categories存品类,一张prices存每日价格。prices表通过category_id关联到品类表。
CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, unit TEXT DEFAULT '元/斤', created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE prices ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, price REAL NOT NULL, record_date TEXT NOT NULL, source TEXT DEFAULT 'manual', note TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories(id), UNIQUE(category_id, record_date) );这里有个关键设计:UNIQUE(category_id, record_date)这个约束保证了同一个品类同一天只能有一条记录。日更的时候如果重复插入,数据库会直接拒绝,避免脏数据。这个约束帮我省了很多去重的代码。
3.2 日更场景下的写入与更新策略
日更数据有两种情况:新增当天数据,或者修正已有数据。对应到 SQL 就是INSERT和UPDATE。
新增用INSERT,但如果当天数据已经存在,直接INSERT会报唯一约束错误。这时候用 SQLite 的INSERT OR REPLACE或者ON CONFLICT语法更省事:
INSERT INTO prices (category_id, price, record_date, note) VALUES (?, ?, ?, ?) ON CONFLICT(category_id, record_date) DO UPDATE SET price = excluded.price, note = excluded.note;这条语句的意思是:如果这个品类这一天还没有记录,就插入;如果已经有了,就把价格和备注更新成新的值。一条语句搞定新增和更新两种情况,日更脚本里特别实用。
UPDATE语句单独用的时候要注意加WHERE条件,我见过有人忘了加条件,结果整张表的价格全被改成同一个值,那真是灾难。所以我在 WorkBuddy 里封装更新指令的时候,强制要求传入category_id和record_date两个参数,从流程上杜绝误操作。
3.3 数据备份与加密的现实考量
SQLite 是单文件数据库,备份极其简单——复制.db文件就行。我设置的是每天日更完成后自动复制一份到备份目录,文件名带上日期,保留最近 30 天。这个动作也交给 WorkBuddy 定时触发,不用我操心。
关于加密,经常有人问 SQLite 数据库文件能不能加密。标准版 SQLite 本身不提供加密功能,但可以通过 SQLCipher 这类扩展实现。不过对于我朋友这种农产品价格数据,加密的必要性不大,数据本身不敏感,反而加密会增加维护复杂度。我的建议是:数据敏感就加密,不敏感就别给自己找麻烦,把精力放在备份上更实在。
注意:备份文件不要和原数据库放在同一个目录,更不要放在同一个磁盘。我见过有人备份和原库放一起,结果磁盘坏了两个一起没。备份的意义在于异地、异盘。
4. Flask 后端开发:从路由到模板的完整实现
4.1 项目结构与 Flask 应用初始化
Flask 项目我习惯用这样的结构:
my-site/ ├── app.py ├── models.py ├── templates/ │ ├── index.html │ └── detail.html ├── static/ │ └── style.css └── data/ └── prices.dbapp.py是入口,models.py封装数据库操作,templates放页面模板,static放静态资源,data放数据库文件。这个结构不复杂,但清晰,后面加功能不会乱。
初始化 Flask 应用:
from flask import Flask, render_template, request, jsonify import sqlite3 app = Flask(__name__) DB_PATH = 'data/prices.db' def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return connrow_factory = sqlite3.Row这一行很关键,它让查询结果可以像字典一样按列名访问,模板里写row['price']比写row[2]可读性高太多。
4.2 路由设计与数据查询
首页展示最新一天的所有品类价格,详情页展示某个品类的历史走势。两个路由:
@app.route('/') def index(): conn = get_db() latest_date = conn.execute( 'SELECT MAX(record_date) FROM prices' ).fetchone()[0] rows = conn.execute(''' SELECT c.name, p.price, c.unit, p.record_date FROM prices p JOIN categories c ON p.category_id = c.id WHERE p.record_date = ? ORDER BY c.name ''', (latest_date,)).fetchall() conn.close() return render_template('index.html', rows=rows, date=latest_date) @app.route('/category/<int:cid>') def category_detail(cid): conn = get_db() rows = conn.execute(''' SELECT price, record_date FROM prices WHERE category_id = ? ORDER BY record_date DESC LIMIT 30 ''', (cid,)).fetchall() conn.close() return render_template('detail.html', rows=rows)这里有个细节:查询最新日期用的是MAX(record_date),而不是假设今天是最后一天。因为有时候数据可能补录,最新日期不一定是今天。用MAX更稳妥。
4.3 模板渲染与前端数据绑定
Flask 用的是 Jinja2 模板。首页模板大概长这样:
<table> <thead> <tr><th>品类</th><th>价格</th><th>单位</th></tr> </thead> <tbody> {% for row in rows %} <tr> <td>{{ row['name'] }}</td> <td>{{ row['price'] }}</td> <td>{{ row['unit'] }}</td> </tr> {% endfor %} </tbody> </table>Jinja2 的{% for %}和{{ }}语法很直观,会 Python 基本就能看懂。数据从后端传到模板,模板渲染成 HTML 返回给浏览器,这就是 Flask 最核心的工作流。
如果你要做数据可视化,比如价格走势图,可以在模板里引入 Chart.js 之类的库,把后端查出来的数据转成 JSON 塞进去。Flask 提供jsonify可以很方便地把查询结果转成 JSON 接口,前端用fetch拉数据再画图。这样前后端职责清晰,图表更新也不用刷新页面。
提示:Flask 获取客户端传来的变量时,类型默认都是字符串。如果你传的是数字,记得用
int()或float()转换,不然比较和计算会出错。这个坑我踩过,排查了半天才发现是类型问题。
5. WorkBuddy 自动化:把日更流程压缩到一条指令
5.1 日更任务的编排逻辑
日更的完整流程是这样的:拉取数据源、清洗数据、写入数据库、备份数据库、重新生成页面、通知我完成。这六步如果手动做,熟练了也要十分钟,而且容易漏步骤。用 WorkBuddy 编排之后,我把它拆成三个任务:数据入库任务、备份任务、页面刷新任务,然后串成一条流水线。
WorkBuddy 的任务定义大概是这样的结构:
tasks: - name: daily_update steps: - run: python scripts/fetch_data.py - run: python scripts/insert_data.py - run: python scripts/backup_db.py - run: python scripts/refresh_pages.py schedule: "0 8 * * *"schedule用的是 cron 表达式,0 8 * * *表示每天早上八点执行。这样我朋友早上到店里,数据已经更新好了,他只需要打开网页核对一下。
5.2 自定义指令的封装技巧
WorkBuddy 的自定义指令是我最喜欢的功能。我把“插入单条价格”封装成一条指令,接收品类名、价格、日期三个参数,内部自动查品类 ID、执行 upsert、记录日志。这样即使不用脚本,我朋友自己也能通过一条命令更新数据。
封装的时候有个技巧:参数校验放在最前面。品类名不存在就直接报错退出,不要等到执行 SQL 才发现外键约束失败。错误信息要写清楚,比如“品类‘白菜’不存在,请先在 categories 表里添加”,这样使用者一看就知道怎么处理。
另外,指令的日志要记全。我让每条指令执行时都往一个operation.log里追加一行,记录时间、操作类型、参数、结果。出问题的时候翻日志,比重新跑一遍去复现快得多。
5.3 定时触发与异常处理
定时任务最怕的是“静默失败”——任务跑了但没成功,又没人知道。我在 WorkBuddy 里配置了失败重试和通知机制:任务失败自动重试两次,两次都失败就发一封邮件提醒我。
异常处理的原则是:能自动恢复的自动恢复,不能自动恢复的立刻告警。比如网络抖动导致拉取数据失败,重试通常能解决;但如果是数据库文件被锁或者磁盘满了,重试也没用,必须人工介入,这时候告警就很重要。
注意:定时任务的时间要避开系统维护窗口和数据库备份时间,不然可能出现任务冲突。我一般把日更放在早上八点,备份放在凌晨三点,两者错开。
6. 常见问题排查与避坑经验实录
6.1 数据库锁定与并发写入问题
SQLite 在写入时会锁库,如果同时有多个写入操作,后面的会报database is locked。日更场景下一般只有一个写入任务,问题不大,但如果你在网页后台也开放了写入接口,就可能和定时任务撞上。
解决办法有两个:一是设置timeout参数,让连接等待锁释放而不是立刻报错:
conn = sqlite3.connect(DB_PATH, timeout=10)二是把写入操作串行化,用队列或者锁保证同一时间只有一个写入。我采用的是第一种,简单有效,10 秒的等待窗口足够应付日更这种低频写入。
6.2 中文编码与日期格式的坑
SQLite 默认用 UTF-8 存储文本,中文一般没问题。但如果你从 CSV 导入数据,CSV 文件的编码可能是 GBK,直接读会乱码。我踩过这个坑,后来统一在读取时指定编码:
with open('data.csv', encoding='utf-8-sig') as f: ...utf-8-sig能兼容带 BOM 的 UTF-8 文件,比纯utf-8更稳。
日期格式我统一用YYYY-MM-DD字符串存储,不用时间戳。原因是字符串比较就是日期比较,WHERE record_date = '2024-01-15'直接就能查,不用转换。而且 SQLite 的日期函数对YYYY-MM-DD格式支持最好。
6.3 页面数据不更新的排查思路
有时候数据明明入库了,页面却还是旧的。排查顺序我总结成一张表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 页面完全没变 | 浏览器缓存 | 强制刷新 Ctrl+F5 |
| 数据是旧的 | 查询条件写错 | 直接在 DB Browser 里跑 SQL |
| 部分数据缺失 | 关联查询漏了 | 检查 JOIN 条件 |
| 报 500 错误 | 模板变量名错 | 看 Flask 控制台报错 |
大部分“数据不更新”的问题,最后都发现是查询条件或者缓存的问题,真正数据库没写进去的情况反而少。所以排查从外往里查,先看浏览器,再看 SQL,最后看数据库。
6.4 部署上线的注意事项
本地跑通不等于线上能跑。部署的时候有几个点要注意:一是路径问题,本地用的相对路径上线后可能找不到文件,建议用绝对路径或者基于__file__计算;二是端口和进程管理,Flask 自带的开发服务器不能用于生产,要用 gunicorn 或 uwsgi 这类 WSGI 服务器;三是静态文件,生产环境建议交给 Nginx 处理,Flask 只管动态请求。
我自己的部署流程是:gunicorn 起 Flask 应用,Nginx 做反向代理和静态文件服务,WorkBuddy 负责定时任务。这套组合跑了大半年,日更没断过。
7. 我实际跑下来的一些体会
这套站从搭起来到现在,日更已经持续了几个月。最大的感受是:建站本身不难,难的是让日更这件事持续下去。技术选型的时候,我优先考虑的不是性能多强、功能多全,而是维护成本多低、出错概率多小。Flask 和 SQLite 的组合恰好满足这一点,而 WorkBuddy 把日更的重复劳动自动化,让“持续”变得可能。
如果让我给准备走这条路的人一个建议,那就是:先把日更流程跑通,再考虑加功能。很多人一上来就想做图表、做用户系统、做权限管理,结果核心的数据更新还没稳定,站就荒废了。先把最简单的一条链路——数据进来、存下来、显示出来——跑顺,剩下的都是锦上添花。
另外,备份这件事再怎么强调都不为过。我现在的习惯是每天日更完成后自动备份,每周手动检查一次备份文件能不能正常打开。备份不是做了就行,得验证它真的能用。这个习惯帮我避免过一次数据丢失,当时原库文件损坏,直接拿备份恢复,十分钟搞定。