ARTICLE DETAIL

资讯详情

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

从豆瓣电影爬虫到可视化大屏:毕业设计核心技术全解析

从豆瓣电影爬虫到可视化大屏:毕业设计核心技术全解析 简介基于PythonFlask的豆瓣电影爬虫采集与分析可视化系统毕业设计源码面向计算机相关专业学生适合作为毕业设计、课程设计或期末大作业的完整参考。项目整体难度适中源码均经本地编译可正常运行评审分达到98分内容通过助教老师审定能够直接支撑同类课题的快速落地也方便在此基础上进行答辩演示与功能改进。压缩包共110个文件包体约6.25MB核心包括5个Python爬虫与后端脚本、6个HTML页面模块、16个CSS和22个JavaScript前端资源另配有分析图表图片、数据库文件及依赖字体样式完整覆盖豆瓣电影数据采集、清洗存储、接口服务和可视化展示全链路。已有241人学习下载借助该项目可清晰理解爬虫与Flask Web的协同机制也可在保留主框架的基础上扩展评分预测、排行榜分析等功能从而形成一份有数据支撑、有界面呈现的高质量毕业设计成果。1. 为什么拿豆瓣电影做爬虫可视化毕业设计如果你正准备交一份能当场运行的毕业设计最怕的不是功能少而是评阅老师一问你“数据怎么来的、反爬怎么应对、可视化数据怎么传的”你答不上来。豆瓣电影是这类题目里最稳的靶场它的列表页和详情页结构固定既不涉及登录态也没有高强度验证码但又有UA检测、请求频率限制和编码细节刚好能把“爬虫采集、数据存储、Flask后端、可视化展示”四层完整链路串起来。更重要的是这份源码里用到的requests、BeautifulSoup、pandas、Flask、ECharts都是面试常问的刚需技术栈整套系统做完既能演示又能讲清原理适合作为期末大作业或毕业设计的骨架再扩展。2. 爬虫层设计requests、BeautifulSoup与限速策略2.1 为什么选requestsBeautifulSoup而不是Scrapy很多人在选题时会纠结要不要上Scrapy。单看豆瓣Top250这个采集规模一共就250条列表数据加上每部电影的详情页用Scrapy的异步并发优势完全发挥不出来反而会引入中间件、Pipeline、Item等一堆概念答辩时不好自圆其说。常见做法是用requests做HTTP请求BeautifulSoup做HTML解析这套组合的代码行数少、断点调试直观遇到反爬也更容易在请求层做控制。如果你的毕设需要从几十万部电影里全量采集那才需要迁移到Scrapy或requests-html。我这里做的取舍是明确标注“本项目面向中小规模采集”并把请求频率、重试机制、UA池做成独立模块将来要接Scrapy只需替换下载中间件即可。2.2 构建带随机UA和限速的请求器豆瓣对没有User-Agent的请求直接返回418对短时间内高频请求会进入验证码流程。我一般会把请求器封装成一个类统一管理header和休眠时间代码是这样的import requests import random import time from fake_useragent import UserAgent class DoubanCrawler: def __init__(self, base_interval3.0): self.session requests.Session() self.base_interval base_interval self.ua UserAgent() def _random_headers(self): return { User-Agent: self.ua.random, Referer: https://movie.douban.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch(self, url, max_retry3): for attempt in range(max_retry): try: resp self.session.get( url, headersself._random_headers(), timeout(3, 8) ) if resp.status_code 200 and 检测到有异常请求 not in resp.text: return resp # 遇到反爬时退避时间拉长 time.sleep(self.base_interval * (attempt 2)) except requests.RequestException: time.sleep(self.base_interval) return None这里的参数设计对答辩很重要base_interval是基础请求间隔max_retry是最大重试次数timeout用元组分别表示连接超时和读取超时。UA由fake_useragent随机生成豆瓣无法通过特征锁定你的请求环境。注意检测到有异常请求这个字符串是豆瓣反爬页的典型标识用它判断比只看状态码可靠得多。2.3 解析列表页与详情页字段Top250列表页的数据结构这些年没大变过每个条目在ol.grid_view下的li里。解析示例如下from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, html.parser) items [] for li in soup.select(ol.grid_view li): rank li.select_one(.pic em).text title li.select_one(.hd .title).text rating li.select_one(.rating_num).text people_count li.select_one(.star span:last-child).text quote li.select_one(.quote .inq) items.append({ rank: int(rank), title: title.strip(), rating: float(rating), people_count: int(people_count.replace(人评价, )), quote: quote.text.strip() if quote else }) return items这里要特别提一下.star span:last-child它取到的是“1345832人评价”这段文本去掉“人评价”后转成整数才能在排序里参与计算。有些条目没有简介这里用条件表达式兜底避免AttributeError。详情页的解析思路类似但字段更多比如导演、主演、类型、制片国家/地区、上映日期这些字段会用于后面的聚合分析所以在爬取阶段就要统一成字典格式。2.4 反爬触发特征与限速参数对照豆瓣的反爬不是直接封IP而是逐步升级。把这个特征表放进毕设文档里能明显增加可信度特征常见原因应对策略返回418缺User-Agent或UA被识别随机UA池两次请求间几乎无间隔访问速度过快sleep随机2~5秒页面出现“检测到有异常请求”触发反爬阈值暂停10分钟切换代理长期高频采集后403IP被临时标记降低并发使用代理池请求异常连接失败豆瓣主动断开增加重试和指数退避注意不要完全固定间隔顺序还是要的。常见做法是time.sleep(random.uniform(2, 5))手动节流既让演示不至于太慢又不至于被秒杀。如果采集总量超过1000条建议把抓到的页面缓存到本地文件方便断点续采也避免网络抖动导致数据不完整。3. Flask后端把爬虫产出变成可查询的API3.1 数据持久化选型SQLite为什么够用爬虫拿到的是list of dicts如果直接丢给前端可视化数据和业务逻辑耦合太重。我建议先落库再出接口。数据库用SQLite还是MySQL是答辩时容易被追问的点。做一个简单对比维度SQLiteMySQL部署成本零配置Python内置需要安装服务并发写锁全局适合单机高并发写入能力强数据类型宽松严格毕设演示文件即库方便提交环境迁移麻烦毕业设计的数据量撑死几万行SQLite的查询性能完全够。如果你往后面试可以补一句“生产环境需要换成MySQLORM迁移也容易”。3.2 建表与批量写入建表时把影片URL、缩略图地址也存下来四是英文也用于存储数据。注意SQLite用INTEGER PRIMARY KEY AUTOINCREMENT但没有必要定义主键直接用rank做唯一索引就行。写入时用executemany。import sqlite3 def init_db(db_path): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS movie ( rank INTEGER PRIMARY KEY, title TEXT NOT NULL, rating REAL, people_count INTEGER, quote TEXT, year INTEGER, country TEXT, genre TEXT, url TEXT ) ) conn.commit() conn.close() def bulk_insert(db_path, movies): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.executemany( INSERT OR REPLACE INTO movie (rank, title, rating, people_count, quote, year, country, genre, url) VALUES (:rank, :title, :rating, :people_count, :quote, :year, :country, :genre, :url) , movies) conn.commit() conn.close()这里用INSERT OR REPLACE爬虫跑第二遍时不会因为主键冲突中断更新。参数使用命名占位符(:rank)和Python字典的键对齐避免手动拼接SQL导致注入风险。国内电影和国外电影的年份、国家字段要单独清洗否则会出现类型错误存不进去。3.3 用Flask提供查询接口Flask部分不建议写大量业务逻辑而是把查询参数暴露成RESTful接口。下面是一个支持排序和筛选的示例from flask import Flask, jsonify, request from flask_cors import CORS import sqlite3 app Flask(__name__) CORS(app) def query_db(sql, params): conn sqlite3.connect(movie.db) conn.row_factory sqlite3.Row cur conn.execute(sql, params) rows cur.fetchall() conn.close() return [dict(row) for row in rows] app.route(/api/movies) def movies(): sort_by request.args.get(sort_by, rating) order request.args.get(order, desc) min_rating request.args.get(min_rating, 0, typefloat) genre request.args.get(genre, ) allowed {rating, people_count, year, rank} if sort_by not in allowed: return jsonify({error: invalid sort_by}), 400 sql SELECT * FROM movie WHERE rating ? params [min_rating] if genre: sql AND genre LIKE ? params.append(f%{genre}%) sql f ORDER BY {sort_by} {order} sql LIMIT ? OFFSET ? params.append(request.args.get(limit, 50, typeint)) params.append(request.args.get(offset, 0, typeint)) return jsonify(query_db(sql, params))代码里的安全细节值得提sort_by先和允许的字段白名单比对防止SQL注入min_rating用Flask的typefloat做类型转换不合法的请求参数直接返回400。数据库连接每次请求都新建毕设够用但答辩时你可以说“正式场景会用连接池”。这里ORDER BY后面没有用占位符因为字段名无法参数化所以白名单是必要防御。3.4 解决中文乱码与JSON安全Flask的jsonify默认会设置utf-8和application/json一般情况下没有乱码问题。但如果你是拼接JSON字符串或者使用json.dumps必须显式设置from flask import Response import json resp_data json.dumps({movies: items}, ensure_asciiFalse) return Response(resp_data, content_typeapplication/json; charsetutf-8)ensure_asciiFalse是关键否则中文会变成\u7535\u5f71。前端拿到后虽然能解析但调试时根本无法直读。另一个坑是SQLite从表中读出TEXT字段时如果库文件不是UTF-8编码会报UnicodeDecodeError。所以在爬虫写入之前统一对字符串做解码和strip。4. 数据清洗与可视化pandas聚合后对接ECharts4.1 从数据库到DataFrame接口数据给前端后前端只能做简单的展示如果要做分布统计放在后端更合理。用pandas读取SQLite非常顺手import pandas as pd df pd.read_sql_query(SELECT * FROM movie, sqlite3.connect(movie.db)) print(df.info())read_sql_query会把SQLite的列自动映射成DataFrame列。常见问题是豆瓣电影年份字段里可能混入“2024-12-20”这样的完整上映日期或者国家字段是“中国大陆 / 中国香港”这些都要提前清洗。我是这样处理的df[year] df[year].astype(str).str.extract(r(\d{4})).astype(float) df[[country_1, country_2]] df[country].str.split( / , n1, expandTrue)str.extract取第一个四位数字作为年份过滤掉空值。国家字段多人种只取第一个主制片国家否则聚合时每个组合都会变成一个单独分组。4.2 按年份和评分区间聚合可视化大屏一般至少需要两个维度每年电影数量和平均评分以及评分区间分布。聚合代码# 按年份统计数量和平均分 year_stats df.groupby(year).agg( movie_count(title, count), avg_rating(rating, mean) ).reset_index() # 评分区间分布 bins [0, 5, 6, 7, 8, 9, 10] labels [0-5, 5-6, 6-7, 7-8, 8-9, 9-10] df[rating_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) rating_dist df[rating_bin].value_counts().sort_index()groupby后的聚合要用agg显式指定每一列的计算方式避免pandas未来版本的行为变化。pd.cut的rightFalse表示区间左闭右开9分进到“9-10”档而不是“8-9”档。这些区间定义和数据边界要在答辩时能讲清楚。4.3 组装ECharts可用的JSON结构ECharts接受的数据格式一般是{categories: [], series: []}所以要把DataFrame转成记录列表def df_to_echarts(df, x_col, y_col): records df[[x_col, y_col]].dropna().to_dict(records) return { categories: [r[x_col] for r in records], series: [r[y_col] for r in records] }调用时to_dict(records)把每一行转成{col: value}字典比to_json更可控也不会出现NaN被转换成null的问题。如果序列里有NaNECharts会渲染成空点所以dropna是必要的。这里的键名要跟前端图表组件的字段名完全一致比如categories和series否则图表完全不显示。4.4 前端页面用AJAX拉取数据Flask渲染模板页面加载后用fetch请求统计接口再渲染ECharts。常见套路是这样!DOCTYPE html html head meta charsetutf-8 script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth: 100%; height: 500px;/div script fetch(/api/stats/year) .then(r r.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: data.categories }, yAxis: { type: value }, series: [{ type: line, data: data.series }] }); }); /script /body /html注意ECharts的CDN链接要写具体版本号锁版本避免未来更新导致语法不兼容。初始化图表前确保DOM元素已经存在所以script放在body末尾。如果你把script放在head里执行document.getElementById(chart)会拿不到元素需要在window.onload里延迟调用。5. 部署与排错编码、端口、图表不渲染的三个高频坑这一章不做面面俱到的部署教程只挑三个实际运行中容易卡住的地方每一个都直接影响期末答辩演示效果。第一个坑是Windows下运行Flask时端口被占用。默认5000端口很容易被其他开发程序占住报错信息是OSError: [Errno 98] Address already in use。常见做法是在启动时指定端口python app.py --port5001或者在代码里写死if __name__ __main__: app.run(host0.0.0.0, port5001, debugFalse)答辩现场一定要用debugFalse因为调试模式会开启交互式调试器暴露系统信息而且代码修改后自动重载会让演示中途中断。第二个坑是浏览器访问接口时返回中文JSON乱码。前面已经说了后端要设置ensure_asciiFalse但还有一个容易忽略的点数据库连接时没有指定UTF-8。SQLite不需要指定字符集但如果你从MySQL迁移过来连接参数里要加charsetutf8mb4。在Flask响应头里可以加一句确保浏览器强制用UTF-8解析app.after_request def add_header(response): response.headers[Content-Type] application/json; charsetutf-8 return response第三个坑是ECharts图表不渲染页面却没有任何报错。最常见的两个原因是图表容器高度为0或者数据JSON字段名不匹配。容器被父级div收缩时即使设了width: 100%高度不到50px图表会静默失败。解决办法是在echarts.init之前打印document.getElementById(chart).clientHeight确认不是0。字段名的问题可以用浏览器的Network面板看fetch响应对照categories和series键名凡是大小写不一致都得不到期待的输出。最后一个验证技巧把所有统计数据接口拼成一个页面在打开可视化首页前先访问/api/stats/year把返回JSON格式化后肉眼核对是否有空年份和排序混乱。豆瓣Top250跨了几十年早期年份电影数量少平均值波动很大适合用面积图而不是折线图去强调趋势。你还可以把接口拼出print(movie_count)放在启动脚本里确保爬虫、数据库、服务三层链路没有断裂然后再打开大屏页面整个演示就会顺畅很多。本文还有配套的精品资源点击获取
返回列表