MySQL与Python实战:从环境搭建到安全CRUD操作

MySQL与Python实战:从环境搭建到安全CRUD操作
1. 为什么是 MySQL Python这不是“Hello World”式的选择而是工程现实的必然MySQL 和 Python 的组合在我过去十年带过的三十多个数据项目里出现频率稳居前三——不是因为它们最时髦而是因为它们在真实世界里“扛得住、修得快、招得到人”。你可能在教程里看到过 PostgreSQL、SQLite 甚至 MongoDB 的闪亮登场但当你接手一个日活 5 万的电商后台、一个需要支撑三年审计追溯的财务系统、或者一个要和 Excel 报表、BI 工具、爬虫脚本无缝对接的运营中台时MySQL Python 往往是那个被拍板写进技术方案文档第一行的组合。它不炫技但足够扎实它不激进但足够灵活。这背后有三重硬逻辑第一MySQL 是目前全球部署量最大的开源关系型数据库它的协议、驱动、运维工具链已经沉淀了二十多年几乎任何 Python 环境下都能用pip install mysql-connector-python一行搞定连接不像某些新锐数据库光是装个驱动就要折腾半小时环境变量和编译器版本。第二Python 的mysql-connector-python官方驱动和PyMySQL纯 Python 实现这两个主流库已经稳定迭代到 8.x 和 1.1.x 版本对 MySQL 5.7/8.0 的所有核心特性——包括 JSON 字段、窗口函数、CTE 递归查询——都提供了开箱即用的支持你不需要自己封装一层又一层的抽象。第三也是最关键的一点它完美匹配“数据管道”的中间态角色。前端可能是 Vue 或 React后端可能是 Django 或 Flask但真正做数据清洗、批量导入、定时报表生成、异常数据修复这些脏活累活的往往就是一段跑在 Linux 服务器上的 Python 脚本它只需要连上 MySQL读、写、算、存四步闭环干净利落。所以这篇教程我不会从“什么是数据库”开始讲起也不会花大篇幅对比 ACID 和 BASE 理论。我会直接带你走进一个真实开发者的桌面一台装着 Windows 或 macOS 的笔记本一个终端窗口一个 VS Code 编辑器以及一个明确的目标——在 45 分钟内从零开始亲手把一本《代码大全》的信息存进你本地电脑的 MySQL 里并能用 Python 脚本把它准确地查出来、改掉作者名、再删掉一条测试数据。过程中你会遇到安装报错、密码不对、表结构建错、中文乱码、事务没提交导致查不到新数据……这些都不是“理论问题”而是你明天上午十点就可能收到的钉钉消息“张工线上订单表少了一条数据能帮忙查下吗”——而这篇教程就是你打开终端、敲下第一行pip install时心里那点笃定的底气来源。2. 环境搭建不是“复制粘贴”而是理解每一步背后的“为什么”2.1 安装 MySQL Server选社区版绕过所有“一键安装器”陷阱很多新手卡在第一步不是因为不会下载而是因为下载了错误的安装包。MySQL 官网提供两种主流安装方式MySQL Installer for Windows图形化向导和MySQL Community Server ZIP Archive免安装压缩包。我强烈建议你选择后者原因有三透明可控Installer 向导会偷偷帮你勾选一堆你根本不需要的服务比如 MySQL Router、MySQL Workbench虽然好用但初学阶段纯属干扰甚至还会试图修改你的系统 PATH 环境变量导致后续你装的其他数据库工具如 PostgreSQL 的 psql命令冲突。而 ZIP 包解压即用所有路径、端口、配置文件都在你眼皮底下。便于调试当连接失败时你能直接去bin目录下手动执行mysqld --console实时看到服务启动的日志输出哪一行报错一目了然。Installer 向导则把这些日志藏得极深你得翻注册表或服务管理器才能找到。贴近生产线上服务器几乎 100% 使用的是 tar.gz 或 ZIP 解压部署模式学会这个等于提前踩过了生产环境的第一道坑。实操步骤以 Windows 为例macOS/Linux 逻辑完全一致仅路径不同去 MySQL 官网下载页 滚动到最下方找到 “MySQL Community Server” 区域点击 “Download” 按钮注意不要点旁边那个绿色的 “No thanks, just start my download.”那是个广告陷阱点它会跳转到第三方下载站。在弹出的页面中取消勾选 “No thanks, just start my download.”然后在列表中找到 “Windows (x86, 64-bit), ZIP Archive”点击右侧的 “Download”。你需要一个 Oracle 账号用邮箱快速注册一个即可。下载完成后解压到一个全英文、无空格、无中文的路径例如C:\mysql-8.0.33。这是铁律。如果你解压到C:\Program Files\mysql或D:\我的数据库后面 90% 的报错都源于此。进入解压后的目录找到my.ini文件如果没有就新建一个。用记事本或 VS Code 打开它填入以下最简配置[mysqld] # 服务端口保持默认3306即可除非你机器上已有其他服务占用了 port3306 # 数据库存储路径必须是你解压目录下的一个子文件夹且该文件夹必须存在 datadirC:/mysql-8.0.33/data # 字符集这是中文不乱码的关键 character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci # 默认存储引擎 default-storage-engineINNODB # 忽略大小写Windows下建议开启避免因表名大小写引发的麻烦 lower_case_table_names1 [client] # 客户端默认字符集 default-character-setutf8mb4提示datadir路径里的斜杠/在 Windows 上必须用正斜杠不能用反斜杠\否则 mysqld 会无法识别。这是无数人踩过的坑。以管理员身份打开命令提示符CMD切换到C:\mysql-8.0.33\bin目录执行初始化命令mysqld --initialize --console你会看到一大段输出其中最后一行会显示类似A temporary password is generated for rootlocalhost: iY6?gK9!sLpX的内容。请立刻、马上、用记事本记下这个临时密码它只出现这一次丢了就得重装。 6. 接着将 MySQL 注册为 Windows 服务这样开机就能自动运行mysqld --install MySQL80MySQL80是服务名你可以自定义但建议带上版本号方便管理 7. 启动服务net start MySQL80首次登录并修改密码mysql -u root -p # 输入你刚才记下的临时密码 # 进入 MySQL 命令行后执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY YourStrongPassword123!; FLUSH PRIVILEGES; EXIT;至此你的本地 MySQL 服务才算真正“活”了过来。整个过程大约需要 12 分钟比 Installer 向导慢不了多少但你收获的是对整个服务生命周期的掌控感。2.2 安装 Python 驱动mysql-connector-pythonvsPyMySQL选哪个Python 连接 MySQL 的主流驱动有两个Oracle 官方的mysql-connector-python和社区维护的PyMySQL。它们的区别不是“好坏”而是“适用场景”。mysql-connector-python由 Oracle 官方维护对 MySQL 8.0 的新特性如 caching_sha2_password 认证插件支持最及时、最稳定。它的底层是 C 扩展性能略高。如果你的项目最终要上生产且数据库版本是 8.0这是唯一推荐的选择。安装命令就是最简单的pip install mysql-connector-pythonPyMySQL纯 Python 实现没有 C 依赖因此在一些受限环境如某些云函数、Alpine Linux 容器里安装更顺畅。它的语法和行为与mysql-connector高度兼容学习成本几乎为零。如果你只是本地学习、写个小脚本或者在 Docker 里跑PyMySQL是个更轻量、更“佛系”的选择。安装命令pip install PyMySQL注意网上很多老教程会推荐MySQLdb但它早已停止维护不支持 Python 3.8请务必绕开。2.3 创建安全的开发环境.env文件不是可选项是必选项在原始教程里它提到“创建.env文件来存密码”但没说清楚为什么这一步比写 SQL 还重要。答案是防止你把生产数据库密码一不小心 commit 到 GitHub 上。我见过太多次一个实习生为了“方便”把password123456写死在代码里然后推送到公司公开仓库结果被自动化扫描工具抓取导致整个数据库被拖库。.env文件的原理极其简单它是一个纯文本文件里面只放键值对比如DB_HOSTlocalhost DB_USERroot DB_PASSWORDYourStrongPassword123! DB_NAMEbook_ratings DB_PORT3306然后在 Python 代码里用python-dotenv库来加载它from dotenv import load_dotenv import os load_dotenv() # 自动在当前目录及父目录中寻找 .env 文件 db_config { host: os.getenv(DB_HOST), user: os.getenv(DB_USER), password: os.getenv(DB_PASSWORD), database: os.getenv(DB_NAME), port: int(os.getenv(DB_PORT)) }关键操作在你的项目根目录比如C:\my_project下新建一个名为.env的文件。注意文件名开头有个点.在 Windows 资源管理器里可能看不到建议用 VS Code 或命令行notepad .env来创建。将上面的配置内容复制进去务必把YourStrongPassword123!替换成你 MySQL 的真实密码。在项目根目录下新建一个.gitignore文件里面写上.env __pycache__/ *.pyc这样Git 就永远不会把你包含密码的.env文件上传到远程仓库。这三步做完你才真正拥有了一个“安全、可复现、可协作”的最小开发环境。它看起来琐碎但正是这些琐碎构成了专业和业余的分水岭。3. 核心操作详解从“连上”到“玩转”每一步都是血泪教训3.1 建立连接connect()方法的参数远不止host,user,password三个原始教程的连接代码非常简洁connector.connect(hostlocalhost, userroot, passwordPASSWORD)但这只是冰山一角。一个健壮的生产级连接至少要考虑以下六个参数参数必填默认值为什么重要我的建议值host是localhost指定 MySQL 服务器地址。localhost和127.0.0.1在 MySQL 里含义不同前者走 Unix Socket更快后者走 TCP/IP更通用。如果连接不上先试试换一个。127.0.0.1更稳定兼容性更好port否3306MySQL 服务监听的端口。如果你改过配置这里必须同步。3306保持默认user是无数据库用户名。永远不要在生产环境用root应创建专用账号。app_user后续会教你如何创建password否无密码。如果为空字符串表示无密码如果是None则会报错。从.env读取绝不硬编码database否无最关键的参数之一。如果指定了连接成功后就自动USE database_name;后续所有 SQL 都在这个库下执行。省去每次cursor.execute(USE book_ratings)的麻烦。book_ratings你的库名charset否utf8中文乱码的终极解决方案。utf8在 MySQL 里实际只支持 3 字节 UTF-8 字符不支持 emoji必须用utf8mb4。utf8mb4所以一个更完善的连接函数应该是这样的import mysql.connector from mysql.connector import Error from dotenv import load_dotenv import os def create_connection(): 创建一个健壮、可重用的 MySQL 连接 load_dotenv() config { host: os.getenv(DB_HOST, 127.0.0.1), port: int(os.getenv(DB_PORT, 3306)), user: os.getenv(DB_USER, root), password: os.getenv(DB_PASSWORD, ), database: os.getenv(DB_NAME, ), charset: utf8mb4, autocommit: False, # 关键关闭自动提交由我们手动控制事务 pool_name: mypool, # 连接池名用于后续复用 pool_size: 5, # 连接池大小避免频繁创建销毁连接 pool_reset_session: True } try: connection mysql.connector.connect(**config) print(✅ 数据库连接成功) return connection except Error as e: print(f❌ 连接失败: {e}) return None # 使用 conn create_connection() if conn is not None: # 后续操作... conn.close()注意autocommitFalse是一个极其重要的设置。它意味着你执行INSERT、UPDATE、DELETE后数据并不会立刻写入磁盘而是先放在一个“事务缓存区”里。只有你显式调用conn.commit()数据才真正落库。这给了你回滚conn.rollback()的机会是数据安全的最后防线。原始教程里提到commit()但没强调这个开关的重要性。3.2 创建数据库与表CREATE DATABASE和CREATE TABLE的隐藏细节3.2.1 创建数据库IF NOT EXISTS是你的朋友但CHARACTER SET是你的命脉原始教程的建库语句是CREATE DATABASE book_ratings;这在本地测试没问题但在团队协作或 CI/CD 流水线里会因为“数据库已存在”而报错中断。更专业的写法是CREATE DATABASE IF NOT EXISTS book_ratings CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;IF NOT EXISTS如果库不存在才创建存在则静默跳过保证脚本幂等性。CHARACTER SET utf8mb4指定库的默认字符集。这决定了未来在这个库里创建的所有表如果没有显式指定字符集就继承这个。COLLATE utf8mb4_unicode_ci指定排序规则。“_ci” 表示 case-insensitive不区分大小写unicode_ci是最通用、对中文支持最好的排序规则。3.2.2 创建表字段类型、约束、索引一个都不能少原始教程的books表结构是CREATE TABLE books( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100), author VARCHAR(100), genre VARCHAR(100), release_year YEAR(4) );这个结构在教学上很清晰但在工程实践中有四个致命缺陷VARCHAR(100)太小一本书的标题动辄 200 字《百年孤独》的副标题就很长。VARCHAR(255)是更安全的起点TEXT类型则适合超长内容如书评。缺少时间戳字段你永远不知道这条数据是什么时候插入的什么时候更新的。这对审计、排查、ETL 增量同步至关重要。YEAR(4)已废弃MySQL 5.7 中YEAR类型已被标记为废弃官方推荐用SMALLINT或DATE。DATE更通用可以精确到年月日。没有索引WHERE author James Altucher这样的查询如果没有在author字段上建索引MySQL 就得全表扫描十万条数据时查询可能从 0.01 秒变成 2 秒。一个生产就绪的books表结构应该是CREATE TABLE IF NOT EXISTS books ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL COMMENT 书名, author VARCHAR(255) NOT NULL COMMENT 作者, genre VARCHAR(100) DEFAULT unknown COMMENT 分类, release_date DATE COMMENT 出版日期, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, is_deleted TINYINT(1) DEFAULT 0 COMMENT 软删除标志0未删除1已删除, INDEX idx_author (author), -- 为 author 字段创建普通索引 INDEX idx_genre (genre), -- 为 genre 字段创建普通索引 INDEX idx_created_at (created_at) -- 为时间字段创建索引方便按时间范围查询 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT图书信息表;BIGINT UNSIGNED比INT支持更大的 ID且UNSIGNED表示只存正数容量翻倍。COMMENT给每个字段加注释SHOW CREATE TABLE books;时能清晰看到是团队协作的必备习惯。is_deleted实现“软删除”而不是物理DELETE保证数据可追溯。ENGINEInnoDB明确指定存储引擎InnoDB支持事务、行锁、外键是现代应用的绝对首选。3.3 数据增删改查CRUD不只是语法更是数据安全的实践3.3.1 插入数据executemany()的效率真相与ON DUPLICATE KEY UPDATE原始教程展示了execute()插单条和executemany()插多条。但有一个关键事实它没说executemany()并不是“一次发多条 SQL”而是 Python 驱动在客户端把多条 SQL 拼成一条INSERT ... VALUES (), (), ()的批量语句再发给 MySQL。这才是它快的根本原因。然而真正的工程挑战在于如何处理重复数据比如你有一批新书数据要导入但其中几本已经在库里了根据title和author判断你是想跳过、覆盖还是报错MySQL 提供了完美的解决方案INSERT ... ON DUPLICATE KEY UPDATE。前提是你得先给title和author组合创建一个唯一索引ALTER TABLE books ADD UNIQUE INDEX uk_title_author (title, author);然后你的插入语句就可以这样写insert_sql INSERT INTO books (title, author, genre, release_date) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE genre VALUES(genre), release_date VALUES(release_date), updated_at CURRENT_TIMESTAMP; data_to_insert [ (The Pragmatic Programmer, Andrew Hunt, programming, 1999-10-20), (Clean Code, Robert C. Martin, programming, 2008-08-01), # 如果 Clean Code 已存在则只更新 genre 和 release_date ] cursor.executemany(insert_sql, data_to_insert) conn.commit()VALUES(genre)表示“使用本次 INSERT 语句中提供的genre值”这比写死genre programming要灵活得多。3.3.2 查询数据fetchone(),fetchall(),fetchmany()的内存陷阱原始教程只用了fetchall()这在数据量小时没问题但一旦你的表有百万行fetchall()会把所有结果一次性加载到 Python 内存里轻则 OOM内存溢出重则让整个服务器卡死。正确的做法是流式读取# 方案1逐行处理内存占用最低 cursor.execute(SELECT id, title, author FROM books WHERE is_deleted 0) while True: row cursor.fetchone() if row is None: break # 处理单行数据比如写入 CSV、发送邮件、调用 API print(fProcessing book: {row[1]} by {row[2]}) # 方案2分批处理平衡内存与效率 cursor.execute(SELECT * FROM books WHERE is_deleted 0) batch_size 1000 while True: rows cursor.fetchmany(batch_size) if not rows: break # 批量处理 rows 列表 process_batch(rows)fetchmany(1000)每次只取 1000 行处理完再取下一批内存占用恒定是处理大数据集的黄金法则。3.3.3 更新与删除WHERE子句是双刃剑永远先SELECT再UPDATE这是所有 DBA数据库管理员都会反复强调的铁律在执行任何UPDATE或DELETE之前必须先用一个SELECT语句确认你要操作的数据 exactly 是哪些。因为一旦执行就没有后悔药。原始教程的更新语句是UPDATE books SET author Big J WHERE author James Altucher这在演示时很酷但在真实世界里它会把所有叫 “James Altucher” 的作者不分青红皂白全部改成 “Big J”。如果库里有 100 本书其中 99 本是 James Altucher 的1 本是同名的其他作者那你就误伤了。安全的操作流程是三步预览SELECT id, title, author FROM books WHERE author James Altucher;确认检查返回的结果是否真的是你想改的那些书。执行UPDATE books SET author Big J WHERE id IN (1, 2, 3);用具体的id列表而不是模糊的author对于删除更是如此。DELETE FROM books WHERE release_year 2000看似无害但如果release_year字段有NULL值NULL 2000的结果是UNKNOWN这条记录就不会被删但你可能以为它被删了。所以更安全的写法是-- 先看要删哪些 SELECT COUNT(*) FROM books WHERE release_year IS NOT NULL AND release_year 2000; -- 再删 DELETE FROM books WHERE release_year IS NOT NULL AND release_year 2000;4. 实战项目构建一个完整的“图书评分系统”脚本4.1 项目目标与结构设计现在让我们把前面所有的知识点组装成一个真正可用的脚本。目标是一个名为book_manager.py的 Python 脚本它能完成以下五件事连接到本地 MySQL。自动创建book_ratings数据库如果不存在。自动创建books和ratings两张表ratings表用于存储用户对书的评分。提供一个命令行菜单让用户可以添加新书、给书打分、查看某本书的所有评分、查看评分最高的前 5 本书。所有操作都有完善的错误处理和用户友好的提示。项目结构如下book_project/ ├── .env # 数据库配置 ├── .gitignore ├── requirements.txt ├── book_manager.py # 主程序 └── db_utils.py # 数据库连接和操作的工具函数4.2 核心代码实现db_utils.py—— 你的数据库工具箱这个文件封装了所有与数据库交互的底层逻辑让主程序book_manager.py只关心业务不关心连接细节。# db_utils.py import mysql.connector from mysql.connector import Error from dotenv import load_dotenv import os # 加载环境变量 load_dotenv() def get_db_config(): 从 .env 文件读取数据库配置 return { host: os.getenv(DB_HOST, 127.0.0.1), port: int(os.getenv(DB_PORT, 3306)), user: os.getenv(DB_USER, root), password: os.getenv(DB_PASSWORD, ), charset: utf8mb4, autocommit: False } def create_database_if_not_exists(db_name): 创建数据库如果不存在 config get_db_config() # 先连接到 MySQL 服务器不指定数据库 config.pop(database, None) try: conn mysql.connector.connect(**config) cursor conn.cursor() cursor.execute(fCREATE DATABASE IF NOT EXISTS {db_name} fCHARACTER SET utf8mb4 fCOLLATE utf8mb4_unicode_ci) print(f✅ 数据库 {db_name} 创建/确认成功) cursor.close() conn.close() return True except Error as e: print(f❌ 创建数据库失败: {e}) return False def create_tables(): 创建 books 和 ratings 表 config get_db_config() config[database] os.getenv(DB_NAME, book_ratings) try: conn mysql.connector.connect(**config) cursor conn.cursor() # 创建 books 表 create_books_sql CREATE TABLE IF NOT EXISTS books ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL, genre VARCHAR(100) DEFAULT unknown, release_date DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT(1) DEFAULT 0, INDEX idx_author (author), INDEX idx_genre (genre) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; # 创建 ratings 表 create_ratings_sql CREATE TABLE IF NOT EXISTS ratings ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, book_id BIGINT UNSIGNED NOT NULL COMMENT 关联 books 表的 id, user_name VARCHAR(100) NOT NULL COMMENT 打分用户昵称, score TINYINT NOT NULL COMMENT 评分1-5 分, comment TEXT COMMENT 评论, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; cursor.execute(create_books_sql) cursor.execute(create_ratings_sql) conn.commit() print(✅ 数据表创建/确认成功) cursor.close() conn.close() return True except Error as e: print(f❌ 创建数据表失败: {e}) return False def get_connection(): 获取一个数据库连接对象 config get_db_config() config[database] os.getenv(DB_NAME, book_ratings) try: conn mysql.connector.connect(**config) return conn except Error as e: print(f❌ 获取数据库连接失败: {e}) return None def close_connection(conn): 安全关闭连接 if conn and conn.is_connected(): conn.close() print(✅ 数据库连接已关闭)4.3 主程序book_manager.py命令行交互的艺术这个文件负责与用户对话调用db_utils.py里的函数来完成具体任务。# book_manager.py from db_utils import create_database_if_not_exists, create_tables, get_connection, close_connection import sys def main_menu(): 打印主菜单 print(\n *50) print( 图书评分管理系统 v1.0) print(*50) print(1. 添加新书) print(2. 给书打分) print(3. 查看某本书的所有评分) print(4. 查看评分最高的前 5 本书) print(5. 退出) print(-*50) def add_book(): 添加新书 conn get_connection() if not conn: return try: cursor conn.cursor() print(\n--- 添加新书 ---) title input(请输入书名: ).strip() author input(请输入作者: ).strip() genre input(请输入分类 (可选默认 unknown): ).strip() or unknown release_date input(请输入出版日期 (格式: YYYY-MM-DD可选): ).strip() if not title or not author: print(❌ 书名和作者不能为空) return # 插入书籍 insert_sql INSERT INTO books (title, author, genre, release_date) VALUES (%s, %s, %s, %s) cursor.execute(insert_sql, (title, author, genre, release_date or None)) conn.commit() # 获取刚插入的书的 ID book_id cursor.lastrowid print(f✅ 新书 {title} 添加成功ID: {book_id}) except Exception as e: print(f❌ 添加新书失败: {e}) conn.rollback() finally: cursor.close() close_connection(conn) def rate_book(): 给书打分 conn get_connection() if not conn: return try: cursor conn.cursor() print(\n--- 给书打分 ---) # 先列出所有书方便用户选择 cursor.execute(SELECT id, title, author FROM books WHERE is_deleted 0) books cursor.fetchall() if not books: print(⚠️ 当前没有可评分的图书请先添加。) return print(请选择要评分的图书:) for i, book in enumerate(books, 1): print(f{i}. [{book[0]}] {book[1]} - {book[2]}) try: choice int(input(请输入序号: )) - 1 if choice 0 or choice len(books): print(❌ 无效选择) return selected_book_id books[choice][0] except ValueError: print(❌ 请输入一个有效的数字) return # 获取用户信息 user_name input(请输入您的昵称: ).strip() if not user_name: print(❌ 昵称不能为空) return try: score int(input(请输入评分 (1-5): )) if score 1 or score 5: print(❌ 评分必须在 1-5 之间) return except ValueError: print(❌ 评分必须是一个数字) return comment input(请输入评论 (可选): ).strip() # 插入评分 insert_rating_sql INSERT INTO ratings (book_id, user_name, score, comment) VALUES (%s, %s, %s, %s) cursor.execute(insert_rating_sql, (selected_book_id, user_name, score, comment or None)) conn.commit() print(✅ 评分成功) except Exception as e: print(f❌ 打分失败: {e}) conn.rollback() finally: cursor.close() close_connection(conn) def view_book_ratings(): 查看某本书的所有评分 conn get_connection() if not conn: return try: cursor conn.cursor() print(\n--- 查看图书评分 ---) # 列出所有书 cursor.execute(SELECT id, title, author FROM books WHERE is_deleted 0) books cursor.fetchall() if not books: print(⚠️ 当前没有图书。) return print(请选择要查看评分的图书:) for i, book in enumerate(books, 1): print(f{i}. [{book[0]}] {book[1]} - {book[2]}) try: choice int(input(请输入序号: )) - 1 if choice 0 or choice len(books): print(❌ 无效选择) return selected_book_id books[choice][0] except ValueError: print(❌ 请输入一个有效的数字) return # 查询该书的所有评分 cursor.execute( SELECT r.user_name, r.score, r.comment, r