基于Python+SQLite的电影数据库设计与实现:从建表到数据分析
简介基于Python的数据库课程设计完整解决方案围绕电影数据Web查询系统展开实现按用户ID检索观影记录与评分、关键词查询电影名、热门风格Top20等功能并配有前端录入界面和结果展示页面适合计算机相关专业学生完成大作业、课设或毕业设计参考。压缩包共77个文件、约4.96MB涵盖Python后端脚本、HTML/CSS/JS前端页面、Markdown分析文档、E-R图与数据图表等资源目录结构把源码、部署指南、操作报告和数据集说明分开便于按需查阅。除核心功能外还包含MongoDB安装与Docker排错、云服务器数据导入、创建索引优化、复制集配置等实战笔记帮助从数据建模、数据导入到线上部署的完整链路入手理解整个项目并能在答辩中讲清技术细节。已有359人浏览学习代码经全程测试运行成功适合需要快速上手数据库Web项目或借鉴完整作业结构的学习者。1. 电影数据库大作业为什么这个选题值得认真做把「基于 Python 的数据库大作业」和「电影数据」放到一起很多同学的第一反应是这不就是做个带界面的增删改查吗其实这个标题背后的考察重点不是界面多华丽而是你能不能把「数据模型设计 → 数据导入 → SQL 查询 → 数据分析 → 文档输出」这一整条链路打通。电影数据集天然自带多对多关系一部电影对应多个演员、一个导演对应多部电影、数值字段评分、时长、年份和文本字段类型、语言非常适合用来展示关系型数据库的核心能力。我见过太多人把作业做成「打开一个 CSV用 Pandas 读一遍然后打印几张图」数据库只起了个存档作用这恰恰是最可惜的做法。这篇文章按我实际做过的一套方案来拆用 Python SQLite 搭一个电影数据库写清建表、导数、查询、分析和避坑点让你从「能跑」走到「能讲、能答、能交差」。2. 架构与选型为什么电影库用 SQLite、设计几张表最合适2.1 SQLite vs MySQL课程设计里怎么选数据库大作业最常见的两个选择是 SQLite 和 MySQL。我的建议很直接如果项目要求里没写「必须用 MySQL 客户端连接」那就用 SQLite。先别急着觉得它「不像数据库」SQLite 是完整的关系型数据库引擎支持标准 SQL、事务、外键约束和视图对课程设计来说功能完全够用。用 SQLite 的最大好处是部署成本为零。Python 自带sqlite3模块不需要在答辩机器上装 MySQL 服务、改 root 密码、处理远程连接权限。另一个好处是数据文件就是一个.db文件交作业时连库带代码一起打包老师拿到就能直接跑不会再出现「我电脑上能跑到你机器上连不上数据库」的尴尬场景。如果项目是组队做或者场景明确是「多用户并发写入」那再考虑 MySQL按pymysql的套路写一套连接封装也不难。选型还要想清楚一件事数据库在这份作业里承担的职责边界。很多同学用 Pandas 做完全部数据处理最后把 DataFrame 塞进数据库数据库沦为「存储容器」。正确的做法是让数据库承担「结构化存储 关系查询 聚合计算」Pandas 只负责结果的可视化和报告绘图。这个分界线会在第 4 章具体体现。2.2 核心表设计从电影事实表到多对多关联电影库表结构设计我一般会先想清楚「业务问题」再动手建表。课程设计里最常被问到的问题是某导演的所有电影、某演员参演过的电影、每年电影的评分趋势、某个类型的平均时长。围绕这些问题一张主事实表和几个维度表就够了。我推荐的表结构如下movie电影表电影 ID、标题、上映年份、时长、评分、评分人数、类型、语言、国家、导演 IDdirector导演表导演 ID、姓名、出生年份actor演员表演员 ID、姓名、性别movie_actor电影演员关联表电影 ID、演员 ID这样设计的原因是导演和电影是一对多关系导演 ID 可以直接放在 movie 表做外键演员和电影是多对多关系必须用关联表拆开。有人会把类型也拆成多对多如果数据集里一部电影的类型是「动作,冒险,科幻」这种逗号分隔字符串拆类型会显著增加导入复杂度。课程设计阶段我会把类型保留为 movie 表的文本字段查询时用LIKE匹配分析时的精度足够代码量却少一大截。2.3 一份容易维护的项目文件结构代码组织方式会影响维护效率也会影响阅读你代码的人的耐心。我采用按职责分模块的写法movie_db/ ├── data/ │ └── movies.csv # 原始数据集 ├── db/ │ ├── init_db.py # 建库建表 │ ├── import_data.py # CSV 导入 │ └── movie.db # 生成的数据库文件 ├── analysis/ │ ├── queries.py # 查询模块 │ └── report.py # 分析统计与输出 ├── main.py # 总控入口 └── README.md # 文档说明这个结构的好处是把「建库」「导数据」「查询分析」三个阶段分开单独调试某一个环节时不会影响其他模块。后面每一章讲的代码都按这个文件划分去放。main.py只负责调函数和打印菜单不要把所有逻辑堆在一个文件里否则文档说明部分会非常难写。3. 建表与导数据从空数据库到可查询的电影库3.1 建表 SQL字段类型、主键和外键约束一次写对这里是建库的初始化模块我把表结构直接写在 Python 里用executescript一次性执行多段 SQL。# db/init_db.py import sqlite3 import os DB_PATH os.path.join(os.path.dirname(__file__), movie.db) SCHEMA_SQL PRAGMA foreign_keys ON; CREATE TABLE IF NOT EXISTS director ( director_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, birth_year INTEGER ); CREATE TABLE IF NOT EXISTS actor ( actor_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, gender TEXT ); CREATE TABLE IF NOT EXISTS movie ( movie_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, release_year INTEGER, duration INTEGER, rating REAL, votes INTEGER, genre TEXT, language TEXT, country TEXT, director_id INTEGER, FOREIGN KEY (director_id) REFERENCES director(director_id) ); CREATE TABLE IF NOT EXISTS movie_actor ( movie_id INTEGER NOT NULL, actor_id INTEGER NOT NULL, PRIMARY KEY (movie_id, actor_id), FOREIGN KEY (movie_id) REFERENCES movie(movie_id), FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ); def init_db(): if os.path.exists(DB_PATH): os.remove(DB_PATH) # 重建前先删旧库避免脏数据残留 conn sqlite3.connect(DB_PATH) conn.executescript(SCHEMA_SQL) conn.commit() conn.close() print(f[OK] 数据库已初始化: {DB_PATH}) if __name__ __main__: init_db()逻辑说明AUTOINCREMENT让每条记录获得自增主键避免手动指定 ID 导致冲突TEXT NOT NULL UNIQUE约束在导演和演员表上保证同一名字不会被重复插入movie 表的director_id外键指向导演表这是关系型数据库「引用完整性」的体现。参数说明PRAGMA foreign_keys ON不能省。SQLite 默认不检查外键约束如果不开启插入一个指向不存在导演的电影记录会静默成功后期查询时就会出现「孤儿数据」。IF NOT EXISTS让脚本可重复执行但配合os.remove重建逻辑保证每次初始化都是干净环境。3.2 CSV 导入编码、去重、外键匹配三步走大部分公开电影数据集是 CSV 格式字段乱、编码杂、有空值。导入脚本要处理的就是把这些脏数据变成符合表结构的记录。# db/import_data.py import csv import sqlite3 from db.init_db import DB_PATH def clean_value(value): 清洗字段去空格、空值转 None if value is None: return None value value.strip() if value or value.lower() nan: return None return value def import_movies(csv_path): conn sqlite3.connect(DB_PATH) cur conn.cursor() # 缓冲字典记住已插入的导演和演员避免重复 director_cache {} actor_cache {} with open(csv_path, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: title clean_value(row.get(title)) if not title: continue # 导演不存在则插入存在则取已有 ID director_name clean_value(row.get(director)) director_id None if director_name: if director_name not in director_cache: cur.execute( INSERT INTO director(name) VALUES (?), (director_name,) ) director_cache[director_name] cur.lastrowid director_id director_cache[director_name] # 电影主体记录 cur.execute( INSERT INTO movie(title, release_year, duration, rating, votes, genre, language, country, director_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( title, clean_value(row.get(year)), clean_value(row.get(duration)), clean_value(row.get(rating)), clean_value(row.get(votes)), clean_value(row.get(genre)), clean_value(row.get(language)), clean_value(row.get(country)), director_id, ) ) movie_id cur.lastrowid # 演员逗号分隔逐个建立关联 actor_str clean_value(row.get(actors)) if actor_str: for actor_name in actor_str.split(,): actor_name actor_name.strip() if not actor_name: continue if actor_name not in actor_cache: cur.execute( INSERT INTO actor(name) VALUES (?), (actor_name,) ) actor_cache[actor_name] cur.lastrowid cur.execute( INSERT OR IGNORE INTO movie_actor(movie_id, actor_id) VALUES (?, ?), (movie_id, actor_cache[actor_name]) ) conn.commit() conn.close() print(f[OK] 数据导入完成: 电影 {cur.rowcount if cur in dir() else ?} 条)逻辑说明clean_value函数统一处理空值和空白字符防止「空字符串」和NULL并存导致查询结果异常。director_cache和actor_cache是内存 buffered 表名和 ID 的字典避免每个导演都执行一次SELECT插入效率更高也避免同名导演被重复插入。INSERT OR IGNORE在 movie_actor 表上使用配合联合主键即使数据里有重复的「同一部电影同一演员」也不会插入两次。参数说明encodingutf-8-sig是为了兼容带 BOM 头的 CSV。很多人用utf-8读取会报错或第一列字段名变成\ufefftitle用-sig可以自动去掉 BOM。DictReader按表头取字段所以 CSV 第一行必须是列名列名要和代码里的row.get(title)对应上。如果数据集列名不同要么改 CSV 表头要么把映射表写出来统一处理——我建议先看一眼数据再定映射。3.3 数据校验导入完必须做的三个检查导入成功不等于数据正确。我每次导完数据都会跑三条 SQL确认没有明显问题。# 临时校验脚本也可以放在 import_data.py 末尾 import sqlite3 conn sqlite3.connect(db/movie.db) cur conn.cursor() # 1. 记录总数 cur.execute(SELECT COUNT(*) FROM movie) print(电影总数:, cur.fetchone()[0]) # 2. 外键孤儿检查 cur.execute( SELECT COUNT(*) FROM movie m LEFT JOIN director d ON m.director_id d.director_id WHERE m.director_id IS NOT NULL AND d.director_id IS NULL ) print(孤儿电影记录:, cur.fetchone()[0]) # 3. 重复标题检查 cur.execute( SELECT title, COUNT(*) FROM movie GROUP BY title HAVING COUNT(*) 1 LIMIT 10 ) print(重复标题样例:, cur.fetchall())逻辑说明总数确认导入没有丢行孤儿检查验证外键约束实际生效重复标题不是一定错误——同名电影可能年份不同但如果同一年的同标题也重复就是数据集本身有问题需要在报告里说明或去重。参数说明第三条 SQL 的HAVING COUNT(*) 1是分组后过滤条件WHERE管的是分组前这是初学者最容易写混的地方。如果用WHERE COUNT(*) 1会直接报 SQL 语法错误。4. 查询与分析模块把数据库变成报告里的图表素材4.1 多表 JOIN 查询统计每个导演的平均评分数据分析模块的价值在于用 SQL 完成聚合计算而不是把数据全部拉出来再用 Python 算。下面这段代码统计每个导演的电影数量和平均评分按评分降序排列。# analysis/queries.py import sqlite3 def query_director_stats(): conn sqlite3.connect(db/movie.db) cursor conn.cursor() cursor.execute( SELECT d.name AS director_name, COUNT(m.movie_id) AS movie_count, ROUND(AVG(m.rating), 2) AS avg_rating, ROUND(AVG(m.duration), 1) AS avg_duration FROM director d LEFT JOIN movie m ON d.director_id m.director_id GROUP BY d.director_id HAVING COUNT(m.movie_id) 3 ORDER BY avg_rating DESC LIMIT 20 ) rows cursor.fetchall() conn.close() return rows if __name__ __main__: for row in query_director_stats(): print(row)逻辑说明LEFT JOIN保留了没有电影记录的导演但后面HAVING COUNT(m.movie_id) 3会把作品数不足 3 部的导演过滤掉——这个门槛是为了排除「只拍过一部片但评分恰好很高」的偶然情况让统计结果更有说服力。GROUP BY d.director_id而不是GROUP BY d.name因为姓名可能有重复用 ID 分组更安全。参数说明HAVING和GROUP BY的配合在本查询中很关键如果换成WHERE COUNT(m.movie_id) 3放在JOIN之后SQLite 会直接报错聚合函数不能出现在WHERE子句里。ROUND(x, 2)控制输出小数位数避免 3.66666666 这种数字污染报告表格。4.2 分布统计评分区间、年份趋势和中位数修正除了导演聚合还需要几个「能撑起报告图表」的统计口径。年份趋势是必备的评分分桶也不难。# analysis/report.py def year_trend(): conn sqlite3.connect(db/movie.db) cursor conn.cursor() cursor.execute( SELECT release_year, COUNT(*) AS cnt, ROUND(AVG(rating), 2) AS avg_rating, ROUND(AVG(duration), 1) AS avg_duration FROM movie WHERE release_year IS NOT NULL GROUP BY release_year ORDER BY release_year ) return cursor.fetchall() def rating_distribution(): conn sqlite3.connect(db/movie.db) cursor conn.cursor() # 评分分桶0-4.9, 5-5.9, 6-6.9, 7-7.9, 8-10 cursor.execute( SELECT CASE WHEN rating 5 THEN 0-4.9 WHEN rating 6 THEN 5-5.9 WHEN rating 7 THEN 6-6.9 WHEN rating 8 THEN 7-7.9 ELSE 8-10 END AS rating_bucket, COUNT(*) AS cnt FROM movie WHERE rating IS NOT NULL GROUP BY rating_bucket ORDER BY rating_bucket ) return cursor.fetchall()逻辑说明CASE WHEN是 SQL 里的条件表达式作用是把连续数值映射为离散区间适合评分分布这种「分组计数」的场景。写区间时注意边界一致性第一条WHEN rating 5第二条WHEN rating 6隐含的是 5.0 到 5.9 落在第二组不会出现漏值和重叠。参数说明ORDER BY rating_bucket按字符串排序对0-4.9、5-5.9这类区间而言字典序恰好也是数值序但如果有10-...这种区间就会排错。稳妥做法是加一个用于排序的分桶序号字段或者在 Python 侧维序。数据量不大时我更推荐在 Python 里用pandas.cut分桶逻辑更直白。4.3 用 Python 补充 SQL 算不动的指标SQL 能做绝大多数聚合但中位数这种指标在 SQLite 里没有内置函数。常见做法是先把排序后的数据拉到 Python再算中位数。这不是「退回 Pandas 处理一切」而是把需要专门算法的部分交给 Python主流程仍然在 SQL。import statistics def median_rating_by_year(year_limit1990): conn sqlite3.connect(db/movie.db) cursor conn.cursor() cursor.execute( SELECT release_year, rating FROM movie WHERE release_year ? AND rating IS NOT NULL ORDER BY release_year, rating , (year_limit,)) rows cursor.fetchall() # 按年份分组计算中位数 result {} for year, rating in rows: result.setdefault(year, []).append(rating) medians {year: statistics.median(ratings) for year, ratings in result.items()} return sorted(medians.items())逻辑说明参数year_limit用?占位传入避免拼字符串带来的 SQL 注入风险——虽然这是本机数据库但养成参数化习惯后切换到 MySQL 也不会翻车。Python 侧用字典收集同一年的评分列表statistics.median会自动处理偶数个样本取中间两个均值的情况。参数说明占位符?是 SQLite 的参数化写法pymysql 里对应%s。如果你后面从 SQLite 迁移到 MySQL这个细节要同步改不然会报TypeError或not all arguments converted。setdefault是 Python 字典的常用初始化技巧比先判断if year not in result更简洁。4.4 输出格式让分析结果直接能粘进报告写操作报告时最烦的就是统计数据到表格之间的复制粘贴。我习惯让分析模块直接生成 Markdown 表格或 CSV 文件。def export_markdown_table(rows, headers, filename): with open(filename, w, encodingutf-8) as f: f.write(| | .join(headers) |\n) f.write(| ---| * len(headers) \n) for row in rows: f.write(| | .join(str(cell) for cell in row) |\n) print(f[OK] 表已输出: {filename})这个函数把任意查询结果写成 Markdown 表格报告需要配图时再用 Pandas 画柱状图和折线图。画图建议用matplotlib并设置rcParams[font.sans-serif] [SimHei]否则图里的中文标签会全部变成方块。5. 避坑电影数据库大作业里的几个重灾区5.1 中文字符变成「锟斤拷」或乱码现象导入带中文的 CSV 后SELECT查出来的导演名、电影名显示为乱码。原因绝大多数是编码不匹配。数据集文件是 GBK 编码但代码用utf-8打开或者文件带 BOM字段名读出来带着\ufeff前缀。解决第一步先确认文件编码。我一般用file命令或直接在看文件时切换编码方式查看。Python 代码里用encodingutf-8-sig兼容带 BOM 的 UTF-8如果确认是 GBK改成encodinggbk或encodinggb18030。最稳妥的是先把数据集用文本工具统一转成 UTF-8 再导入不要在每个读取点都试编码。5.2 主键重复导致导入中断现象导到一半报错UNIQUE constraint failed程序崩溃。原因导演或演员表里已经有同名的记录第二次插入时撞了唯一约束。常见在演员表因为同一演员可能出现在几十部电影里。解决用第 3 章的缓存字典在插入前判断是否已存在已存在就只拿 ID不再重复插入。另一种做法是把INSERT改成INSERT OR IGNORE但只适合不需要返回 ID 的场景。关联表里多用INSERT OR IGNORE维度表里用「先查后插」更稳妥。5.3 外键约束失效孤儿数据满天飞现象删除了某个导演后电影表里还留着指向该导演 ID 的记录查JOIN时这些电影消失。原因忘记启用PRAGMA foreign_keys ONSQLite 默认不检查外键任何非法引用都会被接受。解决在每次新建连接的connect()之后立刻执行PRAGMA foreign_keys ON。注意这个 pragma 是连接级的不是数据库级的只设一次库但每次都新建连接的话仍然会失效。最规范的做法是写一个get_conn()函数统一在这个函数里启用外键。5.4 评分字段混入文本或空字符串现象AVG(rating)结果出奇地大或报TypeError因为类型不匹配。原因CSV 里某些行的评分字段为空或写着 N/A导入时没有清洗直接入库SQLite 是动态类型会把文本存成 TEXT 类型聚合函数计算时报错或跳过。解决严格按 3.2 节的clean_value把所有空字符串和 N/A 转成None即 SQL 的 NULL。另外可以在建表时给rating REAL加CHECK(rating 0 AND rating 10)约束数据库层面挡住不合法数据。5.5 写完代码才发现文档和代码对不上现象操作报告里写的表结构、数据量、查询结果和实际跑出来的不一样。原因文档是最后赶工出来的中间改过表结构或导入逻辑却忘了同步文档。解决我的习惯是每完成一个阶段就更新一次 README 里的「当前状态」段落并记录表结构 DDL、最终数据量和三条核心查询 SQL。报告里提到的每一项数据都必须是用main.py从头执行能复现的。答辩前跑一次全流程如果文档和数据对不上优先改文档而不是改代码——多数情况下是文档描述过时了。6. 进阶技巧加一个查询菜单让演示过程有「操作感」很多答辩现场老师是不会打开你的代码一行一行看的TA 的体验完全来自「双击运行、看到控制台输出、按几个键看到不同结果」这个过程。所以给项目加一个带菜单的main.py总控入口收益极高。# main.py import sys from db.init_db import init_db from db.import_data import import_movies from analysis.queries import query_director_stats from analysis.report import year_trend, rating_distribution MENU 电影数据库系统操作菜单 1. 初始化数据库 2. 导入 CSV 数据 3. 查询导演统计 4. 年度趋势分析 5. 评分分布统计 6. 退出 请选择: def main(): while True: choice input(MENU).strip() if choice 1: init_db() elif choice 2: import_movies(data/movies.csv) elif choice 3: for row in query_director_stats(): print(row) elif choice 4: for row in year_trend(): print(row) elif choice 5: for row in rating_distribution(): print(row) elif choice 6: sys.exit(0) else: print(无效选择请重新输入) if __name__ __main__: main()这个菜单的价值不在代码量而在演示节奏。你可以先选 1 初始化库再选 2 导入数据让老师看到从零到有数据的过程然后依次选 3、4、5展示不同类型的查询输出。操作报告里就写「本系统提供了五个功能模块通过主菜单逐层调用」比任何架构图都实在。关于操作报告本身我的建议是包含四块内容一是项目背景和数据集来源说明二是表结构设计用字段表格加外键关系描述三是核心查询和分析结果贴上统计数据与图表四是操作说明写清楚环境依赖Python 版本、第三方库pip install列表和运行步骤。这里尤其要写清楚第三方依赖——如果代码里import pandas但 README 没提pip install pandas换一台机器必然跑不起来。我也踩过数据一致性以外的坑有一次为了展示效果临时改了查询条件让评分分布更好看结果答辩时切换了另一个数据集输出全部对不上。后来我的习惯永远是报告里的截图和输出必须是当前代码在当前数据集上跑的真实结果宁可数字不好看也不要不可复现。这一点对数据库课程设计比对其他课设更关键因为数据库的所有输出都是可验证的事实一旦被发现对不上整个文档的可信度都会归零。希望这篇拆解能帮你把每一步走稳少走弯路。本文还有配套的精品资源点击获取