ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 Flask 给「我和晓峰的博客」加上 SQL 数据库与路由新内容

用 Flask 给「我和晓峰的博客」加上 SQL 数据库与路由新内容 1. 从「文章页能打开」到「博客像个博客」我和晓峰的博客缺了什么「我和晓峰的博客」最早只有一个首页所有文章标题堆在一起点进去能看到正文就算成功。后来给每篇文章加了ArticlePage又塞进基于 DISQUS 的评论块看起来是完整了但用起来马上暴露问题文章一多首页变成一条没有尽头的长列表想找某个标签下的内容只能靠浏览器 CtrlF数据库里entries表字段越加越乱title还兼着 URL 参数改个标题链接就 404。这一篇要解决的就是这些事用 Flask 路由把视图拆开用 SQL 建文章表和标签表补上文章归档页、分类页和详情页。核心检索词先摆出来——Flask 路由拆分与 SQL 建表存文章标签是一套适合个人博客从「能跑」到「能维护」的最小改造方案适合已经写过 Flask 入门项目、手里有一个能渲染模板的博客、但还没认真设计过数据表和路由结构的人。我试过最省事的做法所有逻辑塞进一个app.pyapp.route(/)里查全部文章app.route(/article/good)里按标题查单篇。这个结构在文章少于 10 篇时没问题超过之后每次改分类都要动首页模板标签统计也没地方放。所以这次改造分三步走先定表结构再拆路由最后逐个验证列表页、详情页、分类筛选。需要说明的是下面所有 SQL 和路由片段都可以直接复制进你自己的项目表名、字段名按你现有代码调整即可。数据库用 SQLite 起步最合适本地一个文件不用装服务后面要换 MySQL 或 PostgreSQL建表语句改一下驱动就行。2. TaoToken 前置给博客加一个可调用的模型接口博客改造本身不需要联网但「我和晓峰的博客」后面大概率会加摘要生成、标签推荐、评论审核这类功能与其等到那时候再折腾密钥管理不如现在就把调用入口理清楚。我用的是 TaoToken官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这个地址后面不加任何查询参数。它的定位是模型调用入口不是编辑器也不替代你本地的 Flask 项目。你可以在博客后台写一个「生成摘要」按钮点一下把文章正文发过去拿回一段摘要存进数据库也可以在发布前调用一次让它根据正文推荐三个标签你确认后再写入tags表。这些都属于锦上添花不影响本篇的主线改造。接入前需要准备三样东西我把它叫「三件套」Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面创建地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite Model ID 按你实际要用的模型填在模型对话页能看到可选列表地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。如果你打算长期在博客里跑编码类或 Agent 类任务可以看 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。注意API Key 只放在服务端环境变量里不要写进前端模板也不要提交到 Git。Flask 项目里用os.environ.get(TAOTOKEN_API_KEY)读取本地开发可以放在.env文件并加入.gitignore。控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用的是 Claude Code 这类工具做辅助开发它的 Anthropic 兼容配置页在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_anthropicutm_campaignrewrite 里面写了 Base URL 和 Key 怎么填。这一段的目的不是让你立刻接模型而是先把「以后要加功能时去哪儿拿 Key、填什么地址」这件事定下来。博客的数据层和路由层是地基模型调用是后面盖的楼层地基没打好楼层越高越晃。3. 可复制配置SQL 建表语句与 Flask 路由注册片段先建表。SQLite 里执行下面这段entries存文章tags存标签entry_tags做多对多关联。字段类型按 SQLite 写换 MySQL 时把INTEGER PRIMARY KEY AUTOINCREMENT改成INT AUTO_INCREMENT PRIMARY KEYTEXT保持不变即可。CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, text TEXT NOT NULL, author TEXT NOT NULL DEFAULT 晓峰, created_at TEXT NOT NULL DEFAULT (datetime(now,localtime)) ); CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE IF NOT EXISTS entry_tags ( entry_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (entry_id, tag_id), FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at DESC); CREATE INDEX IF NOT EXISTS idx_entry_tags_tag ON entry_tags(tag_id);注意slug这一列它是详情页 URL 用的稳定标识和title分开。之前用标题当参数标题一改链接就废现在标题随便改slug不动。created_at默认取本地时间归档页按它倒序排。接着是 Flask 侧。数据库连接用一个get_db()函数管理放在db.pyimport sqlite3 from flask import g DATABASE blog.db def get_db(): if db not in g: g.db sqlite3.connect(DATABASE) g.db.row_factory sqlite3.Row g.db.execute(PRAGMA foreign_keys ON) return g.db def close_db(eNone): db g.pop(db, None) if db is not None: db.close()然后在app.py里注册路由。列表页、详情页、分类页三个视图分开写不要挤在一个函数里from flask import Flask, render_template, abort, request from db import get_db, close_db app Flask(__name__) app.teardown_appcontext(close_db) app.route(/) def index(): db get_db() rows db.execute( SELECT id, title, slug, author, created_at FROM entries ORDER BY created_at DESC ).fetchall() return render_template(index.html, entriesrows) app.route(/article/slug) def article(slug): db get_db() row db.execute( SELECT id, title, text, author, created_at FROM entries WHERE slug ?, (slug,) ).fetchone() if row is None: abort(404) tags db.execute( SELECT t.name FROM tags t JOIN entry_tags et ON et.tag_id t.id WHERE et.entry_id ?, (row[id],) ).fetchall() return render_template(article.html, artrow, tagstags) app.route(/tag/name) def tag_filter(name): db get_db() rows db.execute( SELECT e.id, e.title, e.slug, e.created_at FROM entries e JOIN entry_tags et ON et.entry_id e.id JOIN tags t ON t.id et.tag_id WHERE t.name ? ORDER BY e.created_at DESC, (name,) ).fetchall() return render_template(tag.html, tagname, entriesrows)这里有个关键点SQL 参数用?占位值通过元组传入不要用字符串拼接。之前那段c.execute(select ... where title %s, good)在 SQLite 里其实不生效%s是 MySQL 驱动的写法SQLite 用?。这是最容易踩的坑之一后面排障会细说。如果你用 MySQL把sqlite3.connect换成pymysql.connect占位符从?改成%s其余逻辑不变。表结构里AUTOINCREMENT改成AUTO_INCREMENT。4. 验证请求列表页、详情页、分类筛选各跑一次配置写完启动flask run然后按顺序验证三个页面。先插两条测试数据直接在 SQLite 命令行执行INSERT INTO entries (title, slug, text, author) VALUES (Flask 路由入门, flask-routing, 这是正文内容……, 晓峰), (SQL 建表实践, sql-schema, 这是另一篇正文……, 我); INSERT INTO tags (name) VALUES (flask), (sql); INSERT INTO entry_tags (entry_id, tag_id) VALUES (1, 1), (1, 2), (2, 2);第一步访问http://127.0.0.1:5000/应该看到两篇文章按时间倒序排列标题可点击。如果页面空白先看终端有没有报no such table: entries说明建表语句没执行回到上一步在正确的数据库文件里跑一遍。第二步点第一篇文章URL 变成/article/flask-routing正文、作者、时间都渲染出来标签显示flask和sql。如果 404检查slug是否和数据库里一致注意大小写和连字符。第三步访问http://127.0.0.1:5000/tag/sql应该只出现两篇里带sql标签的那两篇示例里两篇都打了 sql 标签所以是两篇。再访问/tag/flask只出现第一篇。这一步验证的是多对多关联查询是否正确。用 curl 也能快速验证不用开浏览器curl -s http://127.0.0.1:5000/ | grep -o h2[^]*/h2 curl -s http://127.0.0.1:5000/article/flask-routing | grep -o title[^]*/title curl -s http://127.0.0.1:5000/tag/sql | grep -c article-link第三条命令统计分类页里文章链接的数量返回 2 说明筛选生效。如果返回 0多半是entry_tags没插数据或者 JOIN 条件写错。三个页面都通过之后把首页模板里的文章列表抽成一个_entry_list.html片段列表页和分类页共用避免两处维护。这一步不做也不影响功能但后面加分页时会省很多事。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth虽然本篇主线是本地 SQL 和路由但既然提到了模型接口把接入时常见的几类报错一起说了省得你后面单独查。第一类401 Unauthorized。出现在调用模型接口时说明 API Key 没带或带错。检查请求头是不是Authorization: Bearer 你的KeyKey 有没有多余空格环境变量有没有真正加载。Flask 里可以用print(os.environ.get(TAOTOKEN_API_KEY)[:8])打印前八位确认不要打印完整 Key。第二类local proxy failed或连接被拒绝。这类报错通常和本地网络环境有关检查你的请求地址是不是写成了https://taotoken.net/api有没有误加路径或参数。Base URL 就是这一串后面拼/v1/chat/completions之类的路径由 SDK 处理不要自己乱拼。第三类reading choices或返回结构里找不到choices字段。这多半是响应解析代码写死了 OpenAI 格式而你换的模型返回结构不同。先打印完整响应体看一眼再决定取哪个字段。不要盲目套用网上的解析片段。第四类OAuth相关报错。如果你用 Claude Code 或类似工具接入认证方式可能不是简单 Bearer而是走 OAuth 流程。这时候要回到对应的配置页按它给的步骤走Base URL、Key、Model ID 三件套一个都不能少。三件套缺任何一个都会表现为认证失败或模型找不到。回到博客本身最常见的错还是 SQL 占位符。SQLite 用?MySQL 用%sPostgreSQL 用%s写混了就是sqlite3.OperationalError: near %: syntax error。另一个是外键没开SQLite 默认不强制外键PRAGMA foreign_keys ON要每次连接都执行否则删文章时关联表会留垃圾数据。还有一个隐蔽的坑created_at用datetime(now,localtime)存的是字符串排序时按字符串比较格式统一就没问题但如果你手动插入过2024/01/01这种格式排序会乱。统一用YYYY-MM-DD HH:MM:SS。6. 接下来怎么走把归档和分类真正用起来三个页面跑通之后「我和晓峰的博客」算是有了基本骨架。归档页可以按年份分组SQL 里用strftime(%Y, created_at)取年份Python 侧再归并分类页可以加一个标签云统计每个标签下的文章数SQL 是SELECT t.name, COUNT(*) FROM tags t JOIN entry_tags et ON et.tag_id t.id GROUP BY t.name。如果你打算让模型帮忙生成摘要或推荐标签现在数据结构已经支持了摘要可以加一列summary TEXT推荐标签先写到临时表人工确认后再进entry_tags。调用入口用前面说的三件套配置Key 放环境变量Base URL 用https://taotoken.net/apiModel ID 按实际填。最后留一个实用技巧本地开发时把blog.db加进.gitignore但把建表语句单独存成schema.sql提交到仓库。换机器时先跑sqlite3 blog.db schema.sql再启动 Flask环境一秒还原。这个习惯在文章表结构还会继续改的阶段特别值钱。
返回列表