ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从爬虫到Spark可视化:豆瓣电影Top250数据分析全链路实战

2026/9/16 18:15:31 拓冰建站 浏览量
从爬虫到Spark可视化:豆瓣电影Top250数据分析全链路实战 简介这是一份基于豆瓣电影爬虫与Spark数据分析可视化的毕设源码案例面向计算机相关专业学生、毕业设计开发者及大数据入门学习者。项目完整覆盖数据采集、清洗、统计分析与可视化展示流程提供Java/Python源码、配置文件、SQL脚本及说明文档可实现从爬虫抓取到Spark处理再到前端展示的全链路方案。包内共242个文件以XML配置、Java源码、CSS样式、HTML页面、JAR依赖及CSV结果文件等为主同时包含Spark作业输出文件其中Java类负责后端逻辑XML/YAML为配置声明CSV与part-r文件为计算结果压缩包仅5.64MB结构清晰便于按模块查阅。目前已有519人学习下载可用于毕设参考、课程设计或项目初期演示代码经测试可成功运行支持二次开发与功能扩展是理解豆瓣数据生态及分布式计算的良好实践素材。1. 这个毕设题目的本质是串好一条从爬虫到Spark再到可视化大屏的数据链路你真以为这个题目的难点在爬虫豆瓣电影Top250用requests加个UA就能抓下来评分、导演、类型、主演这些字段半小时能跑通。真正让一届又一届学生卡住的是后半段数据抓下来之后怎么进Spark做分析分析结果怎么落到可视化页面上以及最后答辩时老师问一句“你这个Spark到底干了什么”你讲不讲得清。这个项目实际上考察的是完整数据链路采集层负责拿数据计算层负责出结论展示层负责让结论可见。适合正在做毕设选题、或者想拿一个全栈数据项目填充简历的开发者。下面我按自己会做的方案把爬虫、Spark、可视化三层拆开讲末尾补一套文档说明的写法让“源码文档”这套交付物能真正站得住。2. 豆瓣电影爬虫requests抓取、反爬识别与并发采集的成形方案2.1 爬虫选型为什么用requests多线程而不是Scrapy常见的说法是“毕设用Scrapy显得专业”但豆瓣电影这个体量根本用不着分布式爬虫。几百部电影的元数据用requests加线程池几十秒就能跑完Scrapy的安装、中间件配置、Pipeline维护反而拖慢进度。我更推荐requests配合concurrent.futures做并发控制代码总量小每行都能在答辩时讲清楚也更贴合“毕业设计源码案例”这类项目的定位——评分老师要看到的是你对关键点的理解不是框架堆砌。抓取方按说是清晰且固定的先拿分类页或Top250列表页解析出每个电影详情页的链接再进详情页拿字段。豆瓣最常做的是请求头校验和频率限制只要控制好并发数、伪装好UA和Referer成功率很高。2.2 反爬识别与请求头伪装处理302跳转、封IP和验证码豆瓣的反爬有两个明显信号请求返回302跳转到sec.douban.com说明触发风控连续高频请求会返回418或验证码页面。应对手段分三类优先级从高到低排列如下。反爬手段触发特征对应策略成本请求头校验403、418、移动端页面补全UA、Referer、Accept-Language低频率限制302跳转验证页随机延时0.5~2秒控制并发上限低IP限流大量请求后统一跳转维护代理池失败自动切换出口IP高第一类是最容易做也最容易被忽略的。浏览器直接访问能打开代码请求就是418几乎都是请求头缺东西。推荐自带一个完整的请求头配置Accept、Accept-Language、Connection都要有再把UA伪装成Chrome稳定版。再一个是Cookie处理requests.Session能自动管理Cookie第一次访问拿cookie后续请求带着走比手动拼Header省事得多。302跳转的处理逻辑是检查响应状态码和最终URL发现被重定向就sleep并重试。重试超过3次直接丢弃该条记录不要死磕单条数据。2.3 并发采集的落地代码与数据落盘策略# -*- coding: utf-8 -*- 豆瓣电影爬虫并发采集详情页并落盘为CSV import csv import random import time import requests from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor, as_completed HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_detail(url, session): 采集单个电影详情页失败时返回None for attempt in range(3): try: resp session.get(url, headersHEADERS, timeout10) if resp.url.startswith(https://sec.douban.com): time.sleep(random.uniform(1, 2)) continue if resp.status_code ! 200: time.sleep(random.uniform(0.5, 1)) continue soup BeautifulSoup(resp.text, html.parser) title soup.find(span, propertyv:itemreviewed).text.strip() score soup.find(strong, class_rating_num).text.strip() return {title: title, score: score, url: url} except requests.RequestException: time.sleep(1) return None def collect_movie_urls(list_url, session): 从列表页解析出详情页链接 resp session.get(list_url, headersHEADERS, timeout10) soup BeautifulSoup(resp.text, html.parser) links [a[href] for a in soup.select(div.hd a) if a.get(href)] return links session requests.Session() urls collect_movie_urls(https://movie.douban.com/top250, session) results [] with ThreadPoolExecutor(max_workers5) as pool: futures {pool.submit(fetch_detail, u, session): u for u in urls} for future in as_completed(futures): data future.result() if data: results.append(data) with open(douban_movies.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[title, score, url]) writer.writeheader() writer.writerows(results)逻辑说明一下Session复用连接池能减少TCP握手开销ThreadPoolExecutor把并发数锁死在5这是豆瓣比较安全的阈值重试逻辑放在详情页采集函数内部列表页失败则直接放弃。这样设计的好处是单个请求失败不影响整体而且代码短答辩时能逐行解释。爬虫里的一个重要原则数据字段宁多勿少。豆瓣详情页能拿到的字段很多——评分人数、制片国家、语言、上映日期、片长、导演、编剧、主演、类型、IMDb链接——建议全量采集。后期Spark分析时你会发现预先多存一个字段能省掉一次回源抓取。3. Spark分析层集群环境安装、DataFrame建模与执行参数调优3.1 Spark单机与集群环境的安装选型毕设环境里跑Spark最省事的方案是Local模式配合spark-submit脚本其次才是standalone集群。Local模式允许你用local[*]把本机全部CPU核心用作计算资源不需要配置Master和Worker几十MB的数据量跑起来毫无压力。但要注意写“Spark集群搭建”式履历时光跑Local会被追问“你了解集群吗”所以至少要懂理论上的配置方式而不是只会启动一个交互式shell。环境安装按版本对应关系来我常用的是Spark 3.3.x配Python 3.8以上环境Java选8或11。解压之后设置环境变量SPARK_HOME再把bin目录加进PATH。安装完成后用pySpark写一段WordCount验证环境可用类似Spark自带的示例程序跑通即证明环境没问题。3.2 用DataFrame做电影评分与类型分析的核心代码from pyspark.sql import SparkSession from pyspark.sql.functions import col, avg, count, desc, split, explode, round spark SparkSession.builder \ .appName(DoubanMovieAnalysis) \ .master(local[*]) \ .config(spark.sql.shuffle.partitions, 4) \ .getOrCreate() df spark.read.option(header, True) \ .option(encoding, utf-8) \ .csv(douban_movies_full.csv) # 类型字段是类似 剧情/爱情/奇幻 的格式拆开后按类型分组统计平均评分 df_with_genre df.withColumn(genre, explode(split(col(genres), /))) genre_stats df_with_genre.groupBy(genre) \ .agg( count(*).alias(movie_count), round(avg(score), 2).alias(avg_score) ) \ .orderBy(desc(avg_score)) genre_stats.show(20, truncateFalse) # 年份维度统计观察不同年份上映电影的平均评分与数量 year_stats df.groupBy(year) \ .agg( count(*).alias(movie_count), round(avg(score), 2).alias(avg_score) ) \ .orderBy(year) year_stats.write.mode(overwrite).json(output/year_stats.json)这里的explode与split配合是把一对多字段拆成多行的标准做法groupBy配合agg完成聚合orderBy控制输出顺序。spark.sql.shuffle.partitions设为4是因为小数据集的分区太多反而浪费调度开销。结果通过write.json落盘可视化层直接读取该目录即可。如果后续数据量变大还可以改用partitionBy(year)按年份分区存储查询单年数据时能省掉扫描全表的成本。Spark分析的要义是把“统计”变成“能解释的结论”。评分均值、数量分布、国家分布、评分与评分人数的相关性这四类分析足以支撑毕设的“数据分析”部分。再进一步可以看同一导演作品的平均分或者看类型数量随时间的变化趋势这些分析在代码上只是换一个groupBy字段。3.3 三个影响任务运行效率的参数与数据倾斜排查参数配置位置推荐值作用spark.sql.shuffle.partitionsSparkConf / submit脚本核数×2~3控制shuffle后分区数spark.executor.memoryspark-submit --executor-memory2g~4g每个Executor堆内存spark.driver.memoryspark-submit --driver-memory1g~2gDriver侧内存collect大结果时调大spark.serializerSparkConfKryoSerializer减少网络传输体积大数据量时效果明显执行任务时用spark-submit --master local[4] --driver-memory 2g --executor-memory 2g analyze.py这类命令提交比直接python analyze.py更容易控制资源。数据倾斜这个坑在电影数据分析里不常见但一旦出现groupBy某个类型导致某个分区数据量远大于其他分区现象是某个Task长时间运行、其他Task早已结束。解决方法是在groupBy之前给key加随机前缀打散或者改用broadcast join。毕设数据量不大遇到倾斜的概率低但能说出排查思路是加分项。4. 可视化层把Spark输出接进Web图表与结果验证4.1 前端选型ECharts配合Flask后端搭建可视化界面可视化方案有两条路一是把Spark分析结果导出成静态JSON用纯HTMLECharts展示二是Flask搭后端提供数据接口给前端动态加载。毕设答辩时静态页面更稳演示过程中不会因为接口挂了而翻车但动态加载看起来“更有工程感”。我一般折中Flask起一个轻量服务接口读Spark落盘的JSON返回给前端两者都占住。# app.py —— Flask读取Spark分析结果并暴露为接口 import json from flask import Flask, jsonify, render_template app Flask(__name__) app.route(/api/genre_stats) def genre_stats(): with open(output/genre_stats.json, r, encodingutf-8) as f: data [json.loads(line) for line in f] return jsonify(data) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)前端页面里用ECharts的init方法初始化一个DOM容器通过fetch调用这个接口拿数据再用setOption传入xAxis和series。需要注意的是Spark写出的JSON是一行一个对象读取时要逐行解析不能直接用json.load读整个文件。用spark.write.json时输出目录下会自动生成part文件接口读这个目录时要按行处理或者直接让Spark写出CSV再由Pandas转为前端友好的数组。常见的做法是性能优先选JSON开发省事选CSV配Pandas。4.2 分析结果从Spark写入MySQL再暴露成JSON接口的完整链路上面Flask读写本地JSON文件足够演示但完整工程一般会把Spark分析结果先写入MySQL再由后端查询返回给前端。原因是Spark本身负责批量计算MySQL承担查询和对外服务两者的职责分开更合理。Spark写MySQL需要在spark-submit时用--jars mysql-connector-java-x.x.x.jar带上驱动代码里用df.write.format(jdbc).option(url, jdbc:mysql://localhost:3306/douban_db).option(dbtable, genre_stats).option(user, root).option(password, ****).mode(overwrite).save()这里的mode选overwrite是因为分析结果每次是全量重算不需要增量更新。后端从MySQL读取的代码相对直观用PyMySQL执行SELECT * FROM genre_stats ORDER BY avg_score DESC把结果转成dict列表返回即可。这样改完后数据链路是完整的爬虫采集CSVSpark分析后写MySQLFlask按需查库ECharts渲染成图表。答辩时这条链路的每一步都有对应代码哪个环节被问到都能立刻切到相关文件。4.3 大屏适配、图表联动与数据刷新策略可视化层的细节决定演示观感。首先是页面布局大屏设计通常16:9固定分辨率用百分比的容器宽度做适配ECharts图表容器设置固定高度宽度用flex分配。其次是配色保持一致背景用深蓝色系图表主色用青绿色和暖黄色这类配色方案能直接引用大屏模板风格但注意不要让整体页面显得混乱。图表联动是一个容易出效果的点。比如页面左侧是评分分布直方图右侧是类型TopN柱状图点击柱状图中某个类型时直方图联动筛选出该类型下电影评分分布。实现依赖ECharts的dispatchAction事件在柱状图click回调里触发直方图的dataZoom或restore操作代码不到20行答辩演示时能制造明显亮点。数据刷新策略上页面用setInterval每隔30秒重新拉一次接口即可但Spark分析通常不是实时计算刷新意义只在证明接口是活的。一个更好的演示动作是先在页面展示旧数据然后重新运行Spark任务更新MySQL点浏览器刷新按钮后看到图表变化这个过程比自动刷新更能说明“批处理”的流程。5. 文档说明与答辩验证让毕业设计从“能跑”变成“讲得清”5.1 项目级README的写作模板“文档说明”是标题里的明示交付物但大多数毕设README只是把代码结构贴一遍。更合理的README应该回答三个问题这项目是什么、怎么跑起来、数据流向是什么。建议按以下结构组织# 豆瓣电影爬虫与Spark分析可视化 ## 项目简介 一句话说明附带系统架构图文字版。 ## 环境依赖 JDK版本 / Spark版本 / Python库清单 / MySQL版本 ## 快速开始按顺序执行 1. 安装依赖 2. 运行爬虫脚本 3. 运行Spark分析脚本 4. 启动Flask服务 5. 浏览器访问 ## 数据说明 各表字段、来源、口径 ## 代码结构 目录树 每个文件的职责 ## 常见问题 环境变量、编码、端口占用5.2 代码注释的颗粒度与源码交付规范毕设源码交付的常见问题不是注释太少而是注释没有落在关键决策上。不要每行都写注释那样会显得啰嗦应该在高风险逻辑、参数选择、算法选择处写明理由。比如max_workers5旁边标注“超过5会触发豆瓣反爬”spark.sql.shuffle.partitions4旁边标注“本地模式分区过多会徒增调度开销”。这类注释才是评分老师想看到的代表你真的踩过坑。源码文件名统一用英文、函数名统一用下划线风格清洗数据字段名保持与数据库一致能减少很多答辩时“这代码是不是你自己写的”的疑虑。5.3 一条命令从爬虫跑到可视化页面的演示路径最后的落点是一个可重复执行的演示脚本把整个数据链路串成一条命令。写一个run.sh按顺序执行爬虫、Spark分析、启动Web服务三位一体的工作。这样答辩时就能完整演示从数据抓取到图表渲染的全流程每个阶段都能看到明确输出也不会在切换命令时卡壳。# run.sh python douban_spider.py spark-submit --master local[4] --driver-memory 2g --executor-memory 2g analyze.py python app.py脚本里每一行执行前输出当前阶段名称这样现场演示时评委能一眼看出当前在跑哪一层。注意app.py是常驻进程脚本最后一行在真实终端执行时会阻塞演示时用Tab再开一个终端页访问http://localhost:5000这是更稳妥的做法。真正的交付标准是让另一个人照着README和这个脚本从零开始也能把页面跑起来。你按这个标准去整理代码和文档答辩时被问倒的概率就很小了。本文还有配套的精品资源点击获取