☰
Python电影数据库大作业:从建表到分析报告全流程
2026/9/26 8:25:41 网站建设 项目流程

简介:这是一份面向计算机相关专业在校学生与教师的数据库课程设计完整资料,围绕电影数据查询系统展开,适合作为课程大作业、毕设立项或Python数据库入门练手项目。资源包共77个文件,约4.96MB,包含10个Python源码文件、4个HTML页面、5个JavaScript脚本及配套样式文件,另有15份Markdown文档、17张PNG与13张JPG截图,覆盖MongoDB安装配置、复制集搭建、数据导入云服务器、索引优化、数据分析与前端接口说明等环节。项目实现按用户ID检索观影记录并按时间倒序展示前三标签及关联度、关键词模糊查询电影、风格热门Top20等功能,界面含输入框与任务提交按钮,结果分页展示。已有359人学习,文档中附有E-R图、数据表结构图与排错记录,可帮助读者理解从数据导入到Web查询的完整链路,并参考其报告与进度说明完成答辩准备。

1. 电影数据库大作业:从建表到分析报告,一套能直接交的 Python 方案

每年学期末,总有一批人对着「数据库大作业」四个字发愁。选题选了电影数据库,听起来简单——不就是几部电影、几个演员、几条评分吗?真动手才发现:ER 图怎么画才不被老师挑刺、评分和评价到底要不要拆表、Python 连 SQLite 还是 MySQL、数据分析部分用什么图才显得有工作量、操作报告又该写哪些内容。这套「基于 Python 的电影数据库数据系统」本质上是一个完整闭环:用 Python 建库建表、灌入电影数据集、做增删改查、跑数据分析、最后输出可视化图表和操作报告。它适合三类人:数据库课程期末要交大作业的学生、想用一个真实小项目练手 Python + SQL 的入门者、以及需要一份「能跑起来、能讲清楚」的数据分析案例的从业者。下面我按实际做一遍的顺序,把建库、灌数据、查询、分析、报告这条链路拆开讲,参数怎么设、坑在哪,都写清楚。

2. 建库建表:电影数据库的 ER 图怎么画才经得起追问

2.1 先定实体边界,别一上来就画 ER 图

很多人拿到「电影数据库」这个题目,第一反应是打开画图工具开始画 ER 图。这是典型的翻车起点。ER 图是结果,不是起点。你得先想清楚:这个系统里到底有哪些「东西」需要被独立记录。

电影数据库最常见的实体有五个:电影(movie)、导演(director)、演员(actor)、用户(user)、评分(rating)。评价(review)要不要单独拆,取决于你的作业要求里有没有「文字评论」这一项。如果只有打分,评分表就够了;如果要写影评文字,就得把 review 从 rating 里拆出来,否则一张表里既有数字又有长文本,范式上不好看,老师一问就露馅。

我一般会先列一张实体-属性草表,确认每个实体的主键和关键属性,再动手画图。这一步花十分钟,能省掉后面改表结构的两小时。

实体主键关键属性说明
moviemovie_idtitle, year, genre, duration电影基本信息
directordirector_idname, country导演
actoractor_idname, gender演员
useruser_idusername, register_date系统用户
ratingrating_iduser_id, movie_id, score, rate_time评分记录
reviewreview_iduser_id, movie_id, content, review_time文字评价(可选)

这张表不是给你交的,是给你自己理思路的。确认无误后再画 ER 图,实体之间的关系就一目了然:电影和导演是多对一(一个导演可以拍多部电影),电影和演员是多对多(需要中间表 movie_actor),用户和电影通过 rating 形成多对多。

2.2 用 Python + SQLite 建库建表的最小可跑代码

选 SQLite 而不是 MySQL,理由很直接:大作业场景下,SQLite 零配置、单文件、Python 标准库自带 sqlite3 模块,不需要额外装数据库服务。老师要看的是你的表结构设计和 SQL 能力,不是你的运维水平。当然,如果课程明确要求 MySQL,把连接部分换掉即可,建表语句基本通用。

import sqlite3 # 连接数据库,如果文件不存在会自动创建 conn = sqlite3.connect('movie_db.sqlite') cursor = conn.cursor() # 开启外键约束,SQLite 默认关闭,不开启外键形同虚设 cursor.execute("PRAGMA foreign_keys = ON") # 电影表 cursor.execute(''' CREATE TABLE IF NOT EXISTS movie ( movie_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, release_year INTEGER, genre TEXT, duration INTEGER, director_id INTEGER, FOREIGN KEY (director_id) REFERENCES director(director_id) ) ''') # 导演表 cursor.execute(''' CREATE TABLE IF NOT EXISTS director ( director_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, country TEXT ) ''') # 演员表 cursor.execute(''' CREATE TABLE IF NOT EXISTS actor ( actor_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT ) ''') # 电影-演员中间表(多对多) cursor.execute(''' CREATE TABLE IF NOT EXISTS movie_actor ( movie_id INTEGER, actor_id INTEGER, role_name TEXT, PRIMARY KEY (movie_id, actor_id), FOREIGN KEY (movie_id) REFERENCES movie(movie_id), FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ) ''') # 用户表 cursor.execute(''' CREATE TABLE IF NOT EXISTS user ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, register_date TEXT ) ''') # 评分表 cursor.execute(''' CREATE TABLE IF NOT EXISTS rating ( rating_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, score REAL CHECK(score >= 0 AND score <= 10), rate_time TEXT, FOREIGN KEY (user_id) REFERENCES user(user_id), FOREIGN KEY (movie_id) REFERENCES movie(movie_id) ) ''') conn.commit() conn.close() print("数据库和表创建完成")

这段代码有几个关键点值得说。第一,PRAGMA foreign_keys = ON必须显式开启,SQLite 默认不强制外键,很多人建了外键但根本没生效,插入脏数据也不报错,答辩时被问到就尴尬了。第二,CHECK(score >= 0 AND score <= 10)是评分范围约束,加上它,后面插入异常分数时数据库会直接拒绝,比在 Python 层做校验更可靠。第三,中间表movie_actor用联合主键(movie_id, actor_id),防止同一部电影同一个演员被重复插入。第四,建表顺序有讲究——movie 引用了 director,所以 director 要先建,否则外键约束会报错。

2.3 数据集怎么来、怎么灌进去

数据集来源通常有三种:老师给的 CSV、自己从公开数据整理、或者用脚本生成模拟数据。不管哪种,灌数据的核心逻辑是一样的:读源数据 → 清洗 → 按依赖顺序插入。

假设你有一个movies.csv,字段是 title, year, genre, duration, director_name, actors(分号分隔)。灌数据的脚本要处理两个问题:导演和演员需要先去重插入,拿到自增 ID 后再插入电影和中间表。

import sqlite3 import csv conn = sqlite3.connect('movie_db.sqlite') cursor = conn.cursor() cursor.execute("PRAGMA foreign_keys = ON") def get_or_create_director(name, country=None): """导演不存在则插入,返回 director_id""" cursor.execute("SELECT director_id FROM director WHERE name = ?", (name,)) row = cursor.fetchone() if row: return row[0] cursor.execute("INSERT INTO director (name, country) VALUES (?, ?)", (name, country)) return cursor.lastrowid def get_or_create_actor(name, gender=None): cursor.execute("SELECT actor_id FROM actor WHERE name = ?", (name,)) row = cursor.fetchone() if row: return row[0] cursor.execute("INSERT INTO actor (name, gender) VALUES (?, ?)", (name, gender)) return cursor.lastrowid with open('movies.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: # 先处理导演 director_id = get_or_create_director(row['director_name']) # 插入电影 cursor.execute(''' INSERT INTO movie (title, release_year, genre, duration, director_id) VALUES (?, ?, ?, ?, ?) ''', (row['title'], int(row['year']), row['genre'], int(row['duration']), director_id)) movie_id = cursor.lastrowid # 处理演员列表 if row.get('actors'): for actor_name in row['actors'].split(';'): actor_name = actor_name.strip() if actor_name: actor_id = get_or_create_actor(actor_name) cursor.execute(''' INSERT OR IGNORE INTO movie_actor (movie_id, actor_id) VALUES (?, ?) ''', (movie_id, actor_id)) conn.commit() conn.close() print("数据导入完成")

get_or_create这个模式在数据导入里非常常用,核心思路是「先查后插」,避免重复数据。INSERT OR IGNORE用在中间表上,即使遇到重复的 (movie_id, actor_id) 组合也不会报错中断。注意cursor.lastrowid返回的是刚插入那行的自增主键,这个值在同一个连接里是准确的,但多线程环境下要小心。

灌完数据后一定要做个行数校验,确认每张表的记录数和源数据对得上:

cursor.execute("SELECT COUNT(*) FROM movie") print("电影数:", cursor.fetchone()[0]) cursor.execute("SELECT COUNT(*) FROM movie_actor") print("电影-演员关系数:", cursor.fetchone()[0])

如果电影数是 0,检查 CSV 编码和字段名是否匹配;如果中间表数量明显偏少,检查演员分隔符是不是分号,有些数据集用的是逗号或竖线。

3. 增删改查与数据分析:从 SQL 到可视化图表

3.1 增删改查不是目的,是验证表结构的手段

很多人的大作业里,增删改查就是几条孤立的 SQL 语句,跟后面的分析完全脱节。其实增删改查是验证你表结构设计是否合理的最好方式。比如:删除一个导演时,他名下的电影怎么办?如果外键设了ON DELETE CASCADE,电影会被一起删掉;如果设了ON DELETE SET NULL,电影的 director_id 会变成 NULL;如果什么都没设,删除会直接报错。这三种行为对应三种业务逻辑,你在报告里写清楚选了哪种、为什么,就是加分项。

# 增:插入一个新用户 cursor.execute("INSERT INTO user (username, register_date) VALUES (?, ?)", ("test_user", "2025-01-15")) # 删:删除评分低于 2 分的记录(注意先删子表再删主表) cursor.execute("DELETE FROM rating WHERE score < 2") # 改:把某部电影的时长修正 cursor.execute("UPDATE movie SET duration = ? WHERE title = ?", (148, "示例电影")) # 查:查询每部电影的平均分和评分人数 cursor.execute(''' SELECT m.title, ROUND(AVG(r.score), 2) AS avg_score, COUNT(r.rating_id) AS num_ratings FROM movie m LEFT JOIN rating r ON m.movie_id = r.movie_id GROUP BY m.movie_id HAVING num_ratings >= 1 ORDER BY avg_score DESC ''') for row in cursor.fetchall(): print(row)

这里有个细节:LEFT JOIN保证即使电影没有评分也会出现在结果里,HAVING过滤掉评分人数为 0 的记录。如果用INNER JOIN,没评分的电影直接消失,做分析时容易漏数据。

3.2 用 pandas + matplotlib 做电影数据分析

数据分析部分是大作业里最容易拉开差距的地方。同样一份数据,有人只画了个柱状图,有人能做出类型分布、评分趋势、导演产量对比、演员合作网络四五个维度。关键不在于图多,而在于每个图能回答一个具体问题。

import sqlite3 import pandas as pd import matplotlib.pyplot as plt # 设置中文字体,否则图表里的中文会变成方块 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False conn = sqlite3.connect('movie_db.sqlite') # 读取电影和评分数据 df_movie = pd.read_sql("SELECT * FROM movie", conn) df_rating = pd.read_sql("SELECT * FROM rating", conn) # 合并计算每部电影的平均分 df_avg = df_rating.groupby('movie_id')['score'].agg(['mean', 'count']).reset_index() df_avg.columns = ['movie_id', 'avg_score', 'rating_count'] df_merged = df_movie.merge(df_avg, on='movie_id', how='left') # 图1:各类型电影数量分布 genre_counts = df_movie['genre'].value_counts() genre_counts.plot(kind='bar', title='各类型电影数量分布') plt.tight_layout() plt.savefig('genre_distribution.png', dpi=150) plt.close() # 图2:评分与评分人数的散点图 plt.scatter(df_merged['rating_count'], df_merged['avg_score'], alpha=0.6) plt.xlabel('评分人数') plt.ylabel('平均分') plt.title('电影评分与评分人数关系') plt.tight_layout() plt.savefig('rating_scatter.png', dpi=150) plt.close() conn.close() print("图表已保存")

pd.read_sql直接把 SQL 查询结果转成 DataFrame,省去手动拼列表的麻烦。groupby + agg是 pandas 里做分组统计的标准写法,agg(['mean', 'count'])一次算出均值和计数。merge的how='left'保证没评分的电影也保留在结果里,avg_score会是 NaN,后续画图时自动跳过。

中文字体那两行是血泪经验。不设的话,图表标题和轴标签全是方框,交上去老师第一眼就觉得你没跑通。Windows 用SimHei,Mac 用Arial Unicode MS,Linux 服务器上如果没有中文字体,考虑用英文标签或者提前装字体。

3.3 操作报告该写什么、不该写什么

操作报告不是代码的复制粘贴。老师想看的是你的设计决策和验证过程。一份合格的操作报告至少包含:系统功能概述(一段话说清楚做了什么)、ER 图及设计说明(为什么这样拆表)、表结构定义(每张表的字段、类型、约束)、核心功能实现(增删改查各举一例,附 SQL 和运行结果)、数据分析结论(每个图说明了什么)、遇到的问题及解决方式。

不该写的:大段代码原文、安装 Python 的步骤截图、跟本系统无关的技术介绍。报告控制在 8-15 页,重点放在设计思路和结果分析上。

4. 避坑与排查:电影数据库大作业里最容易翻车的 5 个点

4.1 中文乱码:从 CSV 到图表全链路排查

现象:CSV 读进来电影名是乱码,或者图表里中文显示为方块。

原因:CSV 文件编码可能是 GBK 而不是 UTF-8;matplotlib 默认字体不支持中文。

解决:读 CSV 时先试encoding='utf-8',报错就换encoding='gbk'。图表中文问题在代码开头加plt.rcParams['font.sans-serif'] = ['SimHei']和plt.rcParams['axes.unicode_minus'] = False。如果 SimHei 不存在,用matplotlib.font_manager查一下系统里有哪些中文字体。

4.2 外键约束不生效:插入了脏数据却没人报错

现象:插入了一条 rating 记录,user_id 填了一个不存在的用户,数据库居然接受了。

原因:SQLite 默认不开启外键约束,每次连接都要手动PRAGMA foreign_keys = ON。

解决:在每次建立连接后立即执行这个 PRAGMA。注意它是连接级别的,不是数据库级别的,换个连接就得重新设。如果用的是 MySQL,InnoDB 引擎默认开启外键,但也要确认表引擎不是 MyISAM。

4.3 自增 ID 错乱:删了数据后新插入的 ID 不连续

现象:删除了 movie_id 为 3 的记录,下一条插入的电影 ID 是 4 而不是 3。

原因:AUTOINCREMENT 的行为就是单调递增,不会复用已删除的 ID。这是正常行为,不是 bug。

解决:如果作业要求 ID 连续,可以在删除后手动重置sqlite_sequence表,但一般不推荐这么做。ID 不连续不影响功能,报告里说明一下即可。

4.4 评分平均值算错:NULL 值把结果拉偏了

现象:某部电影的平均分算出来是 0,但实际上有评分记录。

原因:用了AVG(score)但 JOIN 产生了 NULL 行,或者 WHERE 条件过滤掉了有效数据。

解决:用AVG(COALESCE(score, 0))处理 NULL,或者用LEFT JOIN后在 Python 层用 pandas 的dropna()过滤。更稳妥的做法是先确认数据里有没有 NULL 的 score,再决定处理策略。

4.5 图表保存后空白:plt.show() 和 plt.savefig() 的顺序问题

现象:保存的图片是空白的,但运行时不报错。

原因:先调了plt.show(),窗口关闭后画布被清空,再调plt.savefig()就保存了个空白图。

解决:先savefig再show,或者干脆不在脚本里show,只保存文件。批量生成图表时,每张图之后要plt.close(),否则内存里会堆积画布,图多了会卡死。

5. 进阶技巧:把大作业变成能写进简历的项目

如果你想让这个电影数据库大作业不只是「交完就忘」,有几个方向可以往下挖。

第一个方向是加一个简单的 Web 界面。用 Flask 或 Streamlit 把查询和图表包一层,浏览器里能点能看。Streamlit 尤其适合这种场景,十几行代码就能把 pandas 的 DataFrame 和 matplotlib 的图表渲染成网页。面试时你说「我做过一个电影数据系统,有 Web 界面」,比「我写过 SQL」有说服力得多。

第二个方向是引入更真实的数据分析。比如用networkx做演员合作网络图,两个演员合作过同一部电影就连一条边,用中心性指标找出「合作最多的演员」。或者用scipy.stats做评分分布的假设检验,看看不同类型电影的评分差异是否显著。这些分析不需要很深的统计学背景,但能让你的报告从「描述性统计」升级到「推断性分析」。

第三个方向是把 SQLite 换成 MySQL 或 PostgreSQL,加上连接池和索引优化。在 rating 表的(movie_id, user_id)上建联合索引,然后对比建索引前后的查询耗时。这个对比实验写进报告,就是实打实的性能优化经验。

# 建索引前后对比查询耗时 import time cursor.execute("CREATE INDEX IF NOT EXISTS idx_rating_movie ON rating(movie_id)") start = time.time() cursor.execute("SELECT movie_id, AVG(score) FROM rating GROUP BY movie_id") cursor.fetchall() print(f"建索引后耗时: {time.time() - start:.4f} 秒")

索引不是越多越好。rating 表如果只有几千条数据,建不建索引差别不大;但数据量到十万级以上,GROUP BY movie_id这种查询没有索引就会明显变慢。报告里写清楚「在什么数据量下、什么查询模式下、索引带来了多少提升」,比单纯说「我建了索引」有价值。

最后一个建议:把整个项目整理成一个 GitHub 仓库,README 里写清楚项目结构、运行方式、依赖列表。数据集如果太大就放个采样版本,代码和文档完整放上去。这个仓库本身就是你数据库和 Python 能力的最好证明。我见过太多人把大作业交完就扔,等到找工作时想展示项目,发现代码早没了。养成把每个课程项目整理归档的习惯,后面会省很多事。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询