AI编程(五十六):数据库+Flask — 让网页替你操作数据库

数据终于住进了正经数据库,可总不能让家人朋友也来敲 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 %} {% endfor %}
编号商品价格操作
{{ p[0] }}{{ p[1] }}{{ '%.2f'|format(p[2]) }}

添加商品

""" @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提需求的话术升级一下:

  1. 先说用户动作,再说技术。 不说”给我写个 Flask CRUD”,而说”我要一个网页:打开看到商品价格表,能填表单添加商品,每行有个删除按钮,删完回到列表页”——AI 会自己决定用什么技术。前者你也看不懂产出,后者生成的网页你点一点就知道对不对。
  2. 拿到代码先跑,用浏览器验收。 判断标准不是”代码看着像对的”,而是”我在页面上点添加,表格里真的多了一行”。网页程序的验收就是亲手点一遍。
  3. 改需求继续说人话。 “价格显示保留两位小数””删除前弹个确认框””表单填了非数字要提示而不是报错”——一句话一个需求,看效果、再改,这就是”跟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 被误删、被覆盖,几十条价格记录就全没了。下一篇学导出和导入:一行命令把整座仓库复制一份,再一行命令从备份搬回来。备份这件事,工具一分钟就能跑完,但”没备份”这三个字的代价,是任何代码都补不回来的。