数据终于住进了正经数据库,可总不能让家人朋友也来敲 Python 命令行吧?今天把数据库接上 Flask,做个能用浏览器打开的小网页:页面上点一点,数据就增删改查——你的”正经软件”第一次有了脸面。只引入一个新概念:Flask。sqlite3 四件套和参数化查询上篇都备齐了,今天全套用上。
一、真实场景:程序很好用,但只有你会用
接着讲你的价格监控工具。上一篇把 sqlite3 磕明白了,现在它每天自动把新价格写进 shop.db,你敲一行 Python 就能查。然后你妈问:”你说的那个记价格的玩意儿,帮我看看酱油降没降价?”
你面临两个选择:一是远程教她装 Python、开终端、敲 python query.py——大概率以”你直接帮我查吧”告终;二是给程序装个网页门脸,她用浏览器打开,想查什么自己点。
这就是今天要干的事。你已经拥有了全部后厨手艺:SQL 会写、Python 操作数据库会写、参数化查询会写。缺的只是前面那个”柜台”——一个能被浏览器访问的入口。
这个柜台,用 Flask 来搭。
二、Flask 是什么:给你的程序装个”网页门脸”
先掰扯清楚一个概念,今天就这一个新东西。
你平时跑的程序,是”你去找它”:你打开终端,敲命令,它才干活。而网页是反过来的,”它等你来找”:程序一直开着,随时等着浏览器来敲门,敲门的人说”我要看 /prices 这一页”,它就把对应的内容递出去。
能一直开着、等着浏览器来敲门、按请求递内容的程序,叫 Web 应用。Flask 就是帮你写 Web 应用的 Python 库——它是 Python 世界里最出名的”轻量级框架”之一,轻量到什么程度?核心就一个文件,装完即用,特别适合咱们这种”给自己的小工具加个网页门脸”的需求。
用开店打比方:你的 Python 程序是后厨(会做菜 = 会处理数据),数据库是仓库(存原料 = 存数据),Flask 是前面的柜台+菜单(客人点单 = 浏览器发请求,你上菜 = 返回网页)。今天这堂课,就是把柜台支起来,再把后厨和仓库接上。
装它一行命令:
pip install flask
三、十行代码:第一个能跑的网页
先跑通最小的例子,别急着接数据库:
# hello.py — 你的第一个 Web 应用
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "你好,我的第一个网页!
这是我的程序开的柜台。
"
@app.route("/about")
def about():
return "关于
这个网页背后是一个 Python 程序。
"
app.run(debug=True)
跑起来:
python hello.py
终端里会冒出一行 Running on http://127.0.0.1:5000——用浏览器打开这个地址,”你好,我的第一个网页!”就蹦出来了。再访问 http://127.0.0.1:5000/about,是另一个页面。
三个零件认一下:
app = Flask(__name__):开店,声明”我要开个 Web 应用了”。@app.route("/"):挂菜单。它下面那个函数负责”客人点了这道菜该上什么”。/是首页,/about是另一个页面——一个地址对应一个函数,这就是 Web 应用的全部世界观。app.run(debug=True):开门营业。debug=True是”装修模式”:改了代码自动刷新、报错时网页上直接显示是哪行出的问题,开发时特别顺手(它是怎么坑人的,坑点疫苗那节讲)。
注意
127.0.0.1的含义:它指”本机”,所以这个网页现在只有你自己能访问。想让家人也访问,要换局域网地址或者部署上线——那是后话,先把柜台支起来。
四、把数据库接上网页:完整的小应用
现在把三样东西接起来:Flask(柜台)+ sqlite3(电话线)+ shop.db(仓库)。做一个商品价格管理页:网页上能看列表、能添加、能删除。
# shop_web.py — 网页版商品管理(完整可运行)
import sqlite3
from flask import Flask, request, redirect, render_template_string
app = Flask(__name__)
DB = "shop.db"
def query(sql, args=(), write=False):
"""一条专线打到底:查完/写完自动挂电话。write=True 才盖章。"""
conn = sqlite3.connect(DB)
try:
cur = conn.execute(sql, args)
rows = cur.fetchall()
if write:
conn.commit()
return rows
finally:
conn.close()
# 建表:程序第一次跑时自动建好
query("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
)
""", write=True)
PAGE = """
商品价格表
商品价格表
编号 商品 价格 操作
{% for p in products %}
{{ p[0] }} {{ p[1] }} {{ '%.2f'|format(p[2]) }}
{% endfor %}
添加商品
"""
@app.route("/")
def index():
products = query("SELECT id, name, price FROM products ORDER BY id")
return render_template_string(PAGE, products=products)
@app.route("/add", methods=["POST"])
def add():
name = request.form["name"]
price = float(request.form["price"])
query("INSERT INTO products (name, price) VALUES (?, ?)", (name, price), write=True)
return redirect("/")
@app.route("/delete/", methods=["POST"])
def delete(pid):
query("DELETE FROM products WHERE id = ?", (pid,), write=True)
return redirect("/")
if __name__ == "__main__":
app.run(debug=True)
python shop_web.py 跑起来,浏览器打开 http://127.0.0.1:5000:
- 页面上是一张商品表——这就是数据库里 products 表的真身;
- 表单里填个”酱油 8.5″,点添加,页面一刷,酱油进表了——你的 INSERT 从命令行搬到了网页上;
- 点某行的删除按钮,那行就没了——你的 DELETE 也上网页了。
你妈不用敲任何命令,打开浏览器点点点就行。这就是”正经软件”的脸面。
几个新面孔,逐个认:
request.form:客人在表单里填的东西,从这里拿。表单里有个name="name"的输入框,request.form["name"]就是用户填的商品名。redirect("/"):干完活把客人领回首页。不然添加完停在一个空白页,客人不知道下一步干嘛。:地址里的变量。/delete/3里的 3 会变成pid=3传进函数。int保证它必须是整数,不是整数直接 404,顺手挡了一类乱输入。render_template_string(PAGE, products=products):把数据塞进 HTML 模板。{% for %}循环出行、{{ }}填值——HTML 归 HTML、Python 归 Python,比字符串拼接干净十倍。methods=["POST"]:给”添加””删除”这类会动数据的地址声明只收表单提交。为什么必须分 GET/POST,坑点疫苗里细说。
注意看 query() 这个封装:上篇讲的 connect / execute / commit / close 四件套全在里面,? 占位符参数化查询也原样保留——上篇的防注入功夫一点没白练,网页表单恰恰是用户输入最多的地方,比命令行更需要它。
五、GET 和 POST:为什么”点菜”和”下单”要分开
这是今天第二个要理清的概念(严格说算半个新知识,跟路由是一体的)。
浏览器访问一个地址,用的是 GET——意思是”给我看看”,纯粹的查看,不改变任何数据。你可以放心地刷新、收藏、分享 GET 页面,看一百遍数据纹丝不动。
而”添加商品””删除商品”是改变数据的动作,走 POST——意思是”我提交了东西,你照着办”。所以代码里写 methods=["POST"] 的地址,直接在浏览器地址栏敲是打不开的(405 错误),只有网页上的表单提交才能触发它。
为什么非要分得这么清?一个字:刷新。假如”删除商品”是个 GET 地址 /delete/3,你删完商品后手滑按了下 F5,浏览器会把”再删一次”原样重发——删一条变删两条(第二次删的是 3 号吗?不一定,反正数据又少了)。这叫”重复提交”,是 Web 开发里最经典的事故之一。用 POST + 表单提交,刷新只会提示”是否重新提交表单”,而且你还有 redirect 先把客人领回首页,双保险。
记一句顺口溜:看用 GET,改用 POST。
六、五个坑点疫苗(先打再上路)
坑1:debug=True 千万别带到”真开门营业”。 debug 模式会把报错详情、甚至代码片段直接显示在网页上,还带一个能执行任意代码的调试控制台——放在公网等于把店门钥匙挂在门把手上。口诀:开发时 debug=True,上线前改 False 或去掉(默认就是 False)。”上线”是以后的事,但这个习惯现在就得有。
坑2:数据库文件的路径是相对路径。 代码里 DB = "shop.db" 意思是”程序从哪儿启动,数据库就在哪个文件夹”。换个工作目录跑,它就找不到旧库、还会悄悄新建一个空的——”我数据怎么没了”的经典来源。稳妥写法是用绝对路径,或者用 os.path.join(os.path.dirname(__file__), "shop.db") 锚定在代码旁边。
坑3:端口被占。 报 Address already in use,说明 5000 端口被别的程序占着(可能是上次的 Flask 没关干净)。两个解法:把上一个终端 Ctrl+C 掉;或者换端口 app.run(debug=True, port=5001)。
坑4:表单字段名和后端对不上。 HTML 里写 name="name",Python 里就得 request.form["name"]。写成 name="mingzi" 而 Python 取 request.form["name"],直接 KeyError 400。字段名是两边的暗号,必须一字不差。
坑5:浮点价格。 float(request.form["price"]) 遇到用户填”abc”会当场炸掉报 500。真要上线,得先检查能不能转成数字——第 10 篇的错误处理在这等着呢,练习里让你亲手补上。
七、跟AI协作的正确姿势(Web应用特供版)
今天开始写”有界面”的程序了,跟AI提需求的话术升级一下:
- 先说用户动作,再说技术。 不说”给我写个 Flask CRUD”,而说”我要一个网页:打开看到商品价格表,能填表单添加商品,每行有个删除按钮,删完回到列表页”——AI 会自己决定用什么技术。前者你也看不懂产出,后者生成的网页你点一点就知道对不对。
- 拿到代码先跑,用浏览器验收。 判断标准不是”代码看着像对的”,而是”我在页面上点添加,表格里真的多了一行”。网页程序的验收就是亲手点一遍。
- 改需求继续说人话。 “价格显示保留两位小数””删除前弹个确认框””表单填了非数字要提示而不是报错”——一句话一个需求,看效果、再改,这就是”跟AI说需求→读懂→改进”的循环,跟改数据脚本没区别,只是多了一个可以点的界面。
八、今日小练习
练习1:给价格监控工具装上门脸。 沿用 shop.db(或自己建个商品表),照第四节做一个网页版,要求:列表按价格从高到低显示;添加表单多一个”品牌”字段;删除按钮换成”降价 10%”按钮(点一次价格乘 0.9)。
提示:ORDER BY price DESC;建表语句加一列 brand;”降价”是 UPDATE 不是 DELETE,price = price * 0.9 就行;三个写操作都走 POST。
练习2:把坑5补上——优雅地拒绝烂输入。 在 /add 里加上检查:价格转不成数字、或者填了负数时,不许入库,回到首页并且页面上显示一行红字”价格填得不对”。(进阶提示:用 Flask 的 flash() 或者给模板传一个 error 变量,选你顺手的。)
提示:try: price = float(...) except ValueError: 是第 10 篇的老朋友;检查通过才执行 INSERT;出错时给 render_template_string 多传一个变量,在模板里用 {% if error %} 显示。
练习3(坑点自考):先诊断,再对答案。 下面每段都有毛病,不看文章说出病在哪、怎么改:①把第四节的 app.run(debug=True) 原样部署到云服务器对外访问;②”删除商品”写成 @app.route("/delete/") 没加 methods,浏览器访问 /delete/3 就把商品删了;③换了台电脑跑 shop_web.py,打开页面表格是空的,以为数据丢了。
提示:①装修模式的门不能开在大街上;②想想”看用 GET,改用 POST”那句顺口溜,以及 F5 的下场;③数据库这个 .db 文件不会跟着代码自己搬家。
下篇预告
下一篇(第57篇):数据备份 — 给仓库留后手。 数据住进了数据库、网页也有了,可万一 shop.db 被误删、被覆盖,几十条价格记录就全没了。下一篇学导出和导入:一行命令把整座仓库复制一份,再一行命令从备份搬回来。备份这件事,工具一分钟就能跑完,但”没备份”这三个字的代价,是任何代码都补不回来的。