ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从爬虫到可视化:豆瓣电影数据看板项目源码解析

从爬虫到可视化:豆瓣电影数据看板项目源码解析

简介:这套基于Python的电影数据可视化分析系统源码,面向正在完成Python课程设计或期末大作业的高校学生,也适合对网络爬虫与数据可视化感兴趣的入门开发者。资源包共8个文件,大小仅3.5MB,结构清晰:4个Python脚本(包括Flask后端入口app.py、爬虫脚本crawler.py、词云生成WordCloud.py)、2个压缩包(templates模板与static静态资源)、1个Markdown说明文档和1个SQLite数据库douban.db,轻量易用,便于按模块对照学习。目前已有2565人学习下载,可帮助读者快速理解电影数据从采集、存储到可视化展示的完整实现流程。借助这套源码,可以学习豆瓣电影数据的爬取方法、基于Flask的Web应用搭建思路,以及词云等可视化图表的生成逻辑,对完成课程设计报告和答辩演示具有直接参考价值。

1. 把豆瓣电影数据变成可视化看板:这份源码到底做了什么

期末周剩三天,别人交的是 PPT,你交一个能点开看图的网站,这印象分完全不一样。这份源码就是一个典型的「爬虫 + Flask + SQLite + ECharts」全家桶:crawler.py去豆瓣抓电影信息,存进douban.db,app.py起一个 Flask 服务,把数据库里的评分、类型、年份按统计口径聚合出来,最后用 ECharts 柱状图和词云展示到页面上。它不是只跑一次的脚本,也不是画好的静态图,而是从数据采集到前端渲染的完整链路。适合正在做期末大作业、课程设计,或者想往简历里放一个「能演示也能答辩」项目的从业者。你能把数据换掉、图表改掉、页面逻辑理顺,这比背十个接口都有说服力。

2. 源码拆包:目录结构、数据流与依赖安装

2.1 目录结构与数据流:crawler 到 db 到 Flask 到前端

解压之后你会看到一层仓库名目录,比如Movie-Analysis-Platform主-main,真正的工程在里面的doubanMovie_flask子目录下。这个嵌套结构是第一处容易踩坑的地方,很多人把路径指错,启动直接报错。整理一下核心文件:

文件/目录角色
app.pyFlask 主程序,路由与聚合查询都在这
crawler.py豆瓣爬虫,抓取电影信息写入 SQLite
WordCloud.py独立脚本,读取评论生成词云图
douban.dbSQLite 数据库,存电影表和评论文本
templates.zip前端模板,解压后得到templates/index.html
static.zip静态资源,解压后放 ECharts、jQuery 和词云图片
doubanMovie/早期版本的备用脚本,可以忽略
杂例/零碎的测试代码,不影响主流程
README.md项目说明与启动命令

数据流是一条直线:crawler.py抓取豆瓣 Top250 电影信息后写入douban.db;app.py通过 SQL 查询聚合出评分分布、类型分布等结果,渲染templates/index.html时把数据以 JSON 接口形式交给前端;前端用 ECharts 画图,WordCloud.py负责把评论变成词云图片放进static目录。

先跑通这条主链路,比研究每一行代码更重要。建议按「数据库 → Flask → 前端图表」的顺序验证,哪里断了就定位哪一段。

2.2 依赖环境:Python 版本与 pip 包清单

项目基于 Python 3,建议直接用 3.8 到 3.10 版本。WordCloud 这个包在 Python 3.11 以上的 Windows 环境偶尔会出现没有预编译 wheel 的问题,3.10 以下基本是pip install一把过。先切到工作目录,再装依赖:

cd doubanMovie_flask pip install flask requests beautifulsoup4 lxml wordcloud jieba

逻辑说明:flask负责 Web 服务,requests加beautifulsoup4是爬虫组合,lxml是 BeautifulSoup 的解析加速器,wordcloud生成词云,jieba做中文分词。如果后续看到脚本里用了pandas,再补一条pip install pandas即可,Docker 或虚拟环境里同样适用。

参数说明:Windows 用户如果pip install wordcloud报错,大概率是本机缺少 VC 编译工具链。常见做法是去 PyPI 下载对应 Python 版本的wordcloudwheel 文件手动安装,或者把 Python 降到 3.9 再试,后者更省时间。

2.3 启动项目:flask run 之前的三件事

拿到源码后不要急着python app.py,先做三件事,每一件都是真实耗时的坑。第一,templates.zip和static.zip必须解压到app.py同级目录,否则 Flask 找不到模板文件,页面会白屏。第二,确认douban.db存在且不是空文件,如果解压包里没有,先用python crawler.py生成数据。第三,检查5000端口是否被占用,被占就把 Flask 的端口参数改掉。

python crawler.py # 生成或更新 douban.db,数据已存在时可跳过 python app.py # 启动 Flask 开发服务器

提示:启动前先用dir或ls看一眼目录,确认app.py旁边就是templates和static文件夹。这一步能帮你省下二十分钟排查白屏的时间。

逻辑说明:crawler.py是独立于 Web 服务的脚本,跑一次会把数据写进数据库;app.py是常驻进程,启动后保持运行。如果只是想看可视化效果,数据库文件齐全时可以跳过爬虫。

3. 爬虫与数据层:douban.db 的字段口径与数据兜底

3.1 crawler.py 抓取逻辑与字段映射

crawler.py的核心逻辑是循环请求豆瓣 Top250 的分页 URL,用 BeautifulSoup 解析页面,把每部电影的标题、评分、评语等字段写入数据库。网上流传的版本大同小异,典型的写法是:

import time import random import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://movie.douban.com/", } def parse_page(html): soup = BeautifulSoup(html, "html.parser") movie_list = [] for li in soup.select("ol.grid_view li"): title = li.select_one(".title").get_text(strip=True) rating = li.select_one(".rating_num").get_text(strip=True) quote = li.select_one(".quote .inq") quote_text = quote.get_text(strip=True) if quote else "" movie_list.append({ "title": title, "rating": float(rating), "quote": quote_text, }) return movie_list def fetch_top250(): for start in range(0, 250, 25): url = f"https://movie.douban.com/top250?start={start}" for attempt in range(3): try: r = requests.get(url, headers=HEADERS, timeout=10) r.raise_for_status() movies = parse_page(r.text) save_to_db(movies) # 项目里封装好的写库函数 break except Exception as e: print(f"第 {start} 条失败:{e},重试 {attempt + 1}/3") time.sleep(3) time.sleep(random.uniform(2, 5))

逻辑说明:URL 里的start参数控制分页偏移,每页 25 条,循环 10 次就是 Top250。User-Agent和Referer缺一不可,豆瓣会在缺少 Referer 时直接返回 418 拒绝访问。每页抓完随机睡 2 到 5 秒,目的是降低请求频率,减少触发反爬校验的概率。

参数说明:timeout=10是单次请求的上限,网络不稳定时可以放大到 20;range(3)是单页最大重试次数,超过就跳过当前页。这两个参数是爬虫稳定性最关键的调节点,期末答辩时被问「为什么爬一半断了」,答案就在这里。

3.2 表结构与统计口径:评分、类型、年份怎么聚合

douban.db里最常见的就是一张movie表,字段设计直接决定后面 Flask 接口好不好写。项目里实际用到的建表语句类似这样:

CREATE TABLE IF NOT EXISTS movie ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, rating REAL DEFAULT 0, votes INTEGER DEFAULT 0, genre TEXT, director TEXT, year INTEGER, quote TEXT, url TEXT UNIQUE );

逻辑说明:rating用 REAL 存浮点数,因为豆瓣评分有 9.7、8.9 这种带一位小数的值;genre是一个字符串,多个类型用斜杠分隔;url加 UNIQUE 约束,防止爬虫重复插入同一部电影。这个约束是去重的关键,很多课程设计里数据越跑越多,就是因为没做唯一性校验。

统计口径方面,Flask 接口常用的聚合语句是把评分转成整数后分组,再把类型字段按斜杠拆开计数:

SELECT CAST(rating AS INT) AS bucket, COUNT(*) AS cnt FROM movie GROUP BY bucket ORDER BY bucket;

代码逻辑:CAST(rating AS INT)会把 7.9 和 7.2 都归到 7 分组,这样柱状图的横坐标是 0 到 10 共 11 个档位,视觉上更整齐。类型分布则是在 Python 里对genre字段做split("/")之后逐个累加,绕开了 SQL 对多值字段的尴尬操作。

3.3 反爬触发或数据量不足时的兜底策略

豆瓣的反爬通常不是直接封 IP,而是返回 418 或要求验证。如果你的crawler.py跑到第三页就断,先检查请求头,再检查延时。但如果是期末提交前最后一晚突然被限流,最稳妥的兜底是从已有数据库或本地 CSV 里导入一份样例数据,保证可视化页面有东西可看。

import sqlite3 conn = sqlite3.connect("douban.db") c = conn.cursor() # 样例数据格式:(title, rating, year, genre) sample_data = [ ("肖申克的救赎", 9.7, 1994, "剧情/犯罪"), ("霸王别姬", 9.6, 1993, "剧情/爱情"), ("阿甘正传", 9.5, 1994, "剧情/爱情"), ] c.executemany( "INSERT OR IGNORE INTO movie (title, rating, year, genre) VALUES (?, ?, ?, ?)", sample_data, ) conn.commit() conn.close()

逻辑说明:executemany一次插入多条,INSERT OR IGNORE配合 url 唯一约束或主键去重。这段脚本的价值不在于插入数据本身,而在于给你留了一条应急路径——爬虫挂了,页面依然能通过答辩演示。

4. Flask 路由与 ECharts 渲染:数据怎么从数据库走到图表

4.1 路由与聚合查询:一个页面一个 JSON 接口

app.py的设计思路很直接:首页路由渲染 HTML,其余路由以/api/开头返回 JSON,前端异步请求后画图。数据库连接写成一个通用函数,避免每个接口重复打开关闭连接。

from flask import Flask, render_template, jsonify import sqlite3 app = Flask(__name__) DB_PATH = "douban.db" def query(sql, args=()): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute(sql, args).fetchall() conn.close() return rows @app.route("/") def index(): return render_template("index.html") @app.route("/api/rating_distribution") def rating_distribution(): rows = query(""" SELECT CAST(rating AS INT) AS bucket, COUNT(*) AS cnt FROM movie WHERE rating > 0 GROUP BY bucket ORDER BY bucket """) return jsonify([{"bucket": r["bucket"], "count": r["cnt"]} for r in rows]) @app.route("/api/genre_distribution") def genre_distribution(): rows = query("SELECT genre FROM movie WHERE genre != ''") counter = {} for r in rows: for genre in r["genre"].split("/"): genre = genre.strip() counter[genre] = counter.get(genre, 0) + 1 return jsonify( sorted(counter.items(), key=lambda x: x[1], reverse=True)[:10] )

逻辑说明:row_factory = sqlite3.Row让查询结果支持按列名访问,r["bucket"]比r[0]可读性好很多。genre_distribution里先用split("/")拆开多值字段,再累加计数,最后用sorted按数量倒序取前 10,保证饼图不会被几十个小类目挤爆。

参数说明:WHERE rating > 0过滤掉没有评分的数据,避免CAST(NULL AS INT)产生的 0 分组干扰图表。实际使用中如果发现图表数据少得可疑,优先检查这一条件是否误伤了有效数据。

4.2 ECharts 核心配置:柱状图、饼图的动态数据绑定

前端模板里最核心的一段逻辑是fetch请求/api/接口,把返回的 JSON 映射到 ECharts 的xAxis和series上。以评分分布柱状图为例:

fetch("/api/rating_distribution") .then((res) => res.json()) .then((data) => { const chart = echarts.init(document.getElementById("ratingChart")); chart.setOption({ title: { text: "豆瓣电影评分分布" }, tooltip: { trigger: "axis" }, xAxis: { type: "category", data: data.map((d) => d.bucket + "分"), }, yAxis: { type: "value", name: "电影数量" }, series: [ { type: "bar", data: data.map((d) => d.count), barWidth: 30, }, ], }); });

逻辑说明:data.map((d) => d.bucket + "分")把整数分数转成字符串类目,data.map((d) => d.count)提取对应的计数值。ECharts 的类目轴和数值轴必须长度一致,错位会导致图形错乱或空白。

参数说明:trigger: "axis"让鼠标悬停时显示整条轴线上的提示,barWidth: 30固定柱子宽度,避免柱子过粗或过细影响观感。若默认主题太素,可以把color数组放在setOption顶层,效果是全局配色统一。

4.3 WordCloud 词云:从豆瓣短评到词频图

WordCloud.py一般是独立运行的脚本,目标是从评论文本里生成一张中文词云图,存到static/wordcloud.png,前端通过<img src="/static/wordcloud.png">直接显示。因为豆瓣短评本身是中文,必须经过分词处理,否则词云会变成一堆单字。

from wordcloud import WordCloud import jieba def generate_wordcloud(comment_file, output_file="static/wordcloud.png"): with open(comment_file, "r", encoding="utf-8") as f: text = f.read() words = " ".join(jieba.cut(text)) wc = WordCloud( font_path="C:/Windows/Fonts/msyh.ttc", width=1200, height=800, background_color="white", max_words=200, collocations=False, ) wc.generate(words) wc.to_file(output_file)

逻辑说明:jieba.cut(text)生成分词迭代器,用空格拼接成 WordCloud 期望的输入格式。font_path必须指向一个支持中文的字体文件,Windows 选择msyh.ttc或simhei.ttf,Linux 服务器一般用/usr/share/fonts/下的文泉驿字体。

参数说明:max_words=200限制词云里最多出现 200 个词,collocations=False禁止 WordCloud 自动把相邻词拼成新词组,这对中文很重要——不关掉的话会出现「电影电影」这种无意义的词组。

5. 课程设计跑路避坑指南:五个典型的翻车现场

5.1 页面白屏:templates 和 static 没解压

现象:python app.py启动后浏览器打开http://127.0.0.1:5000,页面全是白的,控制台报TemplateNotFound或一堆 404 静态资源错误。

原因:Flask 默认在app.py同级目录找templates和static文件夹,而压缩包里这两个目录是 zip 格式,没解压等于不存在。

解决:把templates.zip和static.zip解压到app.py所在目录,确认路径是templates/index.html和static/,然后重启app.py。经验是每次拿到源码先看有没有压缩态的目录,有就先解压。

5.2 爬虫返回 418 或 403:请求头不全与请求频率过高

现象:crawler.py第一次跑能抓 25 条,第二次循环开始报HTTP 418或403,代码本身没有语法错误。

原因:豆瓣对缺少Referer或请求频率过高的客户端返回反爬状态码。418 是「我是茶壶」的玩笑代码,本质是拒绝服务。

解决:在HEADERS里补上完整的User-Agent和Referer,并把每页之间的延时从固定 1 秒改成random.uniform(2, 5)秒。如果还是被限,就暂停十分钟再跑,不要无脑重试。

5.3 词云图片全是方框:中文字体缺失

现象:WordCloud.py正常执行完,生成图片在浏览器里打开一看,中文全部显示为方块或豆腐块。

原因:wordcloud库默认字体是 DroidSansMono,不支持中文。重新生成时没有指定font_path,中文字符全部无法渲染。

解决:在WordCloud构造函数里指定中文字体路径,Windows 用C:/Windows/Fonts/msyh.ttc,macOS 用/System/Library/Fonts/PingFang.ttc。如果不确定系统字体路径,可以先在 Python 里列出matplotlib.font_manager的可用字体逐个试。

5.4 图表空白不渲染:返回的 JSON 字段名对不上

现象:页面能打开,但某个图表的区域是空白的,浏览器控制台报Cannot read properties of undefined或数据长度为 0。

原因:前端代码写的是data.map((d) => d.name),但 Flask 接口返回的是{"bucket": 7, "count": 12},字段名不存在,map 出来的全是 undefined。

解决:打开浏览器开发者工具的 Network 面板,查看接口实际返回的 JSON 结构,再回去对齐前端字段名。切身体会是先console.log(data)再写渲染逻辑,能省大量 debug 时间。

5.5 sqlite3.OperationalError: no such table

现象:python app.py启动时报sqlite3.OperationalError: no such table: movie,数据库文件明明存在。

原因:douban.db是个空库或损坏文件,里面没有建表。可能解压包里本来就没有表结构,或者上次爬虫被中断后数据未落盘。

解决:先手动执行建表语句创建movie表,再跑crawler.py。更保险的做法是把建表逻辑封装成init_db()函数,在app.py启动时自动调用,这样空库也能自动恢复。

6. 把项目换成自己的数据:数据源替换与图表扩展

这份源码真正的扩展点不在 Flask 层,而在爬虫层。只要crawler.py能输出和现在movie表一致的字段,app.py和前端 ECharts 代码一个字都不用改。常见的替换方向是爬猫眼 Top100 或其他电影榜单,核心改动集中在解析逻辑:把parse_page里的 CSS 选择器改成目标站点的列表项结构,字段映射保持title、rating、genre、year不变,入库后所有可视化立刻生效。我一般会在crawler.py里留一个PAGE_SELECTOR配置项,换数据源时只改选择器和 URL 规则。

新增图表类型时,套路是「加一条 Flask 路由,加一个前端容器,加一个 setOption」。比如要做年份与评分的关系趋势,后端加一个/api/year_trend接口返回按年份聚合的平均分,前端在index.html里加一个<div id="yearChart">,然后复制已有柱状图的 fetch 逻辑改成折线图。需要注意依赖的static/echarts.min.js是否包含折线图模块,常用版本都支持,无需额外引入。

最后用自己的真实数据验证一遍全链路:删掉douban.db,跑爬虫,再启动 Flask,检查三个图表和一个词云是否正常刷新。我第一次拿到这类课程设计源码时,就是没解压 templates 直接启动,白屏折腾了二十多分钟,最后发现模板根本不在,那感觉就像考试写完了才发现答题卡没涂。从那以后,我拿到任何源码第一件事永远是先跑通主路径,再谈改功能。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进