
简介这是一套基于Python和Flask框架的豆瓣电影爬虫采集与分析可视化系统毕业设计源码适合计算机相关专业用于毕业设计、期末大作业或课程设计。项目实现了从豆瓣电影页面爬取数据、清洗存储到后端分析并通过Web界面进行可视化展示的完整流程评审得分98分本地编译运行通过。资源包共110个文件涵盖Python后端逻辑、HTML/CSS/JavaScript前端页面、数据库文件、配置文件及说明文档等压缩包仅6.25MB结构清爽便于快速部署与二次开发。前端基于Bootstrap构建响应式界面后端包含爬虫脚本与数据处理模块并提供数据可视化图表。目前已有241人学习下载。交付内容包含可运行源码、前端静态资源、数据文件以及必要的使用说明可帮助学习者直观理解爬虫与Flask Web开发的结合方式适合希望完成高质量课设或毕业设计的学生参考。1. 用PythonFlask把豆瓣电影爬虫做成能答辩的可视化系统豆瓣电影Top250是很多人的第一个爬虫目标但真正把它做成毕业设计难点不在爬而在把采集、清洗、存储、接口、可视化串成一条完整的链路。标题里的“Pythonflask”划定了技术选型爬虫负责拿数据Flask在中间做Web服务层可视化把数据变成能展示的图表。这种结构背后是毕业设计最常见的三层架构——数据采集层、服务层、展示层。适合的人群很明确需要交代码和文档的本科生、想把这套流程迁移到其他站点的爬虫新手还有想搞清楚Flask和ECharts如何配合的Web开发者。这篇文章按我自己实现这套系统的顺序来写从反爬参数到SQLite入库再到Flask接口和图表渲染最后落到排错和答辩准备。2. 先说采集豆瓣电影Top250的爬虫方案与反爬参数设置2.1 选requests而不是Scrapy毕业设计场景的性价比判断很多教程一上来就推Scrapy但对“基于Pythonflask豆瓣电影爬虫采集与分析可视化系统”这样的标题requests反而是更合理的选择。原因有两层第一Top250总共只有10页数据采集量在几百条量级Scrapy的异步并发优势完全发挥不出来第二这个系统的核心展示层是Flaskrequests写出来的代码更直观评阅老师读代码时不需要理解爬虫框架的中间件机制。如果你后续想把采集规模扩大再把requests代码改造成Scrapy的Spider也不难接口逻辑是通用的。另外还要决定用什么解析库。常见做法是BeautifulSoup加lxml解析器或者直接用lxml的xpath。对于豆瓣这种HTML结构相对稳定的页面xpath的表达式写起来更紧凑而且lxml的解析速度明显快于纯Python的html.parser。我一般用lxml后面所有解析示例都按这个来。2.2 豆瓣Top250的URL规律start参数与翻页控制豆瓣Top250的列表页遵循一个固定规律每页显示25条通过URL里的start参数控制偏移量。第一页是start0第二页是start25第三页是start50以此类推。拿到这个规律后翻页就变成了一个循环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: }, headersheaders, timeout10) print(f第{page 1}页状态码: {resp.status_code})这里params参数是关键。requests会把{start: start, filter: }自动编码成查询字符串拼到URL后面不需要手动做字符串拼接。timeout10是必写的否则遇到网络抖动时请求会一直挂住程序卡死在循环里。每页抓取之间建议加一个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检测Refererhttps://movie.douban.com/top250模拟从站内页面跳转过来的请求Cookie浏览器里复制的一段完整Cookie解决部分时段验证码提前触发的问题timeout10秒避免连接挂起拖死采集进程Cookie不需要专门处理直接在浏览器里登录豆瓣后从开发者工具的请求头里复制粘贴到代码中即可。比较稳妥的做法是把Cookie、UA这些放到一个配置文件或环境变量里代码里用os.getenv()读取而不是写死在脚本里这样以后换机器跑不用改代码。2.4 清洗与SQLite入库编码是第一个坑解析页面时最容易翻车的地方是编码。豆瓣页面是UTF-8编码但如果直接用resp.textrequests会根据响应头猜测编码偶尔会猜错导致中文乱码。保险的做法是直接用resp.content解码parser html.fromstring(resp.content.decode(utf-8)) items parser.xpath(//div[classitem]) movie_list [] for item in items: title item.xpath(.//span[classtitle]/text())[0] rating item.xpath(.//span[classrating_num]/text())[0] people item.xpath(.//div[classstar]/span[4]/text())[0] quote item.xpath(.//p[classquote]/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_min0, rating_max10, limit20): 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, default0, typefloat) rating_max request.args.get(rating_max, default10, typefloat) limit request.args.get(limit, default20, typeint) return jsonify(query_movies(rating_min, rating_max, limit)) if __name__ __main__: app.run(debugTrue)逻辑说明request.args.get从查询字符串中读取参数default指定默认值type指定类型转换。这里用了typefloat和typeint如果用户传了非法值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反而能让接口语义更清晰。第三个是debugTrue。本地开发阶段必须开改代码自动重载报错时能在页面看到完整的堆栈。但注意app.run(debugTrue)只能用于本机调试交文档或部署演示时不要带这个参数改成app.run(host0.0.0.0, port5000)这样能在同局域网的其他设备上访问演示。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/echarts5/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需要显式styleheight:400pxJS引入顺序开发者工具Console看报错echarts.min.js没有加载成功容器高度为0是高频错误。ECharts初始化时如果容器不可见或高度为0图表渲染出来是一张空白图且控制台不报错。给容器一个明确的高度是成本最低的解决方案。5.3 答辩时怎么讲这个系统的亮点评阅老师最关注的不是爬虫部分而是“分析”和“可视化”这两个高阶词。你可以把第4章的聚合SQL作为讲解重点说清楚为什么要分段统计评分、为什么类型要提前拆列。准备一张运行截图展示接口返回的JSON和前端图表同时出现在一个页面上这比讲十页PPT都有说服力。系统设计时留一个自定义筛选条件的扩展点——按评分区间过滤这就是你工作量的一部分也是答辩时最能聊的地方。本文还有配套的精品资源点击获取