本篇是《Python 从入门到初级》第 16 / 20 篇。示例使用 Python 3.12 兼容语法,完整目录结构与数据见配套源码包。
CSV 和 JSON 适合交换文件,但当数据需要反复查询、按条件更新并长期保存时,数据库会更方便。SQLite 以文件方式存储,通常不需要单独启动数据库服务,适合本地工具和学习。

1. Python 和数据库各负责什么
Python 负责组织流程、验证输入和处理结果,SQL 负责描述数据库中的写入与查询。本篇把已有订单 CSV 导入本地数据库,再按状态汇总分类金额。
示例继续使用 ops_tools/orders.py 验证数据,因此要保留第 13 篇的包结构。数据库放在自己的 output/orders.sqlite3,不连接线上服务。
先查看实际 SQLite 库版本:
python -c "import sqlite3; print(sqlite3.sqlite_version)"
本例使用的 UPSERT 语法需要 SQLite 3.24.0 或更新版本。Python 版本和底层 SQLite 版本不是同一个数字,特别是在自行编译或特殊发行环境中要区分。
2. 完整示例:导入与参数化查询
运行 python lesson16.py:
import sqlite3
from pathlib import Path
from ops_tools.orders import read_orders
def main():
base = Path(__file__).resolve().parent
output = base / "output"
output.mkdir(exist_ok=True)
orders = read_orders(base / "data" / "orders.csv")
connection = sqlite3.connect(output / "orders.sqlite3")
try:
with connection:
connection.execute("""
CREATE TABLE IF NOT EXISTS orders (
order_id TEXT PRIMARY KEY,
category TEXT NOT NULL,
amount_cents INTEGER NOT NULL CHECK(amount_cents >= 0),
status TEXT NOT NULL
)
""")
connection.executemany("""
INSERT INTO orders(order_id, category, amount_cents, status)
VALUES (:order_id, :category, :amount_cents, :status)
ON CONFLICT(order_id) DO UPDATE SET
category=excluded.category,
amount_cents=excluded.amount_cents,
status=excluded.status
""", orders)
rows = connection.execute("""
SELECT category, SUM(amount_cents)
FROM orders WHERE status = ?
GROUP BY category ORDER BY category
""", ("paid",)).fetchall()
print("已支付分类统计:", rows)
print("数据库订单数:", connection.execute("SELECT COUNT(*) FROM orders").fetchone()[0])
unsafe_looking_text = "paid' OR 1=1 --"
count = connection.execute("SELECT COUNT(*) FROM orders WHERE status = ?",
(unsafe_looking_text,)).fetchone()[0]
print("特殊字符串作为参数的匹配数:", count)
finally:
connection.close()
if __name__ == "__main__":
main()
干净的教学数据库中,预期订单数为 5,已支付分类金额仍是图书 5900、办公 12500、数码 19900 分。数据库输出的分类顺序不必和 CSV 输入顺序一致。
看起来像 SQL 的特殊字符串,作为 status 参数传入后,预期匹配数为 0,而不会变成另一段查询条件。
3. 为什么重复导入没有变成十笔
order_id 是主键,用来标识同一笔订单。插入遇到同一主键时,ON CONFLICT ... DO UPDATE 更新该记录的字段,而不是额外增加一行。
其中 excluded 表示本次准备写入的新值。这个模式便于重复运行教学导入,但不意味着数据库会自动与 CSV 完全同步:CSV 中没有出现的旧数据库记录,不会被本例删除。
语法与限制见 SQLite UPSERT 文档。真实导入还需要明确新旧数据优先级、删除规则和审计要求。
4. 值通过参数传,不拼接 SQL
WHERE status = ? 中的问号是占位符,具体值由后面的元组传入。单元素元组写成 ("paid",),末尾逗号不能忘。
不要把外部输入通过 f-string 或字符串相加拼进 SQL。参数化不仅处理引号,更让数据保持为数据。
占位符用于值,不能直接代替表名或列名。如果需要动态选择字段,通常要从程序自己定义的允许列表选择,而不是接受任意输入。
Python 数据库接口说明见 sqlite3 文档。
5. 提交、回滚和关闭是不同动作
with connection 管理事务的正常提交和异常回滚,但不会替你关闭连接。因此示例用 finally 调用 close。
表结构语句和写入语句的事务细节受连接模式影响,不应把一个 with 简单理解为“文件里所有操作都能整体撤销”。先明确哪些写入处于同一事务,再讨论失败后会留下什么。
此处 executemany 为每条记录绑定参数;fetchall 将查询结果读成列表。数据很大时,读取和写入方式都需要重新评估。
6. 练习与验证
连续运行两次,保持原始 CSV 不变,预期订单数仍为 5。把 A001 的金额改为 6000 分再运行,预期图书的已支付金额更新为 6000 分。练习后恢复 CSV,重新导入恢复基准。
如果自己另加过其他主键,数据库可能超过五行,这不是重复导入造成的。通过查询查明来源,再决定怎样维护教学数据,不要把清库当成唯一的验证方式。