ARTICLE DETAIL

资讯详情

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

Python+MySQL+HTML手把手搭建图书管理系统:从连接到渲染

Python+MySQL+HTML手把手搭建图书管理系统:从连接到渲染 最近帮学生调一个数据库课程设计题目是经典的“图书管理系统”。他代码其实写得不算差但一直卡在同一个地方用Python操作MySQL数据库时连接不上好不容易连上了网页上又显示乱码等数据能查出来了又不知道怎么把结果塞回HTML页面里。我帮他理顺整个链路后发现他缺的不是某条语法而是对“HTML、Python、MySQL三者之间到底怎么协作”缺少一个整体画面。这篇文章就把我跟他讲的完整过程整理出来。核心是一条数据流浏览器里的HTML表单提交到Python后端Python把数据写入MySQL反过来Python从MySQL查出记录动态渲染成HTML表格再返回给浏览器显示。适合刚学完Python语法、正在做数据库课程设计或者想用Python MySQL HTML做小项目打基础的人看完可以照着抄跑通后再去学框架会轻松很多。1. 为什么是Python MySQL HTML这条组合技术选型背后的真实考量很多初学者一上来就纠结“要不要学Flask”“要不要用Django”但其实在真正理解Web交互原理之前越早上框架越容易陷入黑盒。我建议先用手写的方式把HTML、Python、MySQL串起来跑通一次完整请求再去看框架就一目了然。1.1 三层结构到底怎么分工可以把这个系统想成一家餐厅MySQL是仓库负责存放所有食材数据并且只认SQL语言。Python是后厨负责接收点菜单HTTP请求、按照菜单去仓库取菜执行SQL、再把菜做好处理数据。HTML是餐桌上的菜单和传菜窗口负责把用户想点的内容展示出来以及把做好的菜端给用户呈现结果。这三层各有各的协议和语言。浏览器和Python之间走的是HTTP协议Python和MySQL之间走的是MySQL协议。你不需要深究协议细节但必须清楚跨协议通信时中间一定有一层“翻译官”Python就是这个翻译官。1.2 一次完整请求的旅程以“用户添加一本新书”为例整个流程是这样的用户在浏览器里打开HTML页面看到一个表单。用户填写书名、作者、分类、价格、库存点击提交按钮。浏览器把表单内容打包成一个POST请求发送给Python服务。Python解析请求拿到表单里的字段。Python把字段拼接成一条SQL的INSERT语句发送给MySQL执行。MySQL写入成功后返回结果Python收到结果。Python再执行一条SELECT查询拿到最新的图书列表。Python把查询结果拼成HTML表格的tr行塞进HTML模板。浏览器收到完整的HTML页面并渲染用户看到新书出现在表格里。这条链路初学者最好亲手走一遍。你可以先不管Flask的路由、模板引擎等概念因为那都是在这条链路上做的自动化封装。底层逻辑永远是浏览器发请求 - Python处理 - MySQL操作 - Python拼HTML - 浏览器渲染。1.3 这套组合适合谁这套组合最适合三类场景数据库课程设计比如图书管理、学生选课、员工信息管理系统。团队内部小工具不需要高并发只要能在网页上增删改查。Python入门后的第一个“前后端联动”练手项目。它不适合大型线上业务。一旦用户量上来单连接访问MySQL、每次请求都新建连接、模板字符串拼接页面这些做法都会成为瓶颈。但在学习阶段把基础链路搞清楚比盲目上高并发方案重要得多。2. 环境准备驱动选择和连接参数里的坑这个阶段我见过太多人卡住尤其是第一次运行import pymysql后直接报错或者连接MySQL时提示认证插件不兼容。下面把环境和常见的坑一次说清楚。2.1 需要装什么、版本怎么配我建议的环境版本如下组件版本建议说明Python3.8 及以上3.8以下有些依赖兼容性差不建议MySQL5.7 或 8.08.0需要额外注意认证插件问题PyMySQL1.0 及以上pip安装即可纯Python实现数据库管理工具Navicat / DBeaver / MySQL Workbench主要用于调试SQL不是必须如果你的电脑还没装Python或MySQL网上一搜“python安装教程”或“mysql安装配置教程”有很多但有一个关键点要注意安装MySQL时务必记住root密码并把端口确认在3306。很多人后面连不上就是安装时密码输了两遍结果根本不在同一台机器上调试这类问题排查起来非常浪费时间。安装PyMySQL用一行命令pip install pymysql如果下载慢可以换成国内镜像源pip install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后在Python交互式环境里执行import pymysql print(pymysql.__version__)能打印出版本号就说明环境没问题。2.2 pymysql 与 mysql-connector-python 的取舍Python连MySQL的驱动工具不止一个最常见的两个是PyMySQL和MySQL官方提供的mysql-connector-python。我推荐新手直接用PyMySQL理由很实际对比项PyMySQLmysql-connector-python安装复杂度pip装完直接用可能牵扯protobuf等依赖维护活跃度社区活跃更新稳定官方维护但版本变动大与MySQL 8兼容1.0版本后兼容性良好官方兼容完整学习资料数量网上教程多报错好搜相对少一些PyMySQL是纯Python实现不需要本地编译装完就是能用的状态。而mysql-connector-python虽然功能更强但对新手来说偶尔会遇到依赖报错处理起来很劝退。等以后你有了更多经验再按需换驱动也不迟。2.3 首次连接必报的四个经典错我帮你把新手最常见的连接错误整理成一张表照着排查效率最高错误现象常见原因解决办法2003 Cant connect to MySQL serverMySQL服务没启动或端口不对确认服务已启动确认连接参数里端口是33061045 Access denied for user用户名或密码错误或者远程连接没开权限核对root密码确认创建了允许对应IP访问的用户2059 Authentication plugin 异常客户端与MySQL 8的默认认证插件不兼容升级PyMySQL到1.0以上或修改MySQL用户的认证插件乱码 / 中文变问号数据库连接charset没有设置成utf8mb4连接参数加charsetutf8mb4建库时也指定utf8mb4第2059的错误在MySQL 8.0环境下特别容易出现。MySQL 8默认的认证插件是caching_sha2_password而旧版PyMySQL用的是mysql_native_password协议两边对不上就会报错。解决办法很简单升级PyMySQLpip install --upgrade pymysql如果你不想升级PyMySQL也可以这样修改MySQL用户认证插件ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;但注意这种办法适合本地学习环境生产环境尽量用新版驱动不要随意降低认证安全级别。3. 从业务出发设计数据库以图书管理系统为例很多人习惯上来就写SQL跳过需求梳理。但实际上表结构设计直接决定后续代码好不好写。我用一个图书管理系统当例子把从需求到建表的完整过程拆开。3.1 先梳理需求再建表先列出这个系统必须满足的功能新增图书需要记录书名、作者、分类、价格、库存。查看图书列表展示所有图书。按书名或作者查询方便检索。根据这些功能表可以拆成两张分类表categories和图书表books。为什么不能把分类名直接写进books表因为同一分类下可能有多本书。如果把“计算机”这个字符串重复存到每一本书里数据冗余不说后面想改成“计算机技术”就要更新一大堆记录。拆成两张表后books表只需要保存分类ID查询时通过JOIN拿到分类名这是关系型数据库最基本的规范化思想。3.2 建表SQL与字段说明CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_db; CREATE TABLE categories ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) NOT NULL COMMENT 作者, category_id INT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 价格, stock INT NOT NULL DEFAULT 0 COMMENT 库存, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 录入时间, CONSTRAINT fk_books_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;一些设计细节DECIMAL(10,2)存价格不要用FLOAT因为浮点数在计算金额时会有精度误差。TIMESTAMP DEFAULT CURRENT_TIMESTAMP会让录入时间自动生成省去代码里手动传值。外键的ON DELETE SET NULL表示分类被删除后图书不删除只是分类字段置为NULL。所有表都显式指定ENGINEInnoDB因为MyISAM不支持外键和行级事务。建好表后插入一些分类和样例数据INSERT INTO categories (name) VALUES (计算机), (文学), (历史); INSERT INTO books (title, author, category_id, price, stock) VALUES (Python编程从入门到实践, Eric Matthes, 1, 89.00, 20), (MySQL必知必会, Ben Forta, 1, 49.00, 35), (活着, 余华, 2, 35.00, 50), (明朝那些事儿, 当年明月, 3, 138.00, 15);3.3 索引和外键什么时候要什么时候别乱加对外键很多教材强调“必须有”但在真实业务里外键会影响写入性能所以大型互联网系统往往刻意不用外键只保留逻辑关联。学习项目里加上外键没毛病能帮你理解约束关系。索引方面这个示例项目里有一个明显的查询场景按书名搜书。如果books表数据量变大WHERE title LIKE %关键词%这样的模糊查询是走不了普通索引的只能全表扫描。更合理的做法是条件改成前缀匹配SELECT * FROM books WHERE title LIKE Python%;这样就能用到title字段上的索引。你可以用EXPLAIN语句验证EXPLAIN SELECT * FROM books WHERE title LIKE Python%;看到type列是range或ref说明索引生效了如果是ALL说明还是全表扫描。设计索引时优先考虑频繁出现在WHERE和JOIN条件里的字段。4. Python操作MySQL的核心代码连接、增删改查与安全问题环境没问题、表也建好了接下来就是真正的重头戏用Python写代码操作MySQL。这一章我把连接封装、增删改查、SQL注入和事务处理一次讲透。4.1 连接封装不要让连接代码散落到处最忌讳的是每写一个函数就pymysql.connect()一次把同样的参数复制好几遍。后续密码改了要满项目找。我通常把连接封装成一个模块单独放在db.py里。# db.py import pymysql DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: 你的密码, database: library_db, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, } def get_connection(): return pymysql.connect(**DB_CONFIG)这里几个参数逐一说一下host填127.0.0.1表示本机填localhost也通常可以但127.0.0.1可以跳过一些系统下的DNS解析问题。charsetutf8mb4必须加这是中文不乱码的关键。cursorclasspymysql.cursors.DictCursor让查询结果变成字典列表而不是元组列表。字段名比下标好用得多。4.2 通用查询与执行函数接着在这同一个文件里写两个通用函数# db.py def query_all(sql, paramsNone): 执行SELECT语句返回所有结果 conn get_connection() try: with conn.cursor() as cursor: cursor.execute(sql, params) return cursor.fetchall() finally: conn.close() def execute(sql, paramsNone): 执行INSERT/UPDATE/DELETE提交事务 conn get_connection() try: with conn.cursor() as cursor: cursor.execute(sql, params) conn.commit() except Exception: conn.rollback() raise finally: conn.close()query_all里的try...finally保证无论查询成功还是异常连接都能关闭。execute里则多了一个conn.commit()。这里必须强调pymysql默认开启事务执行完INSERT/UPDATE/DELETE之后不调用commit数据不会真正写入数据库。很多新手添加数据后列表没更新就是忘了一件事光执行SQL不提交。4.3 参数化查询SQL注入必须从第一天就防先看一个反面教材很多初学教程会这样写# 反面案例不要这么写 title Python入门 sql SELECT * FROM books WHERE title title 这种字符串拼接有个致命问题如果title是从用户输入来的用户输入了这样一段内容 OR 11拼出来的SQL就变成了SELECT * FROM books WHERE title OR 11因为OR 11恒为真这条查询会返回全部图书数据。如果用到DELETE语句上后果更严重。这就是最基本的SQL注入原理。正确做法是使用参数化查询让MySQL驱动帮你处理转义def search_books(keyword): sql SELECT * FROM books WHERE title LIKE %s OR author LIKE %s like_word f%{keyword}% return db.query_all(sql, (like_word, like_word))注意这里的%s不是Python字符串格式化而是pymysql的占位符第二个参数传入元组后驱动会安全地把值绑定到SQL上用户输入里再带引号或特殊字符都不会破坏SQL结构。这个习惯一定要在写第一行SQL时就养成。等以后项目上线了再补成本会高出很多。4.4 事务处理commit与rollbackexecute函数里的conn.rollback()在什么时候起作用举一个实际场景同时执行两条操作先把某本书的库存扣减再往订单表插入记录。如果第二条SQL因为某个字段太长而失败而第一条已经执行成功那库存就被扣掉了但订单没生成两边对不上。事务保证的就是“要么全成功要么全失败”。所以业务代码里多步写操作最好手动管理事务而不是只调用单个executeconn db.get_connection() try: with conn.cursor() as cursor: cursor.execute(UPDATE books SET stock stock - %s WHERE id %s, (1, book_id)) cursor.execute(INSERT INTO orders (book_id, quantity) VALUES (%s, %s), (book_id, 1)) conn.commit() except Exception as e: conn.rollback() print(事务回滚:, e) raise finally: conn.close()在这里commit()提交整个事务rollback()取消当前事务里的所有操作。只有InnoDB引擎支持事务这也是建表时指定ENGINEInnoDB的原因。5. 用HTML页面完成前端交互不写框架也能串起来前面所有代码都是在Python端操作数据库但用户不会直接运行Python脚本他们要的是网页。所以最后一步很关键让Python启动一个HTTP服务把HTML页面返回给浏览器并处理表单提交。5.1 项目文件结构建议按下面这样组织项目文件之间职责清晰library/ ├── db.py # 数据库连接与通用操作 ├── server.py # HTTP服务与业务处理 └── index.html # 前端页面模板5.2 HTML页面骨架每个标签都别抄得不明不白index.html不需要复杂前端框架一个表单加一个表格就够!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title图书管理系统/title style body { font-family: Arial, sans-serif; margin: 40px; } form { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; } input { margin-right: 10px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } /style /head body h1图书管理系统/h1 form methodpost action/ input typetext nametitle placeholder书名 required input typetext nameauthor placeholder作者 required input typetext namecategory_id placeholder分类ID required input typenumber step0.01 nameprice placeholder价格 required input typenumber namestock placeholder库存 required button typesubmit添加图书/button /form table thead tr thID/th th书名/th th作者/th th分类/th th价格/th th库存/th /tr /thead tbody __TABLE_ROWS__ /tbody /table /body /html你可能见过网上一大段HTML模板像!doctype htmlhtml langzh-cnheadmeta charsetutf-8这些内容但不知道每行是干嘛的。这里简单拆解!DOCTYPE html告诉浏览器这是HTML5文档。html langzh-CN声明页面语言是简体中文有助于浏览器翻译和阅读辅助工具识别。meta charsetUTF-8指定页面编码是UTF-8。如果漏了这行浏览器可能用系统默认编码解析中文会乱码。meta nameviewport contentwidthdevice-width, initial-scale1.0让页面在手机上也能自适应宽度做网页时建议保留。__TABLE_ROWS__是占位符稍后Python会用真实数据替换它。表里放的是书每行的tr记录所以这里我故意留了占位符。5.3 Python把数据库记录动态渲染成HTML表格然后写server.py。这里用Python标准库http.server完全不依赖第三方Web框架逻辑透明适合学习。# server.py import html from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs import db def load_template(pathindex.html): with open(path, r, encodingutf-8) as f: return f.read() def render_book_list(): rows db.query_all( SELECT b.id, b.title, b.author, c.name AS category_name, b.price, b.stock FROM books b LEFT JOIN categories c ON b.category_id c.id ORDER BY b.id DESC ) tr_list [] for r in rows: tr_list.append( tr ftd{r[id]}/td ftd{html.escape(r[title])}/td ftd{html.escape(r[author])}/td ftd{html.escape(r[category_name] or -)}/td ftd{r[price]}/td ftd{r[stock]}/td /tr ) return .join(tr_list) class LibraryHandler(BaseHTTPRequestHandler): def _send_html(self, content): self.send_response(200) self.send_header(Content-Type, text/html; charsetutf-8) self.end_headers() self.wfile.write(content.encode(utf-8)) def do_GET(self): page load_template().replace(__TABLE_ROWS__, render_book_list()) self._send_html(page) def do_POST(self): length int(self.headers.get(Content-Length, 0)) body self.rfile.read(length).decode(utf-8) form parse_qs(body) title form.get(title, [])[0].strip() author form.get(author, [])[0].strip() category_id form.get(category_id, [])[0].strip() price form.get(price, [0])[0].strip() stock form.get(stock, [0])[0].strip() if title and author: db.execute( INSERT INTO books (title, author, category_id, price, stock) VALUES (%s, %s, %s, %s, %s), (title, author, int(category_id) if category_id else None, float(price), int(stock)), ) self.send_response(302) self.send_header(Location, /) self.end_headers() if __name__ __main__: server HTTPServer((127.0.0.1, 8000), LibraryHandler) print(服务已启动http://127.0.0.1:8000) server.serve_forever()这里有几个细节值得单独说明html.escape()用于把用户输入的内容做HTML转义。如果书名里包含script这类标签不转义就可能被浏览器当作代码执行这就是XSS漏洞的基础防护。category_name or -用来处理外键为NULL的情况避免渲染出None。do_POST处理完数据后返回302重定向到首页避免用户刷新页面导致表单重复提交。5.4 实测把服务跑起来看效果在项目目录下执行python server.py然后打开浏览器访问http://127.0.0.1:8000。正常效果是表格里显示刚插入的4本书。你在表单里填一本新书点击提交页面自动刷新新书出现在列表最上面。如果添加完没看到新数据优先检查execute里是否调用了commit()如果页面中文显示乱码检查HTML的meta charset、MySQL连接参数、数据库建库字符集是不是都是utf8mb4。还有一个高频问题运行python server.py后提示端口被占用。这通常是因为上一次的进程没有关闭改一下端口就绕开了server HTTPServer((127.0.0.1, 8001), LibraryHandler)访问时对应改成http://127.0.0.1:8001即可。这个项目做完我最想提醒后来人的一点是不要一上来就搜“免费python源码大全”或者到处找现成代码哪怕搜到了也要亲手把这条链路拆开重写一遍。你第一次手写这套东西可能要折腾一整天但之后再看Flask、Django甚至看任何后端框架都会发现它们只是在帮你把server.py这部分做规范、做高效。HTML表单怎么提交、Python怎么接收、MySQL怎么读写心里有了这张地图后面所有技术都只是在这条主干道上加工具。
返回列表