简介:这是一份基于Python的数据库课程设计完整工程,围绕电影数据库实现Web端数据查询、统计分析与可视化,面向计算机相关专业学生或数据库初学者,可作大作业、课程设计或答辩演示。项目功能涵盖按用户ID检索观影记录、评分排序、电影前三标签及关联度评分,支持关键词模糊匹配和热门风格查询;界面包含用户ID、关键词、风格等录入项,结果分页展示。压缩包共77个文件,约4.96MB,主要包含10个Python源码、4个HTML及配套JS/CSS前端文件、15个Markdown文档(数据分析、索引优化、MongoDB安装与复制集配置、Docker排错等),另有ER图、数据表结构图等图片素材,目录结构清晰。目前已有359人学习下载。资源附数据集、操作报告、完成进度汇报和前端接口说明,代码经测试运行成功,可远程协助部署,适合需要完整参考方案的读者直接借鉴与二次开发。
1. 把电影数据库查询系统做成完整课设:这份 Python 大作业凭什么能直接复现
这份基于 Python 的数据库大作业,实现了一个以电影数据库为核心的 WEB 查询系统,服务端用 Django 搭页面和路由,数据层用 MongoDB 存用户、评分、标签和电影四类信息,整体覆盖了从数据导入、功能查询、索引优化到云服务器部署的完整链路。它不是单纯甩给你一堆代码,而是把排错记录、数据分析、复制集配置、前后端接口说明都整理成了文档,对正在做课程设计或准备答辩的人来说,相当于拿到了一份能对照着走完每一步的工程模板。适合两类人:一类是课程设计需要交完整作业的学生,另一类是打算把 MongoDB 和 Django 快速串起来做小型项目、想提前看见所有坑的开发者。
2. 数据模型先行:E-R 图和四张集合如何把查询语义钉死
2.1 电影、用户、评分、标签:从关系结构到 MongoDB 集合
打开资源包里的数据表结构图和 E-R 图,能发现这套数据在关系世界里是标准的四张表:用户表、电影表、评分表、标签表。用户和电影之间通过评分表产生联系,用户又通过标签表和电影产生第二层联系,E-R 图里用菱形和连线把这层关系画得清清楚楚。等落到 MongoDB 里时,关系模型被转换成了四个集合,集合名的命名方式是全小写复数:users、movies、ratings、tags。
MongoDB 不做 JOIN,所以跨集合查询要在应用层自己拼。这也是先看表结构的意义:你理解了每个字段的归属,才能明白任务 A 里“查用户看的电影”为什么必须先去 ratings 里拿记录、再去 movies 里补片名、最后钻到 tags 里取前三个标签。字段设计方面,评分表里通常包含 userId、movieId、rating、timestamp 四个字段,timestamp 是 Unix 秒级时间戳;标签表里除了 userId、movieId、tag,还带了一个 relevance 字段,表示用户对该标签的关联度评分,这个值直接决定“前三个标签”怎么排序。电影表的 genres 字段建议用数组类型,方便后面按风格做聚合。
| 集合 | 代表字段 | 说明 |
|---|---|---|
| users | userId | 脱敏后的用户编号 |
| movies | movieId, title, genres | genres 存数组,便于 unwind |
| ratings | userId, movieId, rating, timestamp | rating 范围 1~5,可带小数 |
| tags | userId, movieId, tag, relevance, timestamp | relevance 决定标签优先级 |
这一层想清楚之后,后面写查询就只是翻译工作,不会出现“查出来发现缺字段”的尴尬。实际上资源里的数据分析文档也印证了这一点:数据是做过脱敏的,用户 ID 被替换成了无规律的编号,原始姓名和邮箱全部抹掉,但业务关系保持完整,不影响聚合统计。
2.2 数据导入:pymongo 脚本兜底,Studio 3T 只负责可视化
数据导入是第一步,也是最容易翻车的环节。资源包里的 mongo.py 就是干这个活的。如果你自己重写,核心逻辑并不复杂:读 CSV,逐字段写进对应集合,最后建索引。我这里给出一个通用版本,按文件里的字段名来写:
import pymongo import csv client = pymongo.MongoClient("mongodb://127.0.0.1:27017/") db = client["movie_db"] # drop=True 保证重复执行不会产生脏数据,代价是原集合会被清空 db["ratings"].drop() db["movies"].drop() db["tags"].drop() # 导入评分表,rows 是生成的列表 rows = [] with open("ratings.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) for line in reader: rows.append({ "userId": int(line["userId"]), "movieId": int(line["movieId"]), "rating": float(line["rating"]), "timestamp": int(line["timestamp"]) }) # batch_size 控制单次写入条数,避免大数据量时内存爆掉 db["ratings"].insert_many(rows, ordered=False)这段代码的逻辑是:先清空旧集合再导入,保证脚本可以重复执行;insert_many 第二个参数 ordered=False 表示遇到个别坏数据时跳过继续写,不会因为一行脏数据中断整个导入流程。参数上,batch_size 默认 1000 条一批,实际导入时不必刻意调小,MongoDB 对批量插入的吞吐很友好。电影表和标签表同理,差别只在字段映射:标签表里把 relevance 转成 float,movieId 转成 int。
图形化工具方面,Studio 3T 在资源文档里被用来做可视化验证:连接后点 Import Data,选 JSON 或 CSV 格式,按字段名映射到目标集合即可。我的习惯是脚本导入为主、3T 只查不导,因为脚本可重复执行,3T 的向导一遇到字符集问题就要重新配一次映射。导入完成后用 db.ratings.countDocuments() 检查行数,基本能确认有没有丢数据。
3. 环境搭建:本机 MongoDB、Docker 与云服务器登录三步走
3.1 本地 MongoDB 安装:版本一致才能少踩一半的坑
资源文档里提到 Ubuntu 18.04 安装 MongoDB 和复制集配置,但我建议你先在本地把单节点跑通,再上云服务器。安装步骤不复杂,关键在配文件和目录权限。先装服务再手动指定配置启动:
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" \ | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list sudo apt-get update sudo apt-get install -y mongodb-org这里指定 4.4 版本是因为资源文档里所有命令都基于这个版本族,如果你装 6.0,配置项和部分警告信息会变,照着文档核对时会多出干扰项。安装完之后,改 /etc/mongod.conf 里的 net.bindIp,从 127.0.0.1 改成 0.0.0.0 或云服务器内网 IP,否则远程连接永远超时。dbPath 默认在 /var/lib/mongodb,磁盘不足时 idx 文件增长会直接拖垮写入性能,建议提前 df -h 看一眼剩余空间。
启动和验证用一条链路完成:systemctl start mongod 启动服务,mongosh --eval "db.runCommand({ping:1})" 检查连通性。如果拿到 ok 字段为 1,说明服务没问题。之后开启 auth 认证,在 admin 库创建管理账号,只需要一个具备 readWriteAnyDatabase 权限的用户就够用,不要图省事把 root 明文放进连接串。
3.2 Docker 跑 MongoDB:容器化部署的参数要点
资源包里有专门的 docker 安装 MongoDB 文档,用 Docker 的理由很直白:不用污染宿主机环境,换服务器也能一键还原。但容器模式有个容易翻车的点——容器重建后数据丢了。解决方案就是挂载 volume:
version: '3' services: mongo: image: mongo:4.4 container_name: mongo_db restart: always ports: - "27017:27017" volumes: - ./mongo_data:/data/db command: ["mongod", "--bind_ip", "0.0.0.0"]这段 compose 的核心是把容器的 /data/db 目录映射到宿主机当前目录下的 mongo_data,容器删了重建,数据还在。参数说明:ports 的左边 27017 是宿主机端口,右边是容器端口,两边保持一致时连接串不用改;command 里手动指定 bind_ip 是避免容器内默认绑定回环地址导致外部访问不到。restart: always 保证服务器重启后 MongoDB 自动拉起,这对挂在云主机上的课设项目很重要。
用 docker compose up -d 启动后,检查容器日志要盯的是服务启动完成的时间点,而不是容器状态。很多人看到 CONTAINER_STATUS 是 Up 就以为成功了,实际 mongod 还在初始化,立刻连就会报 connection refused。用 docker logs mongo_db | grep "waiting for connections" 看到了监听日志才算真就绪。
3.3 登录云服务器与上传数据:先想清楚端口和权限
远程部署时,文档里给的流程是:云服务器上装 MongoDB -> 开放安全组 -> 本机用 Studio 3T 或 pymongo 远程连。这里真正决定成败的不是安装命令,而是网络策略。我一般分四步走:先 ssh 登录服务器,再 scp 上传数据包,接着开放安全组端口,最后本地连一次验证。
ssh -i ~/.ssh/id_rsa ubuntu@<your_server_ip> scp -i ~/.ssh/id_rsa -r ./movie_data ubuntu@<your_server_ip>:/home/ubuntu/ sudo ufw allow 27017/tcp参数说明:-i 指定密钥文件,云服务器默认禁密码登录,密钥权限必须是 600,否则 ssh 会拒绝加载;scp 加 -r 是因为数据目录里有多个 CSV 文件。ufw 放行 27017 这一步仅对系统防火墙有效,云平台的控制台里还要同步放行同一端口。安全上要注意:生产环境绝对不要把 27017 暴露给全网,课设阶段建议把来源 IP 限制成校园网或自己家宽带的公网 IP,至少比裸奔强。
数据上传后,远程连不上 90% 是 bindIp 还是默认的 127.0.0.1。改完 mongod.conf 重启服务,用 netstat -tlnp | grep 27017 看监听地址:显示 0.0.0.0:27017 才说明绑定了所有网卡。这一节做到位,后面 3T 导入和 Django 连远程 MongoDB 才有意义。
4. 三个查询任务逐个落地:从 Django 视图到 MongoDB 查询
4.1 任务 A:用户历史观影记录 + 每部电影的前三个标签
任务 A 的语义是“根据用户 ID 查该用户看过的电影名字、评分,按时间从新到旧排序,再对每部电影给出前三个标签及关联度”。翻译成 MongoDB 操作就是两步:先按 userId 查 ratings,按 timestamp 倒序拿观影记录;然后逐条去 tags 集合查该电影的前三个标签。资源里的 index.py 是早期原型,到了 Django 里被收敛成视图函数,我这里写的是最终形态:
from pymongo import MongoClient client = MongoClient("mongodb://127.0.0.1:27017/") db = client["movie_db"] def get_user_history(user_id, limit=20): # 先取指定用户的评分记录,倒序排列 ratings = list(db["ratings"].find( {"userId": user_id}, {"_id": 0, "movieId": 1, "rating": 1, "timestamp": 1} ).sort("timestamp", -1).limit(limit)) results = [] for r in ratings: movie = db["movies"].find_one( {"movieId": r["movieId"]}, {"_id": 0, "title": 1} ) # 取关联度最高的前3个标签 tags = list(db["tags"].find( {"movieId": r["movieId"]}, {"_id": 0, "tag": 1, "relevance": 1} ).sort("relevance", -1).limit(3)) results.append({ "title": movie["title"], "rating": r["rating"], "timestamp": r["timestamp"], "tags": tags }) return results这段代码有两个值得注意的参数设置。sort("timestamp", -1) 表示按影片的评级时间倒序,这对应题目里“从新到旧”的要求;limit(3) 对应“前三个标签”,配合 sort("relevance", -1) 可以保证拿到的是关联度最高的那三个。外层 limit=20 是控制一次显示多少条记录,网页端配合滚动条展示,如果用户观影量特别大,建议改成分页而不是一次性拉全量。实际跑出来会发现,同一个 movieId 在 ratings 里有不同用户打分,但这里按 userId 过滤后不会重复。
4.2 任务 B:关键词匹配,$regex 和 $text 怎么选
任务 B 是“查询电影名字里有关键词的电影”,最朴素的写法是正则匹配:
def search_movies(keyword, page=1, page_size=20): regex = {"$regex": keyword, "$options": "i"} movies = list(db["movies"].find( {"title": regex}, {"_id": 0, "movieId": 1, "title": 1} ).skip((page - 1) * page_size).limit(page_size)) return movies这段代码里 $options: "i" 表示忽略大小写,英文电影名搜索时体验正常;skip 和 limit 组合做分页。但这个方案有个性能隐患:如果关键词前面不带 ^,正则匹配会扫描整个集合,也就是 COLLSCAN。数据量在几万条时无所谓,到几十万条会出现明显卡顿。资源文档里专门有一篇《优化(创建索引).md》,核心就是给 title 建索引并在正则里用前缀匹配,例如 $regex: "^"+keyword,这样能走索引。如果你追求更好的英文搜索体验,可以换 $text 索引,但 $text 对中文分词不友好,电影名含有大量中文时反而不如正则稳定。我的建议是:课设场景直接用 $regex 加前缀匹配,简单可控,答辩也能讲清楚取舍逻辑。
4.3 任务 C:风格热门榜 Top20 的聚合管道
任务 C 是选做,但它最能体现对 MongoDB 聚合管道的掌握程度。查询某个风格最受欢迎的 20 部电影,“最受欢迎”的定义不能简单按平均评分排,否则一部只有两个人打 5 分的冷门片会排到第一名,这不符合直觉。资源里的做法是参考 IMDb 加权评分:用平均分和评分数共同计算,这里我用聚合管道实现:
def popular_by_genre(genre, top_n=20): pipeline = [ {"$match": {"genres": genre}}, {"$unwind": "$genres"}, {"$match": {"genres": genre}}, {"$lookup": { "from": "ratings", "localField": "movieId", "foreignField": "movieId", "as": "rate_info" }}, {"$project": { "title": 1, "avg_rating": {"$avg": "$rate_info.rating"}, "rate_count": {"$size": "$rate_info"} }}, {"$match": {"rate_count": {"$gt": 10}}}, {"$sort": {"avg_rating": -1, "rate_count": -1}}, {"$limit": top_n} ] return list(db["movies"].aggregate(pipeline))这里每一步都要看懂:第一个 $match 先按风格过滤电影,缩小数据范围;$unwind 展开 genres 数组是为了后续可能需要的分组操作;$lookup 把评分信息关联进来,$project 里用 $avg 求平均分、$size 数出评分数;第二个 $match 里 rate_count 大于 10 是为了排除样本量太少的片子;最后 $sort 先按平均分再按评分数做次级排序。参数 top_n=20 对应题目里的 Top20,如果改成 50 就能做热门榜前 50。
这段管道的性能瓶颈在 $lookup,因为 movies 与 ratings 的连接没有现成索引。实际优化时给 ratings 的 movieId 建单字段索引能明显加速。答辩时如果老师问“为什么用聚合而不是多条查询”,你就回答:聚合在 mongod 进程内完成关联和排序,不需要重复走网络回环,且管道每一步都是流式处理,内存开销比应用层多次查询小。
5. 数据分析、索引优化与常见问题排查:从秒回千条到毫秒返回
5.1 先看数据质量:分析脚本能暴露哪些问题
资源包里的“数据分析.md”和“简单分析数据.md”不是凑数的,它们回答了一个关键问题:数据集到底长什么样、能不能支撑任务查询。我用聚合函数快速做了一遍体检,发现两个直接影响开发进度的点。第一个是评分表的量级,直接决定了要不要分页、要不要建索引;第二个是标签表里 relevance 字段的缺失情况,如果大量记录没有关联度,任务 A 取前三标签就得先补默认值。体检脚本很简单:
print(db["ratings"].count_documents({})) print(db["movies"].count_documents({})) print(db["tags"].count_documents({}))用聚合看评分分布,一条命令就能看到 1~5 分的占比。如果发现评分集中在高分区间,说明数据本身有偏差,不是程序 bug,写分析报告时要如实说明。这个结论可用于任务 C 的加权公式里:评分分布越不均匀,越要强调评分数在热门榜中的权重。
5.2 索引设计:三个查询分别建什么索引、怎么验证
资源包里的优化文档把索引单独拎出来写,说明它对整体体验影响极大。任务 A 的查询路径是“按 userId 过滤 + 按 timestamp 排序”,复合索引答案很明确;任务 B 的关键在 title 前缀正则;任务 C 的 $lookup 靠 movieId 加速。三条索引建完,执行计划从 COLLSCAN 变成 IXSCAN,效果立竿见影。
db.ratings.createIndex({userId: 1, timestamp: -1}) db.movies.createIndex({title: 1}) db.ratings.createIndex({movieId: 1})参数说明:第一个索引把等值查询字段放前面,排序字段放后面,且排序方向与查询一致,这样才能避免内存排序;第二个索引对 title 建升序索引,配合前缀正则使用;第三个索引服务的是 $lookup 的关联查询,关联字段单索引足够。建完之后用 explain 验证,不要凭感觉:
db.ratings.find({userId: 123}).sort({timestamp: -1}).limit(20).explain("executionStats")看 executionStats 里的 winningPlan 字段,如果出现了 IXSCAN 字样,说明索引生效;如果还是 COLLSCAN,检查是不是正则没用前缀匹配,或者复合索引字段顺序不对。索引不是越多越好,一旦发现用了索引反而更慢,要立即检查是不是索引选择性太差,userId 这种高区分度字段很少出问题。
5.3 避坑清单:Docker 卡死、端口不通、正则不走索引
以下是这套资源复现过程中最常踩的坑,每个都按现象、原因、解决的顺序写,排错时对照着查即可。
现象一:docker ps 长时间卡住无输出,docker stop 也没反应,容器里的 MongoDB 完全访问不了。原因是 docker daemon 或 containerd 进程僵死,常见诱因是磁盘空间写满或 iptables 规则被外部修改。解决方法是先 df -h 检查磁盘占用,确认不是磁盘满再重启服务:sudo systemctl restart docker。如果重启后容器起不来,docker logs mongo_db 看具体报错,重点查挂载目录的权限。
现象二:本地 pymongo 连云服务器 MongoDB 永远超时。原因多半是 mongod.conf 里 bindIp 还绑定在 127.0.0.1,或云平台安全组没有放行 27017 端口。解决方法是改 bindIp 为 0.0.0.0 后重启 mongod,再用 netstat -tlnp 查看监听地址,最后在云平台安全组入方向添加 TCP 27017 规则。注意安全组配置完后要等一两分钟生效。
现象三:$regex 查中文电影名查不出来,英文能查。原因是导入 CSV 时编码不是 UTF-8,MongoDB 里存的已经是乱码,正则自然匹配不上。解决方法是导入脚本里强制指定 encoding="utf-8",导入后在 3T 里肉眼检查一条中文数据,确认不是乱码再继续。这个坑不仔细看很难发现,因为 countDocuments 返回值正常。
现象四:建了 title 索引,explain 显示的还是 COLLSCAN。原因是正则写法是 contains 式匹配,而不是前缀匹配。解决方法是把 { $regex: keyword } 改成 { $regex: "^" + keyword },并把字段值做一下小写预处理,两者配合,MongoDB 才能高效利用 B 树索引。
现象五:任务 C 聚合结果为空,$unwind 之后啥也不剩。原因是 movies 集合里 genres 字段有的是字符串"Action" ,有的是字符串列表,格式不统一,$unwind 对非数组字段直接报错或跳过。解决方法是导入时强制把 genres 转成数组类型,或者聚合前加一步 { $match: { genres: { $type: "array" } } } 过滤掉脏数据。数据质量问题是这类课设里隐藏最深的坑,尽早处理能省一整天的排查时间。
6. 进阶玩法:复制集配置与换数据重跑的完整流程
6.1 复制集:从单节点到三节点的高可用配置
资源文档里单独有一篇 Ubuntu 18.04 安装 MongoDB 与复制集配置,这块属于答辩加分项。单节点部署一旦宕机,整个系统不可用,而复制集提供高可用能力。配置思路是:三台机器或三个实例,都启用复制集模式,然后初始化主从关系:
rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "192.168.1.10:27017" }, { _id: 1, host: "192.168.1.11:27017" }, { _id: 2, host: "192.168.1.12:27017" } ] })执行后通过 rs.status() 查看每个节点的 stateStr,PRIMARY 表示主节点,SECONDARY 表示从节点。主节点挂掉后,集群会自动选举新的 PRIMARY,连接串里写 multiple hosts 就能实现故障转移。这里的参数 _id 必须是复制集名称,三台机器 mongod.conf 里 replSetName 要一致,否则节点之间无法通信。课设场景下不必真的开三台服务器,本机起三个 mongod 进程、不同端口就能模拟。
6.2 换数据集重跑的流程:把复用成本压到最低
这套资源复现完一遍,我拿到手的第一反应是:换一套电影数据,整套流程是不是还能跑?答案是能,但前提是把每步的依赖都整理清楚。我的做法是维护一个检查清单:先改 config.py 里的连接串和数据库名,重跑 mongo.py 导入脚本,再执行三条 createIndex 语句,最后起 Django 服务人工验收三个查询任务。曾经有一次我换了数据源之后直接起服务,任务 B 怎么查都是空,排查半天发现导入脚本里字段名和旧数据源不一样,mongo.py 读不到对应的列。从那以后我每次换数据都强制走一遍完整的导入和索引重建流程,连字段映射都要打印出来看一遍,希望这个习惯能帮到你少走一次弯路。
本文还有配套的精品资源,点击获取