简介:这是一套基于Python和Flask框架的豆瓣电影爬虫采集与分析可视化系统毕业设计源码,适合计算机相关专业用于毕业设计、期末大作业或课程设计。项目实现了从豆瓣电影页面爬取数据、清洗存储到后端分析,并通过Web界面进行可视化展示的完整流程,评审得分98分,本地编译运行通过。资源包共110个文件,涵盖Python后端逻辑、HTML/CSS/JavaScript前端页面、数据库文件、配置文件及说明文档等,压缩包仅6.25MB,结构清爽便于快速部署与二次开发。前端基于Bootstrap构建响应式界面,后端包含爬虫脚本与数据处理模块,并提供数据可视化图表。目前已有241人学习下载。交付内容包含可运行源码、前端静态资源、数据文件以及必要的使用说明,可帮助学习者直观理解爬虫与Flask Web开发的结合方式,适合希望完成高质量课设或毕业设计的学生参考。
1. 用Python+Flask把豆瓣电影爬虫做成能答辩的可视化系统
豆瓣电影Top250是很多人的第一个爬虫目标,但真正把它做成毕业设计,难点不在爬,而在把采集、清洗、存储、接口、可视化串成一条完整的链路。标题里的“Python+flask”划定了技术选型:爬虫负责拿数据,Flask在中间做Web服务层,可视化把数据变成能展示的图表。这种结构背后是毕业设计最常见的三层架构——数据采集层、服务层、展示层。适合的人群很明确:需要交代码和文档的本科生、想把这套流程迁移到其他站点的爬虫新手,还有想搞清楚Flask和ECharts如何配合的Web开发者。这篇文章按我自己实现这套系统的顺序来写,从反爬参数到SQLite入库,再到Flask接口和图表渲染,最后落到排错和答辩准备。
2. 先说采集:豆瓣电影Top250的爬虫方案与反爬参数设置
2.1 选requests而不是Scrapy:毕业设计场景的性价比判断
很多教程一上来就推Scrapy,但对“基于Python+flask豆瓣电影爬虫采集与分析可视化系统”这样的标题,requests反而是更合理的选择。原因有两层:第一,Top250总共只有10页数据,采集量在几百条量级,Scrapy的异步并发优势完全发挥不出来;第二,这个系统的核心展示层是Flask,requests写出来的代码更直观,评阅老师读代码时不需要理解爬虫框架的中间件机制。如果你后续想把采集规模扩大,再把requests代码改造成Scrapy的Spider也不难,接口逻辑是通用的。
另外还要决定用什么解析库。常见做法是BeautifulSoup加lxml解析器,或者直接用lxml的xpath。对于豆瓣这种HTML结构相对稳定的页面,xpath的表达式写起来更紧凑,而且lxml的解析速度明显快于纯Python的html.parser。我一般用lxml,后面所有解析示例都按这个来。
2.2 豆瓣Top250的URL规律:start参数与翻页控制
豆瓣Top250的列表页遵循一个固定规律:每页显示25条,通过URL里的start参数控制偏移量。第一页是start=0,第二页是start=25,第三页是start=50,以此类推。拿到这个规律后,翻页就变成了一个循环:
import requests from lxml import html base_url = "https://movie.douban.com/top250" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://movie.douban.com/top250" } for page in range(10): start = page * 25 resp = requests.get(base_url, params={"start": start, "filter": ""}, headers=headers, timeout=10) print(f"第{page + 1}页,状态码: {resp.status_code}")这里params参数是关键。requests会把{"start": start, "filter": ""}自动编码成查询字符串拼到URL后面,不需要手动做字符串拼接。timeout=10是必写的,否则遇到网络抖动时请求会一直挂住,程序卡死在循环里。每页抓取之间建议加一个time.sleep(2),既是对目标站点的基本礼貌,也是降低触发反爬概率的最简单手段。
2.3 请求头伪装与限速:User-Agent、Referer、Cookie的取舍
豆瓣的常规反爬策略并不复杂,主要看请求头是否像真实浏览器。最需要伪装的是User-Agent,其次是Referer。UA的格式要完整,不能只写python-requests这种默认值,至少带操作系统信息和浏览器版本。Referer填首页地址,避免请求被识别为孤立请求。
2.3.1 请求头参数对照表
| 参数 | 推荐值 | 作用 |
|---|---|---|
| User-Agent | 带Windows/macOS标识的浏览器UA | 绕过最基础的UA检测 |
| Referer | https://movie.douban.com/top250 | 模拟从站内页面跳转过来的请求 |
| Cookie | 浏览器里复制的一段完整Cookie | 解决部分时段验证码提前触发的问题 |
| timeout | 10秒 | 避免连接挂起拖死采集进程 |
Cookie不需要专门处理,直接在浏览器里登录豆瓣后,从开发者工具的请求头里复制粘贴到代码中即可。比较稳妥的做法是把Cookie、UA这些放到一个配置文件或环境变量里,代码里用os.getenv()读取,而不是写死在脚本里,这样以后换机器跑不用改代码。
2.4 清洗与SQLite入库:编码是第一个坑
解析页面时最容易翻车的地方是编码。豆瓣页面是UTF-8编码,但如果直接用resp.text,requests会根据响应头猜测编码,偶尔会猜错导致中文乱码。保险的做法是直接用resp.content解码:
parser = html.fromstring(resp.content.decode("utf-8")) items = parser.xpath('//div[@class="item"]') movie_list = [] for item in items: title = item.xpath('.//span[@class="title"]/text()')[0] rating = item.xpath('.//span[@class="rating_num"]/text()')[0] people = item.xpath('.//div[@class="star"]/span[4]/text()')[0] quote = item.xpath('.//p[@class="quote"]/span/text()') movie_list.append({ "title": title, "rating": float(rating), "people": int(people.replace("人评价", "")), "quote": quote[0] if quote else "" })解码逻辑:先取原始字节流resp.content,再按UTF-8显式解码,彻底绕开resp.text的编码猜测逻辑。清洗的重点有两个,一个是人评价这个字符串要去掉才能转成整数,另一个是quote字段不是每条都有,要用列表判断兜底,否则取下标会抛IndexError。数据入库用Python内置的sqlite3模块就够了,不需要额外引入SQLAlchemy,毕业设计项目用标准库能少装一个依赖,运行环境出问题的概率也小。
3. Flask后端:把爬虫结果变成可查询的Web接口
3.1 项目结构与爬虫模块的边界划分
爬虫代码和Flask代码不能混在一个文件里。我见过很多学生把requests.get直接写在路由函数里,这样每次打开页面都重新抓一遍,速度慢且容易被封。合理的结构是把爬虫写成一个独立的模块,只在需要更新数据时才执行,Flask只负责读数据库:
douban_project/ ├── app.py # Flask入口 ├── spider.py # 爬虫采集模块 ├── db.sqlite3 # SQLite数据库文件 └── templates/ └── index.html # 可视化页面模板采集入库和Web服务在这个结构里被拆开了,spider.py跑一次是刷新数据,app.py启动后永远只做查询和渲染。这样设计还有一个好处:采集频率可以被明确控制,不会因为用户多次刷新页面导致请求风暴。如果你想做定时刷新,在spider.py里加一个while True加time.sleep(86400)的循环即可,但毕业设计通常不需要。
3.2 用Flask暴露电影查询接口
Flask部分最核心的接口是“按条件筛选电影”。常见做法是不用render_template直出HTML,而是先写一个JSON接口,前端页面通过fetch调用。这样分层清晰,也方便你在浏览器里直接验证接口返回的数据内容:
from flask import Flask, jsonify, request import sqlite3 app = Flask(__name__) def query_movies(rating_min=0, rating_max=10, limit=20): conn = sqlite3.connect("db.sqlite3") conn.row_factory = sqlite3.Row sql = """ SELECT title, rating, people, quote FROM movies WHERE rating >= ? AND rating <= ? ORDER BY rating DESC LIMIT ? """ rows = conn.execute(sql, (rating_min, rating_max, limit)).fetchall() conn.close() return [dict(row) for row in rows] @app.route("/api/movies") def movies_api(): rating_min = request.args.get("rating_min", default=0, type=float) rating_max = request.args.get("rating_max", default=10, type=float) limit = request.args.get("limit", default=20, type=int) return jsonify(query_movies(rating_min, rating_max, limit)) if __name__ == "__main__": app.run(debug=True)逻辑说明:request.args.get从查询字符串中读取参数,default指定默认值,type指定类型转换。这里用了type=float和type=int,如果用户传了非法值,Flask会把参数置为默认值而不是报500错误。sqlite3.Row让查询结果可以按列名取值,dict(row)转成普通字典后才能被jsonify正确序列化。
3.3 必调参数:JSON中文编码、请求方式与debug模式
Flask有几个设置项在这个项目里必须处理,否则会踩坑。第一个是JSON中文编码。Flask 2.3版本之后默认app.json.ensure_ascii为False,中文会正常显示;如果你用的是旧版本,需要在创建app后设置app.config["JSON_AS_ASCII"] = False,否则接口返回的是\uXXXX格式的Unicode转义序列,前端图表解析倒没问题,但浏览器直接看接口很难受。
第二个是请求方式。查询接口只允许GET足够,不需要methods=["GET", "POST"],限定GET反而能让接口语义更清晰。第三个是debug=True。本地开发阶段必须开,改代码自动重载,报错时能在页面看到完整的堆栈。但注意:app.run(debug=True)只能用于本机调试,交文档或部署演示时不要带这个参数,改成app.run(host="0.0.0.0", port=5000),这样能在同局域网的其他设备上访问演示。
3.4 按年份和类型做二次聚合查询
Top250数据里,评分和评价人数是数值型字段,可以直接用于区间过滤,但年份和类型是以字符串形式存在的,比如“1994”和“剧情 / 爱情”。如果要做“某一年评分最高的电影”或者“某类型电影的平均评分”,最方便的办法是入库时就把这些字段拆好,而不是每次查询时处理字符串。在SQLite里加两个字段:year INTEGER和genre TEXT,入库时正则匹配出年份,类型按/分割后存第一项。
import re def parse_movie(raw): year_match = re.search(r"(\d{4})", raw["year_text"]) genre = raw["genre_text"].split("/")[0].strip() return { "title": raw["title"], "rating": raw["rating"], "year": int(year_match.group(1)) if year_match else 0, "genre": genre }拆字段的好处到后面可视化阶段会体现出来:类型分布统计完全可以用一条GROUP BY genre的SQL完成,不需要把全表数据拉到Python里再数数。如果你用的豆瓣数据源字段名不完全一样,思路是一样的——入库之前把需要聚合分析的维度全部拆成独立列,宁可多存一列,也不要让SQL写成LIKE "%剧情%"这种性能很差的查询。
4. 可视化:ECharts图表与Flask模板的渲染实战
4.1 直出HTML还是前端拉接口:两种方案的取舍
可视化部分有两种常见做法。第一种是Flask用render_template把数据直接渲染进模板的<script>标签里,页面加载时图表立即显示;第二种是模板里放空的<div>,页面加载后用JavaScript通过fetch从/api/movies拿数据再喂给ECharts。我推荐第二种,理由很实际:接口层已经写好了,前端拉数据能让接口和数据展示解耦,调试接口不用刷新页面,图表渲染失败时也能区分是数据问题还是前端问题。
模板里用cdn方式引入ECharts。不需要下载js文件到本地,直接引https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js,但要注意演示机器必须联网,如果答辩现场网络不稳,提前下载echarts.min.js放到static目录更保险。
4.2 评分分布与类型Top10的聚合SQL
可视化图表对应的数据,用SQL聚合比用Python循环高效得多,也更能体现你对数据分析的掌握程度。评分分布用分段统计,在SQL里写CASE WHEN表达式:
SELECT CASE WHEN rating >= 9 THEN '9分以上' WHEN rating >= 8 THEN '8到9分' WHEN rating >= 7 THEN '7到8分' ELSE '7分以下' END AS rating_level, COUNT(*) AS count FROM movies GROUP BY rating_level;逻辑说明:这条SQL把评分分成四段,每段统计电影数量,返回结果直接就是饼图或柱状图需要的数据格式。GROUP BY的对象是CASE表达式本身,SQLite允许按别名分组,但为了兼容性,这里重复写了完整表达式。类型分布类似,SELECT genre, COUNT(*) FROM movies GROUP BY genre ORDER BY count DESC LIMIT 10即可,注意genre字段已经拆过,不需要处理字符串拼接。
4.3 ECharts图表与Flask路由的配合
Flask端再加两个路由,一个返回评分分布,一个返回类型Top10,前端一次性拉两个接口:
@app.route("/api/rating_dist") def rating_dist(): conn = sqlite3.connect("db.sqlite3") rows = conn.execute(""" SELECT CASE WHEN rating >= 9 THEN '9分以上' WHEN rating >= 8 THEN '8到9分' WHEN rating >= 7 THEN '7到8分' ELSE '7分以下' END AS level, COUNT(*) AS count FROM movies GROUP BY level """).fetchall() conn.close() return jsonify([{"level": r[0], "count": r[1]} for r in rows])前端模板里这样接入:
fetch('/api/rating_dist') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('ratingChart')); chart.setOption({ title: { text: '豆瓣Top250评分分布' }, tooltip: {}, xAxis: { data: data.map(d => d.level) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.map(d => d.count) }] }); });关键点在于data.map(d => d.level)和data.map(d => d.count),这一步把接口返回的对象数组拆成了两个纯数组,正好对齐ECharts的xAxis.data和series.data结构。如果接口字段名和前端取的对不上,图表会没有任何报错地显示空白,这是最常见的排错点。
5. 排错顺序与答辩准备:这个系统最容易翻车的几个位置
5.1 爬虫返回登录页或验证码时的排查顺序
采集阶段最容易遇到的现象是页面能打开但解析不到电影条目。先打印resp.status_code和len(resp.content),对比正常值;然后看返回内容里有没有“登录”或“验证”关键词。常规处理顺序是:先换更完整的UA和Referer,再补Cookie,最后把time.sleep(2)的间隔拉到5秒。整套系统里爬虫只需要跑一次,跑慢一点完全不影响使用。注意不要用代理池之类的重型方案,毕业设计用不上,也涉及合规风险,保持低频率采集就够。
5.2 接口有数据但图表空白:三个必查位置
| 排查位置 | 检查方法 | 常见原因 |
|---|---|---|
| 接口返回 | 浏览器直接访问/api/rating_dist | 数据格式不对,字段名拼写错误 |
| DOM容器 | 检查<div>是否有高度 | ECharts容器默认高度为0,需要显式style="height:400px" |
| JS引入顺序 | 开发者工具Console看报错 | echarts.min.js没有加载成功 |
容器高度为0是高频错误。ECharts初始化时如果容器不可见或高度为0,图表渲染出来是一张空白图,且控制台不报错。给容器一个明确的高度是成本最低的解决方案。
5.3 答辩时怎么讲这个系统的亮点
评阅老师最关注的不是爬虫部分,而是“分析”和“可视化”这两个高阶词。你可以把第4章的聚合SQL作为讲解重点,说清楚为什么要分段统计评分、为什么类型要提前拆列。准备一张运行截图,展示接口返回的JSON和前端图表同时出现在一个页面上,这比讲十页PPT都有说服力。系统设计时留一个自定义筛选条件的扩展点——按评分区间过滤,这就是你工作量的一部分,也是答辩时最能聊的地方。
本文还有配套的精品资源,点击获取