ARTICLE DETAIL

资讯详情

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

从爬虫到Spark再到ECharts:豆瓣电影数据分析全流程实践

从爬虫到Spark再到ECharts:豆瓣电影数据分析全流程实践 简介一份基于豆瓣电影爬虫与Spark数据分析可视化的毕业设计源码包面向计算机相关专业的在校学生、教师或大数据入门者尤其适合作为毕设课题、课程设计或项目初期演示的参考模板内容围绕“爬虫采集—数据清洗—Spark分析—可视化展示”完整链路展开帮助读者快速理解并复现真实的数据分析项目。包内共242个文件压缩包仅5.64MB主要包含22个Java源码、165个XML配置以及Python爬虫脚本、Spark分析编译类、前端页面CSS/HTML/JS文件、CSV结果数据、SQL数据库脚本和Markdown说明文档既有采集端代码也有分析计算与可视化展示模块目录结构清晰便于按需检索和二次开发。目前已有519人学习下载资源附带可运行源码、文档说明及辅助信息学习价值在于既能掌握豆瓣电影数据的抓取与预处理又能了解Spark分布式统计和图表可视化的落地方法。对于准备毕业设计或想向大数据分析方向进阶的学习者这是一份不错的实践样例。1. 爬虫加Spark分析这套豆瓣电影源码到底能学到什么豆瓣电影的数据规模不大不小正好卡在「单机爬虫能搞定、Excel已经撑不住」的中间地带。用requests把电影信息抓下来存成结构化文件再用Spark做词频、类型、评分等统计最后用ECharts渲染成图表——这一整套链路做完等于把数据采集、离线计算、可视化展示三个最常被问到的环节完整走了一遍。项目源码里出现的WordNum.class、TypeNum.class、LvNum.class这些编译产物说明分析任务已经跑通过直接看代码就能知道每个统计是怎么写的。适合正在做毕设、打算入门Spark或者想补全数据项目经验的人也适合拿来改造成其他垂直领域的数据分析demo。2. 豆瓣电影爬虫字段设计、反爬策略与落盘格式2.1 数据字段设计先想清楚Spark要算什么爬虫不是把页面HTML存下来就算完而是要为后续分析准备好结构化字段。我习惯先列一个字段清单把要分析的口径和字段一一对应。这个项目里需要支持分词词频、类型统计、评分分布、评论数量和年份分析那么至少需要下面这些字段字段名说明来源示例movie_id豆瓣电影唯一IDURL1292052title电影名称页面标题肖申克的救赎year上映年份年份标签1994rating豆瓣评分评分文本9.7comment_num评论人数评论数标签2345678genres类型列表类型标签剧情/犯罪comments短评拼接文本短评区域希望让人自由...注意genres这个字段要存成列表或者用逗号分隔的字符串因为一部电影有多个类型后续用Spark做flatMap拆分才能正确统计各类型占比。comments字段用于词频统计抓取时可以拼接前几条短评不用全量抓取否则数据量会迅速膨胀。2.2 抓取实现requests加队列比Scrapy更轻量对于豆瓣这种中等规模站点使用requests配合线程池就够用了没必要一上来就上Scrapy。下面是简化版的爬虫核心逻辑重点展示如何构造请求、解析字段和断点续抓import requests import pandas as pd from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor 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 } def fetch_movie(movie_id): url fhttps://movie.douban.com/subject/{movie_id}/ try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) # 以豆瓣电影页面结构为例实际解析需按页面调整 title soup.find(h1).get_text(stripTrue) year soup.select_one(.year).get_text(stripTrue).strip(()) rating soup.select_one(.rating_num).get_text(stripTrue) comment_num soup.select_one(.rating_people).get_text(stripTrue) genres ,.join([a.get_text() for a in soup.select(.tags .type)]) return { movie_id: movie_id, title: title, year: year, rating: rating, comment_num: comment_num, genres: genres, } except Exception as e: print(f抓取失败 {movie_id}: {e}) return None if __name__ __main__: ids list(range(1292052, 1292070)) # 示例ID段 results [] with ThreadPoolExecutor(max_workers4) as pool: for result in pool.map(fetch_movie, ids): if result: results.append(result) df pd.DataFrame(results) df.to_csv(douban_movies.csv, indexFalse, encodingutf-8-sig)代码里用ThreadPoolExecutor开4个线程原因是不想给目标站点造成压力同时又能比单线程快好几倍。headers里的User-Agent必须设成常见浏览器的样子否则豆瓣会直接拒绝。timeout参数设置为10秒避免某个电影页面卡住导致整个线程池阻塞。爬取结果统一放进DataFrame再落盘成CSVSpark可以直接读取不需要额外转格式。2.3 反爬遇到的几个坑以及我的处理方式豆瓣对单IP的请求频率比较敏感最常见的是返回418或者302跳转。我踩过的坑集中在三个方面请求头不全只带了User-Agent缺少Accept-Language和Referer后来补上就稳定很多。没有加延时即使开了线程池每个线程连续请求也会触发封禁。我一般在线程任务里加一个time.sleep(random.uniform(0.5, 1.5))。Cookie失效登录后的Cookie用于访问更多数据但Cookie过期后请求会静默失败。我的做法是写一个健康检查函数定期用一个已知ID请求连续失败就停止爬虫并输出日志方便人工介入。关于断点续爬更稳妥的做法是每次抓完一个电影就立刻把记录追加到CSV而不是全部抓完再写入。这样即使中途被封已经抓到的数据还在重跑时只需要跳过CSV里已有的movie_id即可。3. Spark数据分析从CSV到多个统计结果文件3.1 为什么选择Spark而不是Pandas豆瓣电影数据量可能在几万条到几十万条之间Pandas完全能处理。但作为毕设或数据项目用到Spark是一个加分项因为它体现了分布式计算的思路。而且当评论数据被展开成词条之后规模会膨胀几十倍此时Spark的延迟计算和分区优势就体现出来了。项目文件里的_part-r-00000.crc这些临时文件正是Spark写Hadoop输出格式时生成的校验文件说明任务跑的是标准Spark流程。环境方面我用的是Spark 3.x搭配Scala 2.12Python版PySpark也可以。代码逻辑一致这里以Scala为例因为编译产物是.class文件说明原始工程是Java或Scala写的。3.2 读取CSV并构建DataFrameimport org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ val spark SparkSession.builder() .appName(DoubanMovieAnalysis) .master(local[*]) .getOrCreate() val df spark.read .option(header, true) .option(encoding, UTF-8) .csv(data/douban_movies.csv)这里.master(local[*])表示使用本地多线程模式星号代表利用所有可用核心。如果部署到集群只需要把master换成yarn或spark://地址。读取时显式指定UTF-8否则Windows环境下生成的csv可能出现乱码。3.3 四个核心统计任务的实现项目里的WordNum、TypeNum、LvNum分别对应词频、类型统计、评分等级统计YearNum是年份统计CommontNum是评论数量分析。下面把逻辑拆开写便于理解每个类在做什么。// 1. 电影类型统计拆开逗号分隔的genres val typeDF df .select(explode(split(col(genres), ,)).as(type)) .groupBy(type) .count() .orderBy(col(count).desc) // 2. 年份分布统计过滤异常年份后分组 val yearDF df .filter(col(year).rlike(^\\d{4}$)) .groupBy(year) .count() .orderBy(year) // 3. 评分等级统计将评分映射到区间 val lvDF df .withColumn(level, when(col(rating).cast(double) 9.0, 9分以上) .when(col(rating).cast(double) 8.0, 8-9分) .when(col(rating).cast(double) 7.0, 7-8分) .otherwise(7分以下)) .groupBy(level) .count() // 4. 评论数量Top10电影 val hotDF df .select(title, comment_num) .withColumn(comment_num, col(comment_num).cast(int)) .orderBy(col(comment_num).desc) .limit(10)explode和split是Spark处理数组字段最常用的组合split把字符串切成数组explode把数组展开成多行。评分等级用了多个when判断cast(double)是为了防止CSV里的评分字段被读成字符串。评论数量必须转成int再排序否则会按字典序排列导致999排在10000前面。3.4 结果输出格式与目录组织每个统计结果需要单独保存项目里可以看到多个part-r-00000文件这就是Spark的默认输出格式。为了让后续可视化方便读取我建议统一输出为parquet或者带header的CSV。下面是推荐写法typeDF.coalesce(1).write .option(header, true) .mode(overwrite) .csv(output/type_stat) yearDF.coalesce(1).write .option(header, true) .mode(overwrite) .csv(output/year_stat)这里使用了coalesce(1)强制合并成一个分区文件好处是生成的文件直接就是需要的目录且内容完整不会出现一堆part文件。缺点是数据量很大时单文件写入会有性能问题不过对于豆瓣电影这种万级数据量完全够用。mode(overwrite)表示每次跑任务都覆盖旧结果避免手动清理历史文件。4. 可视化展示把Spark结果变成前端图表4.1 结果数据如何给前端使用Spark输出的CSV文件可以直接被前端异步加载但实际项目中更推荐把多个统计结果合并成一个JSON文件让前端一次请求拿到全部数据。我的做法是写一个Python脚本读Spark输出目录里的CSV再统一打包成douban_stats.json。结构如下{ typeStat: [{type: 剧情, count: 1200}], yearStat: [{year: 1994, count: 45}], levelStat: [{level: 9分以上, count: 30}], hotMovies: [{title: 肖申克的救赎, comment_num: 2345678}] }前端只需要一个fetch请求就能拿到全部图表数据避免多次请求造成的加载闪烁。如果要做成实时展示可以换成后端接口但作为毕业设计静态JSON文件配上简单的HTTP服务就够了。4.2 ECharts图表配置的几个关键点使用ECharts做可视化时我通常聚焦四个图表饼图展示电影类型占比柱状图展示年份分布条形图展示评分等级横向柱状图展示热门电影评论数。下面以类型统计为例fetch(douban_stats.json) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(typeChart)); chart.setOption({ tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 70%], data: data.typeStat.map(item ({ name: item.type, value: item.count })) }] }); });饼图这里使用了环形效果通过radius数组控制内半径和外半径视觉上比实心饼图更现代。tooltip设置为item触发鼠标悬浮时能展示该类型的名称和数量。需要注意ECharts实例要在DOM渲染完成后再初始化否则获取不到容器节点。4.3 可视化大屏的排版建议如果是做可视化大屏建议把页面分成上中下三块区域顶部放总览统计数字中部放类型占比和年份趋势底部放热门电影排行和评分等级。左侧放饼图、右侧放柱状图这种布局符合人眼从左到右的阅读习惯。另外为了适配大屏分辨率ECharts容器要使用百分比宽度并且在窗口变化时调用chart.resize()。还可以用CSS Grid或Flexbox来划分区域而不是用绝对定位这样不同尺寸的屏幕都能自适应。5. 编码细节与常见报错的应对方法5.1 从源码中能学到的三个编程习惯这个项目的编译产物里有WordUtil.class说明作者把文本处理逻辑单独抽了一个工具类这种写法很值得借鉴。第一个习惯是把清洗逻辑独立成函数比如去除标点、过滤停用词、统一大小写方便复用和单元测试。第二个习惯是写常量管理比如CSV文件路径、输出目录、豆瓣请求头都定义成常量避免散落在代码各处。第三个习惯是给每个统计任务写单独的类或对象如TypeNum、YearNum职责单一之后想更换某个统计逻辑直接定位对应类就行不会影响其他模块。5.2 关于Spark任务长时间不结束遇到本地模式Spark跑很久不结束先检查是不是数据倾斜。比如某一年份的电影数量特别多groupBy后落到单个分区上导致其他分区空闲等待。我一般通过df.groupBy(year).count().show(false)先把数据分布打出来观察有没有极端情况。如果存在倾斜可以加一个随机前缀进行两阶段聚合先把key打散再对结果去掉前缀聚合一次。对于这个电影数据的规模极少出现真正的倾斜更多时候是local[*]线程数太多小任务反而被线程切换拖慢改成local[2]反而更快。5.3 爬虫与Spark衔接时的编码问题在Windows上爬虫生成的CSV默认是GBK编码而Spark读取时默认UTF-8会出现乱码。我的习惯是爬虫落盘时统一用encodingutf-8-sig这个参数会在文件头部写入BOMExcel打开也不会乱码Spark读取时指定option(encoding, UTF-8)即可。另外CSV里的年份字段可能混入空格或“()”在Spark里用regexp_replace(col(year), [^0-9], )清洗后再转int能避免很多解析异常。5.4 让项目能演示得更出彩的改进方向如果想让这个毕设项目更有竞争力可以在现有基础上加入评论情感分析。爬虫已经拿到了comments字段用Spark调用一个简单的分词库按词频表打正负情感分生成情感趋势折线图作为第五张图表。再或者将Spark任务包装成定时执行每天抓取新增电影数据用增量统计替换全量统计这样整个项目就从一次性分析变成了可持续运行的框架。改动成本不高但面试时能讲述的深度会完全不同。本文还有配套的精品资源点击获取
返回列表