
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, rowsrows, datelatest_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, rowsrows)这里有个细节查询最新日期用的是MAX(record_date)而不是假设今天是最后一天。因为有时候数据可能补录最新日期不一定是今天。用MAX更稳妥。4.3 模板渲染与前端数据绑定Flask 用的是 Jinja2 模板。首页模板大概长这样table thead trth品类/thth价格/thth单位/th/tr /thead tbody {% for row in rows %} tr td{{ row[name] }}/td td{{ row[price] }}/td td{{ row[unit] }}/td /tr {% endfor %} /tbody /tableJinja2 的{% 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, timeout10)二是把写入操作串行化用队列或者锁保证同一时间只有一个写入。我采用的是第一种简单有效10 秒的等待窗口足够应付日更这种低频写入。6.2 中文编码与日期格式的坑SQLite 默认用 UTF-8 存储文本中文一般没问题。但如果你从 CSV 导入数据CSV 文件的编码可能是 GBK直接读会乱码。我踩过这个坑后来统一在读取时指定编码with open(data.csv, encodingutf-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 页面数据不更新的排查思路有时候数据明明入库了页面却还是旧的。排查顺序我总结成一张表现象可能原因排查方法页面完全没变浏览器缓存强制刷新 CtrlF5数据是旧的查询条件写错直接在 DB Browser 里跑 SQL部分数据缺失关联查询漏了检查 JOIN 条件报 500 错误模板变量名错看 Flask 控制台报错大部分“数据不更新”的问题最后都发现是查询条件或者缓存的问题真正数据库没写进去的情况反而少。所以排查从外往里查先看浏览器再看 SQL最后看数据库。6.4 部署上线的注意事项本地跑通不等于线上能跑。部署的时候有几个点要注意一是路径问题本地用的相对路径上线后可能找不到文件建议用绝对路径或者基于__file__计算二是端口和进程管理Flask 自带的开发服务器不能用于生产要用 gunicorn 或 uwsgi 这类 WSGI 服务器三是静态文件生产环境建议交给 Nginx 处理Flask 只管动态请求。我自己的部署流程是gunicorn 起 Flask 应用Nginx 做反向代理和静态文件服务WorkBuddy 负责定时任务。这套组合跑了大半年日更没断过。7. 我实际跑下来的一些体会这套站从搭起来到现在日更已经持续了几个月。最大的感受是建站本身不难难的是让日更这件事持续下去。技术选型的时候我优先考虑的不是性能多强、功能多全而是维护成本多低、出错概率多小。Flask 和 SQLite 的组合恰好满足这一点而 WorkBuddy 把日更的重复劳动自动化让“持续”变得可能。如果让我给准备走这条路的人一个建议那就是先把日更流程跑通再考虑加功能。很多人一上来就想做图表、做用户系统、做权限管理结果核心的数据更新还没稳定站就荒废了。先把最简单的一条链路——数据进来、存下来、显示出来——跑顺剩下的都是锦上添花。另外备份这件事再怎么强调都不为过。我现在的习惯是每天日更完成后自动备份每周手动检查一次备份文件能不能正常打开。备份不是做了就行得验证它真的能用。这个习惯帮我避免过一次数据丢失当时原库文件损坏直接拿备份恢复十分钟搞定。