ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Django电影数据分析可视化系统:从爬虫采集到词云展示的实现

Django电影数据分析可视化系统:从爬虫采集到词云展示的实现

简介:一份基于Python Django框架实现的电影数据分析与可视化系统代码包,面向数据分析、爬虫及Web可视化初学者,可用来学习从数据采集到前端展示的完整流程。压缩包共42个文件、约299.78MB,主要包含HTML模板、Python源码、CSV数据文件、SQLite数据库以及环境安装说明等,目录采用templates、static、data等分类组织,模板、样式与数据分离,便于按模块定位代码。系统涵盖用户登录、电影爬虫、TOP10推荐、模糊搜索和数据可视化分析界面,能够按电影年代、产地、类型等维度生成饼图、柱状图,并通过词云图分析电影评价,从数据入库到图表渲染一应俱全。已有1040人学习下载,代码内置爬虫与预处理脚本、停用词表和示例数据,操作说明与初始数据库一并附赠,适合在真实数据集上动手复现,也可作为课程设计或毕业设计的参考工程。

1. 先放结论:电影数据分析系统的难点不在画图,在数据进库

先放个结论:这套基于 Python Django 的电影数据分析与可视化系统,难点不在画图,在数据怎么干净地进库。很多人以为写个脚本读一遍 CSV 就能出图表,真上手才发现,登录要接 session、搜索要拼模糊匹配、词云要切中文还要过滤停用词,前端还得有一个能交互的展示看板。我拆的这份工程,恰好把这些环节完整串了一遍:爬虫采集、CSV 中转、SQLite 存储、用户登录、TOP10 推荐、按片名导演演员的模糊搜索,以及年代、产地、类型的饼图柱状图和评论词云图。适合正在做课程设计、想参考一套完整工程链路、或者打算把 Django 项目改造成自己分析系统的从业者。下面按我复现时的顺序来拆。

2. 搭骨架:从 manage.py 到 runserver 的启动底子与建表顺序

拿到工程后,第一件事不是看代码,而是先把这个工程的布局摸清楚。Django 项目上手最大的障碍是「文件这么多,谁管谁」,只要把入口、配置、数据、模板这四层分清楚,后面所有的操作都会顺。

2.1 先读懂这份工程的文件布局

打开目录后,我按「从哪里启动、哪里配、哪里存数据、哪里放页面」四个维度把文件归了一下类,整理成下面这张表:

路径/文件作用复现时要留意的点
manage.pyDjango 命令行入口,启动服务、建表、检查配置全靠它所有 manage 命令都要在项目根目录执行
app/项目配置和视图逻辑所在地,含 settings.py、urls.py、views.py、wsgi.py、asgi.pyDjango 项目包,不是普通业务 app
create_data.py读取 movie_data 下的 CSV,把数据清洗后写入 SQLite删掉 db.sqlite3 后靠它重建数据
db.sqlite3系统的数据库文件,用户数据、电影数据都存在这里可以删除重建,也可以直接用现成的
movie_data/爬虫原始数据,含 movies.csv、users.csv、person.csv、ratings.csv、comments.csv新爬的数据先进这里,再跑脚本入库
data/运行期依赖文件,含 tasks.csv、stopwords.txt 等stopwords.txt 是词云过滤停用词的关键
templates/页面模板,base.html 是公共框架,fc_*.html 是各功能页fc_charts.html 是可视化主页面
static/项目的 css、图片等静态资源页面样式丢失时先查这里和 settings 配置

这十个文件/目录里,最核心的关系在 app、templates、movie_data、create_data.py 四者之间。Django 的常规逻辑是:请求先进入 urls.py 路由,由 views.py 里的视图函数处理,视图从数据库里取数据,再渲染 templates 下的模板;页面引用的静态样式从 static 读。而 create_data.py 是一个不经过 HTTP 的独立脚本,它直接读取 movie_data 里的 CSV 文件,把数据灌进 db.sqlite3。好处是「可重建」:哪天数据库搞坏了,删掉 db.sqlite3,重新跑一次 create_data.py,整个系统回到初始状态。

这里要注意一个细节:工程里有两个看起来很接近的目录,movie_data 和 data。我复现时的理解是,movie_data 是爬虫抓回来的原始数据文件,data 里放的是运行时依赖的字典和任务配置,比如停用词表。实际改数据时,优先改 movie_data 下的 CSV,不要动 data 下的配置。

2.2 环境安装与启动:从 venv 到 runserver 的完整命令

项目里的「环境安装.docx」和「环境启动操作.txt」已经把依赖和启动顺序写成文档了,我按自己的习惯重新走了一遍,推荐的执行顺序如下。先建虚拟环境,避免把依赖装到系统 Python 里,后面部署到服务器时会清爽很多。

# 1. 创建虚拟环境,Windows 和 Linux 都适用 python -m venv venv # 2. 激活虚拟环境 # Windows 下用这条: venv\Scripts\activate # macOS / Linux 下用这条: source venv/bin/activate # 3. 安装核心依赖,Django 是骨架,pandas 处理 CSV,jieba 分词,wordcloud 生成词云 pip install django pandas jieba wordcloud # 4. 创建 Django 内置表,auth、session 等登录相关表在这一步生成 python manage.py migrate # 5. 执行数据创建脚本,把 movie_data 下的 CSV 灌进 SQLite python create_data.py # 6. 启动开发服务器,默认监听 8000 端口 python manage.py runserver

每一条命令都不要跳过。尤其是第 4 步 migrate,很多人以为数据全在 create_data.py 里,其实 Django 自带的用户认证表是在迁移时才建出来的。登录功能没有 auth 相关表,启动后访问登录页面会直接报 no such table 的错误。第 5 步 create_data.py 是数据入库的总入口,执行前确认 db.sqlite3 和 create_data.py 在同一个目录下,脚本里读 CSV 的路径用的是相对路径,位置不对会找不到文件。第 6 步跑起来后,浏览器访问 http://127.0.0.1:8000 就能看到登录页。

注意:migrate 必须先于 create_data.py 执行。create_data.py 写入数据时依赖表结构已存在,而内置的 user 表是登录功能的基础,顺序反了会出现表找不到或外键报错。

如果启动后页面能打开但登录报错,先回来看第 4 步有没有真正执行成功。这一步做完,系统骨架已经立住了,但页面上还没有什么可视化内容——因为数据来源这关还没过,这就是下一章要拆的部分。

3. 数据管道:爬虫 + CSV 中转 + create_data.py 一键入库

这个系统的数据链路分成两段:爬虫把网页内容抓到 CSV,create_data.py 把 CSV 洗进 SQLite。这条管道设计得很适合拿来学习,因为它把「采集」和「入库」彻底解耦了。

3.1 为什么爬虫结果先落 CSV 再进库

很多人在这一步会纠结:为什么不直接在爬虫里连数据库,抓一条写一条?我最初也这么干过,但跑了一天后换了方案。先落 CSV 再入库,有三个实打实的好处。

第一是可重跑。反爬严格的站点,重复请求很容易被封,CSV 是纯文本,解析挂了重跑一遍成本极低。第二是可检查。CSV 可以用文本编辑器直接打开,入库之前先肉眼扫一遍字段和空值,比入库之后再排查快得多。第三是可接多数据源。列表页、详情页、评论页通常是不同页面,分开抓、分开存,最后统一在 create_data.py 里合并,逻辑更干净。

爬虫部分常见做法是用 requests 拿页面,BeautifulSoup 解析节点。下面这段代码是我按这份工程的数据字段改的示例,目标是抓取每个电影条目并追加到 movie_data 下:

import csv import time import requests from bs4 import BeautifulSoup def fetch_detail(item_id): # 列表页拿到详情页 URL 后,逐个请求并解析 url = f"https://example-movie-site.com/detail/{item_id}" resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") return { "title": soup.select_one(".title").text.strip(), "year": soup.select_one(".year").text.strip(), "country": soup.select_one(".country").text.strip(), "genre": soup.select_one(".genre").text.strip(), "rating": soup.select_one(".rating").text.strip(), "director": soup.select_one(".director").text.strip(), "actors": soup.select_one(".actors").text.strip(), } def save_to_csv(items, path="movie_data/movies.csv"): with open(path, "a", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "year", "country", "genre", "rating", "director", "actors"]) writer.writeheader() writer.writerows(items) # 常见做法:按页抓取,每次抓完 sleep 一下,避免请求过快 for page in range(1, 11): items = [fetch_detail(i) for i in range((page - 1) * 20, page * 20)] save_to_csv(items, "movie_data/movies.csv") time.sleep(1)

这段代码的字段和 movie_data/movies.csv 是能对上的:title、year、country、genre、rating、director、actors。这里有两个关键的参数设置。第一个是 headers 里的 User-Agent,至少要伪装成一个浏览器,否则很多站点直接返回 403。第二个是 encoding="utf-8-sig",这个编码能避免 Excel 打开 CSV 时中文乱码,也是后面 create_data.py 读取时的前提。time.sleep(1) 是给目标站点留缓冲,真实爬虫里还要在解析节点外套一层 try/except,否则一个页面超时,整批数据就中断了。

3.2 create_data.py 的三件事:建表、去重、批量写入

CSV 只是中间产物,真正决定系统里有什么数据的是 create_data.py。这个脚本要做三件事:加载 Django 配置、读取 CSV、批量写入数据库。核心逻辑我拆成了下面这段:

# create_data.py 主流程 import os import django # 1. 指定 Django 配置模块,让脚本可以脱离 runserver 独立运行 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") django.setup() import pandas as pd from app.models import Movie # 假设模型类叫 Movie,字段与 CSV 对应 def load_csv_to_df(path): # 2. 读取 CSV,utf-8-sig 防止中文乱码 df = pd.read_csv(path, encoding="utf-8-sig") # 丢弃完全空的行,避免 None 入库导致前端报错 df = df.dropna(how="all") return df def sync_movies(): df = load_csv_to_df("movie_data/movies.csv") # 3. 字段级空值补为空字符串,不要写入 None df = df.fillna("") objs = [ Movie( title=row["title"], year=row["year"], country=row["country"], genre=row["genre"], rating=row["rating"], director=row["director"], actors=row["actors"], ) for _, row in df.iterrows() ] # 4. 批量写入,主键冲突时直接跳过,不影响后续数据 Movie.objects.bulk_create(objs, ignore_conflicts=True) if __name__ == "__main__": sync_movies()

这段脚本有三个容易被忽略的点。第一个是os.environ.setdefault和django.setup(),这是脚本脱离 runserver 独立运行的前提,很多新手脚本里缺了这两行,一执行就报 django.core.exceptions.ImproperlyConfigured。第二个是bulk_create(ignore_conflicts=True),它表示遇到主键冲突就跳过这一条,不会中断整批写入,所以脚本可以反复执行。第三个是空值处理的两道防线:dropna(how="all")去掉整行空记录,fillna("")把字段级空值补齐,否则数据库里存了 None,前端模板渲染时会显示一个刺眼的 None 字样。

这套管道设计得比较基础,但很实用。如果以后数据量大到百万级,可以把bulk_create换成批量分片写入,一次写 500 条,再加事务包裹,速度能再上一个台阶。对于课程设计和日常分析场景,当前方案足够稳。

4. 核心链路:登录、模糊搜索、TOP10 推荐与可视化接口的串联

数据库里有数据之后,系统的核心链路就清晰了。这一章我会按「用户登录 → 搜索 → TOP10 推荐 → 图表接口」的顺序来拆,每一段都是可以直接拿去改的代码。

4.1 登录与搜索:session 与 Q 模糊查询

登录功能在 Django 里有现成的认证体系。工程里的 users.csv 提供了用户数据,通过 create_data.py 灌入库后,用 auth.authenticate 校验即可。登录成功后调用 auth.login,把登录态写进 session,后续页面就能通过 request.user 识别当前用户。参考实现如下:

# views.py 里的登录视图 from django.shortcuts import render, redirect from django.db.models import Q, Count from django.http import JsonResponse from django.contrib import auth from .models import Movie # 模型类,按工程实际导入路径为准 def login(request): if request.method == "POST": username = request.POST.get("username", "").strip() password = request.POST.get("password", "").strip() user = auth.authenticate(username=username, password=password) if user is not None: auth.login(request, user) return redirect("index") return render(request, "start.html", {"error": "用户名或密码错误"}) return render(request, "start.html")

登录之后是搜索。工程里要求的搜索方式是按电影名、导演、演员模糊匹配,Django 的 ORM 里面对模糊匹配的常规方案是icontains,配合 Q 对象把三个字段用 OR 连接起来:

def search(request): kw = request.GET.get("q", "").strip() results = Movie.objects.none() if kw: results = Movie.objects.filter( Q(title__icontains=kw) | Q(director__icontains=kw) | Q(actors__icontains=kw) )[:50] return render(request, "fc_search.html", {"results": results, "kw": kw})

这里几个参数要说明一下。icontains在 SQLite 里会生成 LIKE '%关键词%' 的查询,大小写不敏感,这正是模糊匹配的语义。三个 Q 对象用|连接,生成的 SQL 是 OR 条件,只要片名、导演、演员任一字段命中就返回。[:50]是切片限制,防止搜索词太泛时把几千条结果一次性渲染到页面上。调用前先strip()去掉空白,空关键词直接返回空结果,避免无意义的全表匹配。

TOP10 推荐在这个工程里用的是「评分优先」的代理策略,也就是把评分最高的十部电影展示出来。这个指标最直接,也最容易解释:

def top10(request): movies = Movie.objects.filter(rating__gt=0).order_by("-rating")[:10] return render(request, "fc_movies.html", {"movies": movies})

rating__gt=0过滤掉没有评分的电影,order_by("-rating")降序排列,切片拿前十条。如果你的系统需要按用户偏好加权,可以在查询条件里加入用户的历史评分或收藏维度,但基础版本的「偏好」用评分排序已经能自圆其说。

4.2 可视化接口:把聚合结果转成 ECharts 需要的 JSON

可视化是这个系统的重头戏。工程里有饼图、柱状图、词云图三类,对应模板文件是 fc_charts.html 和 fc_cloud.html。后端要做的,不是直接画图,而是把数据库里的记录聚合成前端图表库能直接吃的 JSON 结构。

以年代分布和类型分布为例,聚合逻辑用 ORM 的 values + annotate 就能实现:

def chart_data(request): # 按年代聚合,得到每年的电影数量 years = list( Movie.objects.values("year") .annotate(cnt=Count("id")) .order_by("year") ) # 按类型聚合,注意 genre 字段可能存了多个类型,用分隔符拆分 genres = {} for g in Movie.objects.values_list("genre", flat=True): for one in str(g).split("/"): genres[one] = genres.get(one, 0) + 1 return JsonResponse({ "years": [{"name": y["year"], "value": y["cnt"]} for y in years], "genres": [{"name": k, "value": v} for k, v in genres.items()], })

values("year")在 SQL 层面等价于 GROUP BY year,annotate(cnt=Count("id"))是每个分组的数量,order_by("year")保证 X 轴按时间顺序排列。genre 字段的处理要特别留意,电影类型在 CSV 里常见的存法是「剧情/爱情」这种复合格式,如果直接按整串分组,会得到「剧情/爱情」这个整体,而不是「剧情」和「爱情」两个独立分类,所以拆开统计更合理。

词云接口走的是另一条路,它需要读评论数据、分词、过滤停用词,最后返回词频 Top 100:

# 词云接口:jieba 分词 + 停用词过滤 + 词频统计 import jieba def wordcloud_data(request): with open("data/stopwords.txt", encoding="utf-8") as f: stopwords = {line.strip() for line in f} df = pd.read_csv("movie_data/comments.csv", encoding="utf-8-sig") words = {} for text in df["comment"].dropna(): for w in jieba.lcut(str(text)): if len(w) > 1 and w not in stopwords: words[w] = words.get(w, 0) + 1 top = sorted(words.items(), key=lambda x: x[1], reverse=True)[:100] return JsonResponse({ "words": [{"name": w, "value": c} for w, c in top] })

词云这一步最能体现数据清洗的价值。jieba 分词后如果不做过滤,高频词永远是「的」「了」「很」这类虚词,词云图没有任何信息量。工程里的 stopwords.txt 就是干这个的,读文件时每行strip()去掉换行符和空格,否则停用词比不上的情况经常发生。len(w) > 1是基础过滤,把单字词排除掉。词频超过 100 就截断,也是为了让前端渲染时词云不至于太密。

4.3 模板侧的数据对接:base.html 继承与前端图表渲染

后端把 JSON 给出来了,前端怎么接?这就要看 fc_charts.html 和 base.html 的关系。base.html 是公共框架,顶部导航、css 引用都在这里;fc_charts.html 通过 Django 模板继承来复用这套框架,自己的内容放在 block 区域里。

前端图表渲染用 ECharts,这是这套工程里很合适的选型,饼图、柱状图、词云都能覆盖,而且 JSON 结构刚好能直接对接后端返回值。核心渲染代码:

// fc_charts.html 中的饼图渲染 fetch('/chart_data/') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('genreChart')); chart.setOption({ series: [{ type: 'pie', data: data.genres, // 后端返回的 {name, value} 数组直接可用 radius: ['35%', '65%'] }], tooltip: { trigger: 'item' } }); });

后端返回的 JSON 结构和 ECharts 的 series.data 高度一致,都是{name, value}数组,可以省去前端的二次加工。radius: ['35%', '65%']表示的是环形饼图的内径和外径比例,如果你想要实心饼图,去掉这个字段即可。词云图也是一样的套路,fetch /wordcloud_data/ 之后把 data.words 传给词云系列。这里强烈建议把 echarts.min.js 下载到 static 目录下本地引用,开发服务器无所谓,但部署到生产环境时依赖 CDN 会有额外的网络风险。

5. 避坑清单:五个我在复现时反复翻车的点

这套工程整体不算复杂,但真按步骤跑起来,有几个位置几乎每跑一遍都会遇到问题。我按数据相关和环境相关分成两节,把我踩过的坑记下来,每条都按现象、原因、解决来写。

5.1 数据相关的三个坑

坑一:CSV 导入时中文变乱码

现象:create_data.py 跑完,页面上电影名显示成一串问号或乱码。

原因:爬虫保存 CSV 时用了 GBK 或系统默认编码,而 pandas 默认按 utf-8 读取,两边对不上,导入后自然无法正常显示。

解决:统一用 utf-8-sig 编码。读的时候写pd.read_csv(path, encoding="utf-8-sig"),爬虫保存时用open(path, "w", encoding="utf-8-sig")。实际排查中很多「乱码」其实就是漏了这个 BOM 标记,Excel 打开过一次再另存为,编码就会被改掉,所以爬虫和入库两端的编码声明都必须写死。

坑二:词云图里中文全变方框

现象:词云能出图,但字符全是方框,一个汉字都没有。

原因:wordcloud 库默认字体不支持中文,它按字符编码找字形,找不到就画方框。这是字体问题,不是数据问题。

解决:给 WordCloud 指定中文字体路径,比如 Windows 下用font_path="C:/Windows/Fonts/simhei.ttf";Linux 服务器上可以把开源中文字体放到项目的 static/fonts 目录下,传绝对路径。如果字能出来但高频词不对,去检查 stopwords.txt 里是不是每行头尾带了空格,读文件时逐行strip()是必须的。

坑三:评分缺失导致 TOP10 混入无评分电影

现象:首页「TOP10」里出现评分 0 或空值的电影,排序乱套。

原因:爬虫没抓到评分,CSV 里该单元格为空,入库时没有过滤。

解决:在查询时加条件Movie.objects.filter(rating__gt=0),只保留评分大于 0 的记录;更彻底的做法是在 create_data.py 里用df.dropna(subset=["rating"])把无评分的行直接扔掉,入库之前就把脏数据挡在门外。

5.2 环境与部署的两个坑

坑四:页面 HTML 能打开,但样式全丢

现象:浏览器里内容正常渲染,但文字全部挤在左上角,没有任何排版,控制台里一堆 css 404。

原因:settings.py 里 STATIC_URL 虽然配了,但 STATICFILES_DIRS 没有指向 static 目录,或者模板里用了写死的相对路径。

解决:在 settings.py 里加上STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")],模板里统一用{% load static %}加<link rel="stylesheet" href="{% static 'css/xxx.css' %}">。改完重启 runserver,浏览器强制刷新清缓存,别问我为什么知道要清缓存。

坑五:Django 依赖装不上,或者 runserver 启动就报 ModuleNotFoundError

现象:pip install 时报版本冲突,或者服务启动时某个第三方库导入失败。

原因:这类老工程通常有一批依赖间的版本约束,直接pip install django装到最新版,旧代码没跟上新版 API 的变化。

解决:先看工程里有没有 requirements.txt,有就pip install -r requirements.txt按锁定的版本装;没有就按「环境安装.docx」里标注的版本来。导入失败时第一时间看 traceback 里是哪个库名,再用pip show 库名查实际版本。核心原则是别用最新版跑老工程。

6. 验证数据可信度:给分析结果加一道质量巡检

系统能跑、图表能动,这不算完。数据分析的底线是数据可信,尤其是爬虫数据,源头不可控,入库前如果不做体检,后面的可视化再漂亮都是自欺欺人。

我的习惯是在项目里加一个独立的质量巡检脚本,每次数据入库后跑一遍,把关键指标打印出来。这个脚本可以放在项目根目录下,比如命名 check_data.py,内容参考如下:

# check_data.py 数据质量巡检,跑一遍再下结论 import sqlite3 con = sqlite3.connect("db.sqlite3") cur = con.cursor() def check_movie_table(table_name): total = cur.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0] print(f"总记录数: {total}") # 检查评分空值率,超过 5% 就要回头查爬虫 null_sql = f"SELECT COUNT(*) FROM {table_name} WHERE rating IS NULL OR rating = ''" nulls = cur.execute(null_sql).fetchone()[0] print(f"rating 字段空值: {nulls} ({nulls/total:.1%})") # 检查标题重复,重复率过高说明爬虫存在重复抓取 dup_sql = f"SELECT COUNT(*) - COUNT(DISTINCT title) FROM {table_name}" dup = cur.execute(dup_sql).fetchone()[0] print(f"标题重复数: {dup}") # 检查评分越界,正常电影评分范围是 0~10 bad_sql = f"SELECT COUNT(*) FROM {table_name} WHERE CAST(rating AS REAL) < 0 OR CAST(rating AS REAL) > 10" bad = cur.execute(bad_sql).fetchone()[0] print(f"评分越界: {bad}") # 把表名替换成你工程里的实际表名 check_movie_table("movies_table") con.close()

这个脚本查四件事:总记录数、评分空值率、标题重复数、评分越界。这几个指标能快速暴露大部分数据层问题。空值率超过 5%,说明爬虫阶段有字段大面积漏抓,这时候再好看的词云图也没意义;重复数高,说明爬虫翻页逻辑里有重复抓取,去重逻辑要在入库前补上。可视化界面是给外人看的,数据质量是给自己看的,两张图的差距往往就在这一步。

这真的不是我一开始就有的习惯。有一回词云图看起来特别漂亮,后来才发现停用词文件里多了个空格,「电影」这个词根本没被滤掉,整张图都在重复同一个词,我当时还拿这张图去演示过。从那以后,我每次改完 create_data.py 或者换了爬虫源,第一件事不是开浏览器看图表,而是先跑一遍这个巡检函数。图表是最会骗人的,先让数据说话,希望帮到你。

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

返回列表