ARTICLE DETAIL

资讯详情

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

基于Python+Django的豆瓣电影数据分析与可视化实战

基于Python+Django的豆瓣电影数据分析与可视化实战 简介这份基于PythonDjango的豆瓣电影数据分析毕业设计项目源码主要面向计算机相关专业正在准备毕业设计的学生也适合需要系统性项目实战练习的中级学习者。项目经导师指导并认可评审分为98分提交的源码均已在本地编译并通过严格调试下载后即具备可运行条件。压缩包共2001个文件、约24.05MB内容以Django后端Python脚本.py为核心配合HTML/CSS/JavaScript等前端页面Bootstrap与Font Awesome样式库并包含大量SVG图标、source map调试映射及少量配置文件文件结构完整能清楚区分后端逻辑、前端资源与数据配置方便按模块学习或二次修改。目前已有60人学习浏览。借助这份源码读者可以快速复现一套豆瓣电影数据分析系统掌握Django项目搭建思路、后端数据处理与前端可视化展示的衔接方式并理解类似数据类毕设项目的完整组织流程适合直接用于课程设计、期末大作业或个人项目起步。1. 这个毕设选题好在哪它其实是一整套完整的数据工程闭环很多人误以为“PythonDjango对豆瓣电影数据进行分析”就是一个爬虫脚本加上几张统计图实际上这个标题背后藏着一个非常标准的全栈数据链路采集、清洗、存储、分析、可视化、Web呈现。毕业设计或者求职作品集里最怕的是“功能单点”要么只会爬虫要么只会调库画图。这个项目胜在把 Python 的数据处理能力和 Django 的 Web 能力打通了老师问任何一个环节你都能拿出代码和参数来回答。这同时也是我给学生指导时最常推荐的选题方向因为它难度梯度友好基础差的人可以只做 Top250 静态页面的采集和展示基础好的人可以在数据建模和并发爬取上玩出花。这篇笔记我会从环境搭建、数据建模、分析逻辑、可视化展示到最后的避坑指南完整走一遍这个项目的落地路径你照着做就能得到一个能跑通、能答辩、能写进简历的毕业设计源码框架。2. 搭建 Django 数据采集环境分析与请求链路设计2.1 安装 Django 与项目脚手架用命令行快速搭出标准结构这个项目的第一步不是写爬虫而是先把 Django 的“空壳”立起来。我一般会在虚拟环境里做隔离避免把系统 Python 环境搞脏。# 创建并激活虚拟环境Windows / macOS / Linux 通用 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装核心依赖web 框架 数据采集 数据分析做一遍 pip install django requests pandas matplotlib # 建立项目与 App 结构 django-admin startproject douban_proj cd douban_proj python manage.py startapp movies这里有两个参数值得注意。startproject douban_proj生成的是外层配置文件夹和manage.py而startapp movies生成的才是业务模块。这个项目里我们把“电影”相关的模型、视图、模板全部塞进movies这个 App 里。装完依赖后记得去douban_proj/settings.py里把movies加进INSTALLED_APPS列表官方叫法是“注册 App”。很多人第一步就跑migrate时报错no such table八成就是忘了注册 App导致 Django 不知道去哪找模型。这一步不需要写代码但它是整个项目的地基。2.2 用 requests 抓取豆瓣电影列表请求头与频率控制重头戏来了。豆瓣电影 Top250 是公开榜单但它的反爬机制一直很敏感。我一般不建议用 Selenium 去硬刚太重了直接使用requests配合完整请求头就够用。这个方法也是大部分公开源码包里采用的思路。import requests import time import random from fake_useragent import UserAgent def fetch_page(page_num0): 抓取豆瓣电影 Top250 的单页数据 page_num: 分页偏移量Top250 每页25条从0开始最大到225 url fhttps://movie.douban.com/top250?start{page_num}filter headers { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, # 核心参数User-Agent 必须伪装成主流浏览器 User-Agent: UserAgent().random, Referer: https://movie.douban.com/, } try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 这一步很重要豆瓣返回的 charset 是 gbk按 gbk 解码 return response.text except requests.exceptions.RequestException as err: # 遇到 418/429 就延时退避别硬刚 print(f请求失败: {err}准备冷却 {random.uniform(2, 5):.1f} 秒) time.sleep(random.uniform(2, 5)) return None这段代码是骨架。UserAgent().random的目的是让每次请求的浏览器标识都不同降低被识别为脚本的概率。Referer参数的作用是告诉服务器“我是从豆瓣跳转过来的”这是很多反爬绕过里的基础操作属于公开通用的安全请求头设置。如果你不想装fake_useragent库也可以从网上随便复制三五个真实的 UA 字符串放进列表里随机抽。另外一个血泪参数就是timeout。我之前带学生做项目时他不设置超时爬虫跑到第 3 页就卡死不动整个程序像黑匣子一样毫无反应。加了timeout10之后至少能保证单次请求失败后可以自动跳过错继续跑。解析 HTML 这件事我建议直接用parsel或lxmlXPath 比正则更抗结构变化。因为豆瓣的 HTML 结构很稳定你可以通过定位div.item来循环提取标题、评分、评价人数、导演、年份、类型这些字段。把这些字段先整理成 Python 字典列表再转成pandas.DataFrame存成 CSV 备份一份这样即使后续数据库写崩了原始数据还能再导一次。2.3 为什么要先把数据转成 CSV给数据上“后悔药”这一步是很多新手容易跳过的。他们总觉得直接抓完就model.objects.create()很爽但一旦采集脚本中途报错或者字段漏了就得全部重爬。我的习惯是先把数据落盘到douban_movies.csv然后做清洗最后再批量导入 Django 数据库。CSV 在这里扮演的是“后悔药”角色也是你和老师解释工程逻辑时的加分项。import csv import pandas as pd # 假设 parse_page(html) 函数已经解析出一堆电影字典 all_movies [] for page in range(0, 226, 25): html fetch_page(page_numpage) if html: page_movies parse_page(html) all_movies.extend(page_movies) print(f第 {page//25 1} 页完成累计抓取 {len(all_movies)} 条) time.sleep(random.uniform(1, 2)) # 用随机延时规避频控 # 保存原始数据 df pd.DataFrame(all_movies) # encoding 必须用 utf-8-sig否则 Excel 打开中文会乱码 df.to_csv(douban_movies.csv, indexFalse, encodingutf-8-sig) print(f数据已保存形状为{df.shape})CSV 顺便也让你能用 Excel 或者 Pandas 做一次肉眼查错。你看看rating列是不是被爬成了字符串release_date是不是还带个“(中国大陆)”后缀这些都是后续清洗的重点。让学生理解“持久化中间数据”是数据分析项目中非常关键的工程习惯后面写论文的“数据来源”章节也有素材可写。3. Django Models 建模让电影数据在数据库里“安家”3.1 设计电影表结构字段类型与索引选择Django 的 ORM 是这个框架的精华所在。给豆瓣电影建表字段不能想怎么定就怎么定要符合第三范式的习惯同时也要让查询高效。以 Top250 为例我一般会这么定models.pyfrom django.db import models class Movie(models.Model): # 电影名可能很长CharField 给足 255 字符 title models.CharField(max_length255, verbose_name电影名称) # 评分是浮点数保留一位小数即可 rating models.FloatField(verbose_name豆瓣评分) # 导演可能有多位用逗号分隔所以也算字符串 director models.CharField(max_length255, blankTrue, verbose_name导演) # 年份用 IntegerField便于做区间筛选和排序 release_year models.IntegerField(verbose_name上映年份, db_indexTrue) # 题材类型同样是逗号分隔字符串 genres models.CharField(max_length255, blankTrue, verbose_name电影类型) # 评价人数Top250 的经典参考维度 comment_count models.IntegerField(verbose_name评价人数) # 详细描述字段可能为空 summary models.TextField(blankTrue, verbose_name电影简介) class Meta: ordering [-rating, comment_count] verbose_name 豆瓣电影 def __str__(self): return f{self.title} ({self.release_year})在这个模型里db_indexTrue是一个经常被初学者忽略的参数。加了它之后在release_year上做filter或order_by会走数据库索引数据量一旦到几千条查询速度的差异就会非常明显。你也可以把title设为唯一键uniqueTrue方便后面做去重但我更推荐用get_or_create而不是unique约束因为爬虫数据的标题难免带点细微差异强约束会导致导入失败。3.2 编写 Django 自定义管理命令用 management command 导入 CSV不要在视图里写数据导入不要在 shell 里一条条敲最干净的做法是写一个自定义的管理命令这也是源码包里最常见的结构。它能在命令行直接运行还能方便地复现数据导入过程。import csv from django.core.management.base import BaseCommand from movies.models import Movie class Command(BaseCommand): help 从 CSV 文件导入豆瓣电影数据到数据库 def add_arguments(self, parser): # 给命令传递一个参数CSV 文件的路径 parser.add_argument(csv_file, typestr, helpCSV 文件的路径) def handle(self, *args, **options): csv_path options[csv_file] imported_count 0 skipped_count 0 with open(csv_path, moder, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: # 用 get_or_create 做幂等插入 obj, created Movie.objects.get_or_create( titlerow[title], defaults{ rating: float(row[rating]), director: row[director], release_year: int(row[release_year]), genres: row[genres], comment_count: int(row[comment_count]), } ) if created: imported_count 1 else: skipped_count 1 # 每导入 50 条打印一次进度方便观察数据导入过程 if imported_count % 50 0 and imported_count 0: self.stdout.write(f已导入 {imported_count} 条...) self.stdout.write(self.style.SUCCESS( f导入完成新增 {imported_count} 条跳过 {skipped_count} 条。 ))这段代码里的get_or_create是 Django ORM 里我最喜欢用的一个方法。它先查后插天生防重复。defaults关键字参数里放那些你希望“更新”的字段而title作为匹配条件放在外面。写完后在项目根目录执行python manage.py import_movies douban_movies.csv就能看到导入效果。代码后面的self.stdout.write也不是摆设答辩时你可以直接运行一次命令给老师看屏幕上的进度打印这比口述“我导入了数据”要有说服力得多。3.3 视图函数里怎么拿数据ORM 查询与 Provide 给模板数据进库之后页面展示就轮到视图函数了。对于初学者我建议先写一个最简单的视图把Movie对象传给模板from django.shortcuts import render from movies.models import Movie def movie_list(request): # 按评分降序排评分相同就按评价人数降序 movies Movie.objects.all()[:100] return render(request, movies/movie_list.html, {movies: movies})这种写法的查询参数all()[:100]用到了数据库的LIMIT 100能看到效果但对“数据分析”项目来说还不够。我们真正要展示的是多维度的统计表比如评分的分布直方图、某年产出量最高的导演——这就轮到下一章里的 Pandas 血战 ORM 了。4. 核心分析逻辑把 SQL 交给 Pandas把结果交给 Django4.1 SQL 还是 Pandas在 Django 中嵌入 DataFrame 的规范姿势做数据分析项目和普通 Web 项目最大的区别就在于你如何处理查询结果。如果我们只用 Django ORM 的annotate和aggregate写出来又长又难懂而且不适应复杂的透视表需求。我一般会在 Django 视图里直接把数据导出成pd.DataFrame然后走 Pandas 的聚合管线。import pandas as pd from django.shortcuts import render from movies.models import Movie def analysis_view(request): # 把所有必要字段一次性掏出来避免 N1 查询 queryset Movie.objects.all().values( title, rating, release_year, genres, comment_count ) df pd.DataFrame(list(queryset)) # 按年份分组求评分均值再取最近 20 年 year_rating ( df.groupby(release_year)[rating] .mean() .reset_index() .tail(20) ) # 统计类型的出场次数因为 genres 是逗号分隔需要拆分 genres_list df[genres].str.split(|,, expandTrue).stack() genre_counts genres_list.value_counts().head(10) # 拼好上下文传给模板 context { best_movies: df.nlargest(10, rating).to_dict(records), year_rating: year_rating.values.tolist(), genre_counts: genre_counts.reset_index().values.tolist(), } return render(request, movies/analysis.html, context)这里的values()是 ORM 里的隐藏技能。它返回的不是Movie对象而是字典列表这正好是pd.DataFrame()能吃下的格式。如果你用list(Movie.objects.all())再转那你会被迫处理一堆模型中没用到的字段浪费内存和带宽。df[genres].str.split(...).stack()这一段是拆“剧情,爱情”这种多值字段的土办法。stack()会把多列压缩成一列产生所有类型的列表再交给value_counts()统计个数。这个思路在答辩时非常亮眼因为它展示了你能处理真实世界脏数据的能力而不是学校里那种规规整整的 CSV。4.2 三种必做的分析模型榜单 TOP10、年代趋势、类型占比如果说这一个章节实际运行的价值那就是毕业设计里最核心的“数据分析”展示页面。你要做出来的图表也不复杂就三个维度足以覆盖老师的提问分析维度计算逻辑可视化建议高分榜单nlargest(10, rating)按评分取前十水平条形图年代趋势groupby(release_year)[rating].mean()按年计算平均分折线图类型分布拆分genres字段后做value_counts()饼图或者柱状图为什么选这三个维度因为答辩时老师最常问的三个问题是“豆瓣评分高不代表评价人数多吧”、“这几年的分数是不是虚高”、“中国电影到底哪类比较强”你手里的数据都能回答。特别是年代趋势这个分析我建议把release_year做一个区间裁剪不要碰 2000 年前的老片不然数据量太稀疏折线图难看得像心电图。代码里.tail(20)就是取最近的 20 年这个“去脏数据”的动作实际是在调优分析效果。4.3 不要过度依赖 ORM 聚合什么时候你会想用 extra 或原生 SQL有同行拿着一套“Django 数据可视化”的活喜欢用Model.objects.filter(rating__gte8).count()去计算不同分数段的人数写起来像念经。如果你也要做评分区间分布我给你一个对比结论数据量在几千条以下时全表load到 Pandas 里做pd.cut是完全没有问题的但如果你的爬虫把全站十几万条电影都爬下来了那最好还是用 ORM 的Count加Q对象。from django.db.models import Count, Q from movies.models import Movie def rating_distribution_view(request): # 直接让数据库做分组统计而不是全表捞数据 dist { 9分以上: 0, 8-9分: 0, 7-8分: 0, 7分以下: 0, } high Movie.objects.filter(rating__gte9).count() mid_high Movie.objects.filter(rating__gte8, rating__lt9).count() mid Movie.objects.filter(rating__gte7, rating__lt8).count() low Movie.objects.filter(rating__lt7).count() dist[9分以上] high dist[8-9分] mid_high dist[7-8分] mid dist[7分以下] low return dist这种写法在数据库层面执行COUNT利用上了rating字段的索引。Pandas 并不是万能神药它在 IO 上不如数据库原生聚合所以代价就是你要多写几行filter。做数据分析项目时“数据量大时用数据库数据量小时用 Pandas”这是业界很普适的一条准则写进论文里也是干货。5. 这个毕设最容易踩的 4 个坑现象、原因与解决5.1 静态文件全白CSS 和图表组件加载不上现象页面能显示文字但所有样式全部丢失图表是空白方块浏览器控制台里全是报红。原因Django 在开发服DEBUGTrue下不主动服务静态文件除非你在urls.py里加了static()。很多从 Flask 转来的同学会很不适应因为 Flask 默认自动找static目录。Django 的哲学是静态文件交给 Nginx 处理开发环境下你必须在入口处手动加一句。代码样式加载不出来十有八九是这块没配置。解决在project/urls.py里追加from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 你的原有路由 ] # 这个追加只对 DEBUG 模式生效 if settings.DEBUG: urlpatterns static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)同时在settings.py里确认STATIC_URL /static/。如果是用 CDN 引的 ECharts那就先检查你自己电脑能不能打开 CDN 地址很多学校机房网禁外网要提前把 JS 包下载到本地static/之下这样答辩现场才不会翻车。5.2 中文字体乱码Matplotlib 画图全是方块现象用matplotlib直接画plt.title(电影评分分布)输出到图片里的中文全是实心豆腐块。这个情况在 Windows 上反而不常出现因为你本机装了微软雅黑一旦部署到 Linux 的云服务器上就会出现崩坏的字体。原因Matplotlib 自带字体里没有中文字库系统层面没有字体时会默认用 DejaVu Sans它对中文没有支持。这属于典型的“本地没问题部署就翻车”。解决先强制指定字体不要依赖服务器系统预设。import matplotlib.pyplot as plt import matplotlib as mpl # 这行是核心指定中文字体为黑体 mpl.rcParams[font.sans-serif] [SimHei] # Windows: SimHei mpl.rcParams[axes.unicode_minus] False # 解决负号乱码 plt.rcParams[font.family] sans-serif如果你是在 Linux 上跑系统里压根没有 SimHei就得先让管理员装一个fonts-wqy-microhei文泉驿中文字体包然后把上面的参数改成[WenQuanYi Micro Hei]。在答辩现场用matplotlib画图最好提前把图片导出保存成png而不是现场savefig这样可以规避服务器临时缺字体的问题。5.3 爬虫被封 IP 导致采集中断现象采集脚本跑到第 100 条的时候突然连续返回 418 状态码或者直接 “Connection timed out”。原因攻击性太强。requests连发走的还是默认的 Python UA。这个锅你不能全甩给豆瓣它已经是公开平台里比较克制的一个了你高频请求服务器必然启动风控。解决第一降低频率time.sleep(3)是最基本的尊重。第二使用fake_useragent随机替换身份。第三不要对同一页面进行重复爬取设计去重逻辑。如果被风控了最稳妥的做法是停半小时再继续不要换 IP 硬碰硬那是一条会被算法打得更惨的路线。做毕业设计不需要追求“秒爬几十万数据”能稳定跑完 Top250 就已经证明你掌握了爬虫节奏控制。5.4 Django 的 QuerySet 是惰性的你不 list 它就“空手而归”现象写了一个函数想print(movies)看看查到什么结果屏幕上打出来的是QuerySet object里面对象的字段也访问不到。或者在 Pandas 转 DataFrame 时发现df是空的。原因Django 的 QuerySet 是一种懒加载Lazy Loading设计。它不会在实例化时真正执行 SQL而是在你迭代、切片、list()时才去数据库里查。如果你把它直接丢给模板渲染模板迭代它会触发查询但如果你直接把它传给pd.DataFrame()方法错误就会拿到“空掌门”。解决Pandas 的DataFrame不能直接接收一个未求值的 QuerySet 对象必须强制求值# 错误示范DataFrame 里全是空值 # df pd.DataFrame(Movie.objects.all()) # 正确示范先取字典列表再喂给 DataFrame values Movie.objects.all().values(title, rating) df pd.DataFrame(list(values))values()方法让 ORM 只取我们指定的列list()强制执行查询拿到结果列表。这两步合在一起是 Django 数据分析项目的灵魂。你后续学 Django REST Framework 里的序列化本质上也是在做同样的事把 ORM 对象转成可操作的数据结构。6. 给图表穿上 Django 外衣用 ECharts 和自定义模板提升答辩档次6.1 在 Django 模板里安全地输出 JSON模板语法与safe过滤器数据分析不能只有表格复读图表才是高级的展示语言。我优先推荐 ECharts 而不是 Matplotlib 直接嵌页面因为 ECharts 的交互感更强响应式效果也更好出现在大屏上会特别有视觉冲击力。你需要把视图里算好的DataFrame转成 JSON 文本再传给模板import json from django.shortcuts import render from movies.models import Movie import pandas as pd def chart_view(request): # 取数据后聚合结果接上一段 qs Movie.objects.all().values(rating, release_year) df pd.DataFrame(list(qs)) # 按年份分组求平均分 yearly_avg df.groupby(release_year)[rating].mean().reset_index() years yearly_avg[release_year].tolist() ratings [round(r, 2) for r in yearly_avg[rating].tolist()] context { # json.dumps 可以确保在页面上输出为合法 JS 对象 years_json: json.dumps(years), ratings_json: json.dumps(ratings), } return render(request, movies/chart.html, context)模板里我们直接把这个 JSON 字符串塞进script标签里做成 ECharts 配置项!-- templates/movies/chart.html -- div idtrendChart stylewidth: 100%; height: 400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script // 注意这里必须加上 safe 过滤器否则 Django 会把引号转义掉 var years {{ years_json|safe }}; var ratings {{ ratings_json|safe }}; var chart echarts.init(document.getElementById(trendChart)); var option { title: { text: 豆瓣电影历年平均分趋势, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: years }, yAxis: { type: value, scale: true }, series: [{ name: 平均分, type: line, data: ratings, smooth: true, areaStyle: { opacity: 0.2 } }] }; chart.setOption(option); /script模板里第一个坑就是{{ years_json|safe }}没有safeDjango 会为了防 XSS 把、、转义成quot;之类的 HTML 实体JS 会直接报语法错误。这个细节非常经典几乎每个从零写该项目的人都会卡在这里。6.2 答辩时如何用“参数调优”和“异常处理”来包装你的代码代码能跑只是及格答辩拿高分的关键在于你对边界情况的处理。我最后这份给读者的建议是三个具体动作第一把你的视图层的所有数据查询都做到“不出错”。给Movie.objects.all()后面加上条件筛选让老师可以通过 URL 参数切换“只看某个年份之后”或“只看评分大于 8 分”的结果这会展示出你懂得动态交互。# 请求参数 ?start_year2000min_rating8 start_year request.GET.get(start_year, 1900) min_rating request.GET.get(min_rating, 0) qs Movie.objects.filter( release_year__gtestart_year, rating__gtemin_rating )第二在管理后台admin.py里注册Movie模型from django.contrib import admin from movies.models import Movie admin.register(Movie) class MovieAdmin(admin.ModelAdmin): list_display [title, rating, release_year, comment_count] list_filter [release_year, genres] search_fields [title, director]这让老师能直接通过后台看到你的表结构和数据录入情况是一个非常讨巧但很实用的加分项。第三把耗时分析结果做缓存。在views.py里加一个简单的装饰器备用from django.views.decorators.cache import cache_page cache_page(60 * 30) # 缓存 30 分钟 def analysis_view(request): # 现有的分析逻辑解释是这个接口的计算量较大用缓存可以降低对数据库的重复压力这也是真实业务里很常见的优化点。不要小看这三行代码它说明你考虑了性能问题而不仅仅是写通了功能这是从“码农”到“工程师”的分水岭。最后收个尾讲讲我自己的习惯我给类似的毕业设检查代码第一件是就是看settings.py里的DEBUG有没有改成False第二件是看requirements.txt里有没有固定版本号。这俩是刚进公司时被老工程师骂出来的癖好。你把项目提交上去或放进简历之前补一个requirements.txt里面写django4.2.x、pandas2.0.x这种精确版本让别人能一键恢复环境是很拉好感的细节。希望这篇笔记能帮你把这个经典选题做扎实真到答辩那天你会觉得底气和运气都在你这边。本文还有配套的精品资源点击获取
返回列表