☰
Hadoop电影推荐系统离线实践:爬虫采集、MapReduce计算与SpringBoot接口
2026/10/3 14:30:36 网站建设 项目流程

简介:基于Hadoop大数据技术的电影推荐系统项目资源,包含可运行源码、SQL脚本与设计文档(LW),适用于毕业设计、课程设计、大作业和工程实训等场景,也适合初学或进阶学习者作为项目实战练手。系统采用Python爬虫采集电影数据,结合SpringBoot、Vue、MySQL与Hadoop构成完整技术栈;管理员可在后台管理用户、维护电影信息、审核论坛交流并配置系统,通过可视化看板查看评分、导演、类型、地区等实时统计图表,实现更精准的个性化推荐;用户则可在个人中心设置偏好、查看收藏列表与浏览历史。资源共642个文件,压缩包大小约26MB,涵盖Java后端源码、Vue前端页面、Python爬虫脚本、SQL数据库脚本、启动与运行脚本,以及docx/doc说明文档;其中还包含JS、CSS、XML等辅助文件,用于页面交互、样式与配置,目录结构清晰,便于分类查阅。目前已有94人学习下载。借助该完整项目,可以掌握从数据采集、分布式存储与计算,到后端接口开发、前端管理页面搭建的完整项目落地思路;附带的SQL文件和设计文档还能辅助快速部署、二次开发,或直接作为毕业设计、项目立项的可靠基础。

1. 这套电影推荐系统不是让你用 Hadoop 扛实时流量,而是补上离线计算这条腿

拿到一份 5b002 基于 Hadoop 大数据技术的电影推荐系统设计时,很多人第一反应是:现在谁还用 MapReduce 做推荐?确实,线上实时推荐基本被 Flink 和 Spark Streaming 占据,但这类 springboot+spider 题目的重点本就不在实时性,而是一条完整的离线链路:spider 负责采数据,Hadoop 负责批量算,SpringBoot 负责把结果暴露成接口。对课程设计和毕设来说,这个组合比单机推荐系统更有辨识度,也比纯 Spark 方案更容易部署到普通虚拟机。适合人群很明确:想用大数据技术跑通一个闭环,又不想把精力全耗在实时计算上的同学,以及第一次把 Hadoop 和 Web 工程串起来的一线开发。先把这条链路跑通,再谈优化。

2. 先想清楚架构:这四层各自解决什么问题

2.1 推荐数据天生是离线的,正好放进 Hadoop

电影推荐系统处理的数据是用户对电影的评分、点击、收藏记录。这类数据有几个特点:总量大但增长慢、单条价值密度低、建模不要求秒级回算。以豆瓣或 MovieLens 的结构为例,一天可能产生几十万条评分,一个学期也就是几千万条。这个量级在单机上用 MySQL 也能跑,但处理全量数据时需要预先清洗、分布式存储和容错,这些正是 Hadoop 的看家本领。题目既然挂在 Hadoop 大数据技术名下,就必须把"为什么不上单机、为什么不用 Spark 实时算"讲清楚,否则答辩时容易被问倒。

我的选择是:Hadoop 负责离线计算层。HDFS 存放原始评分和爬虫落地的电影元数据,MapReduce 在凌晨固定时段批量计算用户兴趣或物品相似度,把结果写回 HDFS。交互式查询交给 SpringBoot,它不碰算法,只读已经算好的结果。这个方案的优点是把"大数据处理"和"Web 开发"解耦,哪一层出了问题都能单独定位。相比直接用 Spark 全家桶,Hadoop 的 MR 概念更基础,也更容易在白板上画出执行流程。

2.2 Spider 采集的数据,为什么要先落到 HDFS

爬虫在这个项目里不是核心算法,而是数据来源。常见的做法有两种:一种是爬虫抓完直接写 MySQL,SpringBoot 从 MySQL 服务页面;一种是抓完先落本地 CSV,再通过hadoop fs -put丢进 HDFS。前者适合在线业务,后者适合离线训练。我在做这类设计时更倾向第二种,理由有三个:HDFS 对追加写入和批处理友好,MR 直接读 HDFS 上的文件,省去"先导 MySQL 再导 HDFS"的中间步骤;原始数据不需要频繁更新,正好符合 HDFS 的"一次写入、多次读取"模型;如果爬虫抓坏了,删目录重跑即可,不会污染业务数据库。

爬虫抓的字段要克制,别一上来就抓短评全文。我会定义两张最基础的表:movies.csv至少包含movieId,title,genres;ratings.csv包含userId,movieId,rating,timestamp。评分数据尽量从公开数据集拿,爬虫只负责补充电影海报、上映年份或简介,这样能避开两个坑:评分稀疏导致推荐结果全空,以及大规模爬取引发的访问频率问题。一句话:spider 是配角,它不是让你证明爬虫技术有多强,而是让推荐系统有数据可用。

2.3 SpringBoot 不负责算法,它只做接口层和调度入口

很多第一次做这个题目的人会把 SpringBoot 当成计算引擎,试图在内存里用 Java 写协同过滤,这会把项目做成"一个普通 Web 应用挂了个 Hadoop 的名字"。正确的分工应该是:SpringBoot 接收前端的用户 ID 请求,从 HDFS 结果目录读取对应的推荐列表,返回 JSON;另外通过@Scheduled或简单的手动触发接口,执行"上传数据到 HDFS → 提交 Hadoop Streaming 任务"的脚本。

如果想让查询更快,可以在 SpringBoot 里加一层内存缓存,把当天的推荐结果预热到ConcurrentHashMap,而不是每次请求都去读 HDFS。同一个用户重复登录时,命中缓存就不需要再查文件系统。缓存只适合离线结果,因为离线结果在下次计算前不会变化,非常适合缓存。这样做的好处是,答辩时你能讲清楚"计算离线做、查询在线做、缓存挡住重复读"三个层次,而不是一锅粥。落地顺序我固定在六步:定目录结构、写爬虫、传 HDFS、提交流程跑 MR、结果落目录、SpringBoot 读结果,中间任何一步失败都不影响前后步骤排查。

3. 从零搭伪分布式 Hadoop:安装、配置和 IDEA 联调

3.1 安装前必须定死的三件事:JDK、SSH、Hadoop 版本

伪分布式是整个项目最容易翻车的环节,主要不是 Hadoop 本身难,而是环境太杂。我一般建议在虚拟机上装 CentOS 7 或 Ubuntu 20.04 这类 LTS 版本,内存给到 4G 以上,然后按顺序处理三件事。

第一件事是 JDK。Hadoop 2.x 和 3.x 对 JDK 版本要求不同,但一致性要求是"必须先装 JDK,再装 Hadoop,再改JAVA_HOME"。我常用的组合是 JDK 8 配 Hadoop 3.x 系列,兼容性最稳。装完验证java -version,然后把JAVA_HOME写进~/.bashrc。

第二件事是 SSH 免密登录。伪分布式虽然没有多台机器,但 NameNode 和 DataNode 之间仍然要通过 SSH 启动,免密省掉每次输入密码的麻烦。命令很简单:

ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ssh localhost

测试时如果出现Host key verification failed,先执行一次ssh-keyscan localhost >> ~/.ssh/known_hosts。这一步没做通,后面执行start-dfs.sh会出现 DataNode 连不上 NameNode 的怪现象,日志却干干净净。

第三件事是 Hadoop 安装路径。解压后我习惯统一放在/usr/local/hadoop而不是/root/hadoop,因为后面很多配置文件里要写绝对路径,路径写得太深容易出错。解压完成后在~/.bashrc里加:

export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin

然后source ~/.bashrc,执行hadoop version确认能输出版本信息。到这里,安装阶段完成。

3.2 三个必须手改的 XML 配置文件

Hadoop 伪分布式只需要改三个文件:core-site.xml、hdfs-site.xml、yarn-site.xml。千万不要去改hadoop-env.sh里的内存参数堆得过大,小虚拟机反而会起不来。

core-site.xml指定默认文件系统为 HDFS,并设置 NameNode 地址:

<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/tmp</value> </property> </configuration>

hadoop.tmp.dir是 Hadoop 持久化数据的根目录,必须是一个真实存在的目录,并确认当前用户有写权限。默认值在/tmp下,系统重启会被清空,这是 NameNode 起不来的常见原因之一。

hdfs-site.xml设置副本数为 1:

<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/usr/local/hadoop/nn</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/usr/local/hadoop/dn</value> </property> </configuration>

伪分布式只有一台机器,副本数设成 3 会导致 DataNode 一直处于Under-Replicated状态,虽然不影响小实验,但看着碍眼,也会拖慢写入。nn和dn目录也要提前建好。

yarn-site.xml是后面提交任务卡住的重点,先给一个最小配置:

<configuration> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>3072</value> </property> <property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> </configuration>

memory-mb要结合虚拟机物理内存设置,我给 4G 虚拟机留 3G;vmem-check-enabled在伪分布式环境建议关掉,否则 JVM 虚拟内存超限会把容器杀掉,任务反复失败。

3.3 启动、校验,以及 Windows 下用 IDEA 联调 HDFS

配置完成后,第一次启动要先格式化 NameNode,这条命令只能执行一次,重复格式化会把之前的元数据全部清掉:

hdfs namenode -format start-dfs.sh start-yarn.sh jps

jps输出里应该看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager五个进程。少任何一个都说明启动没成功,不要急着看代码,先去$HADOOP_HOME/logs目录翻对应组件的.log文件。接下来创建项目要用的目录,并上传一个测试文件:

hadoop fs -mkdir -p /movie/raw echo "1,101,5.0" > /tmp/test.csv hadoop fs -put /tmp/test.csv /movie/raw/ hadoop fs -cat /movie/raw/test.csv

能输出1,101,5.0,说明 HDFS 链路通了,后面就可以把真实评分数据丢进去。

Windows 上用 IDEA 调试 SpringBoot 时,Java 代码要访问 Linux 上的 HDFS,最常见的问题是没有把core-site.xml放进 classpath,以及缺少winutils.exe。我一般会在src/main/resources下放一份core-site.xml,里面把fs.defaultFS指向hdfs://192.168.x.x:9000,然后把 Windows 上的HADOOP_USER_NAME环境变量设成 Linux 上的启动用户(通常是root或hadoop),这样能省掉大量权限问题。开发机联不上时,优先在宿主机用telnet 虚拟机IP 9000测端口通不通,而不要先怀疑代码。

4. 把推荐链路跑通:爬虫、Streaming 计算与 SpringBoot 接口

4.1 用 Jsoup 写一个可停可续的爬虫,把电影数据落到本地 CSV

既然标题里有spider,爬虫模块建议直接放在 SpringBoot 工程里,用 Jsoup 做页面解析,这样整个项目保持 Java 技术栈统一。下面是最小版本的核心逻辑:

public void crawlMovieList() { String url = "https://example.com/movies?page=1"; Connection conn = Jsoup.connect(url) .timeout(3000) .header("User-Agent", "Mozilla/5.0"); Document doc = conn.get(); Elements items = doc.select(".movie-item"); try (BufferedWriter writer = Files.newBufferedWriter( Paths.get("movies.csv"), StandardCharsets.UTF_8)) { writer.write("movieId,title,genres\n"); for (Element item : items) { String title = item.select(".title").text().trim(); String genres = item.select(".genres").text().trim(); writer.write(item.attr("data-id") + "," + title + "," + genres + "\n"); } } catch (IOException e) { log.error("爬取失败", e); } }

代码里的example.com只是占位,实际换成你选定的目标页面。这段逻辑有三个参数值得调:timeout设 3 秒是为了不让单页卡死整个任务;User-Agent必须带上,很多页面会拦截无头请求;写入文件用StandardCharsets.UTF_8,避免中文电影名变成乱码。爬完后,先人工抽查几十行 CSV,确认没有奇怪的引号或换行,再执行:

hadoop fs -put movies.csv /movie/raw/movies.csv

不要直接让爬虫去连 HDFS,中间保留本地 CSV 这一步,既方便检查数据质量,也能在爬虫写坏时快速重来。这是反复踩坑后留下的习惯。

4.2 用 Hadoop Streaming 跑电影相似度:mapper.py 和 reducer.py 的最小版本

推荐计算如果全用 Java MapReduce 写,类多、打包慢,不适合课程设计这种短周期项目。我一般用 Hadoop Streaming 提交 Python 脚本,既保留 MR 分布式执行,又能快速调逻辑。任务输入是/movie/raw/ratings.csv,每一行是userId,movieId,rating,目标是统计"被同一个用户看过的电影两两之间共现次数",作为电影相似度的基础。

mapper.py读入每个用户的全部评分,把同一用户看过的电影两两组合,输出movieA:movieB\t1:

#!/usr/bin/env python3 import sys def parse(): user_movies = {} for line in sys.stdin: line = line.strip() if not line: continue parts = line.split(',') if len(parts) < 3: continue uid, mid, rating = parts[0], parts[1], parts[2] user_movies.setdefault(uid, []).append(mid) for uid, mids in user_movies.items(): mids = sorted(set(mids)) for i in range(len(mids)): for j in range(i + 1, len(mids)): print(f"{mids[i]}:{mids[j]}\t1")

reducer.py累加相同键的次数,输出共现次数前 100 对:

#!/usr/bin/env python3 import sys from collections import Counter counter = Counter() for line in sys.stdin: line = line.strip() if "\t" not in line: continue key, _ = line.split("\t", 1) counter[key] += 1 for pair, cnt in counter.most_common(100): print(f"{pair}\t{cnt}")

这里有个容易忽略的点:同一用户重复看同一部电影时评分会出现多行,set(mids)去重后共现次数才不会被同一用户重复刷高。当单个用户观影量很大时,建议先按评分排序取 TopN 再两两组合,否则组合数随观影量平方增长,map 输出会爆炸。提交命令如下:

hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -files mapper.py,reducer.py \ -mapper "python3 mapper.py" \ -reducer "python3 reducer.py" \ -input /movie/raw/ratings.csv \ -output /movie/sim

-files是分发脚本到所有节点的关键,不加它任务会报File not found。输出目录/movie/sim必须事先不存在,否则 Hadoop 会拒绝执行。跑完用hadoop fs -cat /movie/sim/part-00000查看,能看到类似101:102\t3的结果,就说明计算链路通了。

4.3 SpringBoot 读取 HDFS 结果:FileSystem API 与用户兜底策略

计算层产出相似度结果后,SpringBoot 的职责是把它变成可查询的推荐接口。下面这段代码用 Hadoop FileSystem API 读取/movie/sim下所有 part 文件,整理成倒排索引结构:

@Configuration public class HdfsClient { private FileSystem fileSystem; @PostConstruct public void init() throws IOException { Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://localhost:9000"); fileSystem = FileSystem.get(conf); } public List<String> readSimResult() throws IOException { List<String> lines = new ArrayList<>(); RemoteIterator<LocatedFileStatus> files = fileSystem.listFiles(new Path("/movie/sim"), true); while (files.hasNext()) { LocatedFileStatus status = files.next(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(fileSystem.open(status.getPath())))) { String line; while ((line = reader.readLine()) != null) { lines.add(line); } } } return lines; } }

fs.defaultFS可以直接从 classpath 里的core-site.xml读取,但我在代码里显式写了一次,目的是让大家看清这个参数来自哪里。在实际项目中,连接地址应该放到application.yml,用@Value注入,不要写死在类里。读取结果后,用字符串解析movieA:movieB\t次数,建立"相似电影 + 权重"的映射,再写一个RecommendController返回 JSON。

有一个必须处理的情况:冷门用户没有出现在任何共现结果里。我的做法是,接口先查用户已看过的电影,再根据相似度映射找候选电影;如果候选为空,就返回评分次数最多的 Top10 热门电影兜底,并单独标一个source: "hot"字段。这样既不会让前端拿到空列表,也方便答辩时解释冷启动策略。

5. 避坑清单:我跑这个项目时翻过的车,请你直接跳过

5.1 NameNode 起不来,日志里全是 incompatible clusterID

现象:格式化后第一次执行start-dfs.sh,NameNode 进程启动后立刻退出,日志里出现storage directory ... has incompatible clusterID,DataNode 那边始终连不上。

原因:重复执行了hdfs namenode -format,而 DataNode 的持久化目录还保留着上一次格式化的 clusterID,新旧集群 ID 对不上。通常是参考教程时手痒多敲了一次格式化命令。

解决:全部停止后,清掉配置过的临时目录再重新格式化,这是唯一可靠的解法。

rm -rf /usr/local/hadoop/tmp /usr/local/hadoop/nn /usr/local/hadoop/dn hdfs namenode -format start-dfs.sh

注意:格式化命令一生只敲一次,之后除非换机器,再也不要碰它。清空目录后就能恢复,但之前上传的数据会全部丢失。

5.2 YARN 任务一直卡在 ACCEPTED,节点日志什么错都不报

现象:提交 Hadoop Streaming 后,任务状态一直停在ACCEPTED,NodeManager 里看不到 container 启动,等一小时也不动。

原因:为伪分布式配置的yarn.nodemanager.resource.memory-mb过大,或者 JVM 虚拟内存检查开启,容器启动了又被静默杀掉。这种问题最像玄学,因为它不给你可读的堆栈,纯靠日志关键词去猜。

解决:先确认虚拟机内存,free -h看一下真实可用内存;然后把yarn-site.xml中的memory-mb调整到不超过物理内存的一半,并确认已经设置yarn.nodemanager.vmem-check-enabled为false。改完重启 YARN,任务就能正常跑到RUNNING和SUCCEEDED。

5.3 SpringBoot 读 HDFS 报Permission denied: user=Administrator

现象:IDEA 里启动 SpringBoot,调用读取/movie/sim的接口,日志抛出Permission denied,文件明明用 hadoop 用户能读。

原因:Java 客户端默认取当前系统用户名,Windows 下是Administrator,Linux 上的 HDFS 用户是hadoop,HDFS 目录权限默认 755,其他用户只能读,如果结果目录的写权限也没开,问题会更明显。

解决:开发环境最省事的办法是启动 SpringBoot 前设置HADOOP_USER_NAME=hadoop;也可以在代码里用UserGroupInformation.createRemoteUser("hadoop")再登录代理。如果只是单机实验,直接修改 HDFS 目录权限也可以,但不推荐养成关权限的习惯。

5.4 爬虫抓下来的中文电影名到推荐结果里全是乱码

现象:本地 CSV 打开正常,传到 HDFS 后用hadoop fs -cat也正常,但推荐接口返回的中文标题变成æµ·ç一类字符。

原因:编码链路不统一。爬虫输出文件用了StandardCharsets.UTF_8,但某些中间脚本用系统默认编码读取,或者 CSV 被 Excel 打开过一次并保存成了 GBK,Hadoop Streaming 读入时按平台默认编码解析,于是中文字节被错误翻译。

解决:全链路强制 UTF-8。爬虫写出时指定StandardCharsets.UTF_8,上传前检查文件头部是否有 BOM;Hadoop Streaming 脚本里读取标准输入后立即encode('utf-8');SpringBoot 读取 HDFS 文件时用InputStreamReader(fileSystem.open(path), StandardCharsets.UTF_8)。乱码问题一旦发生,检查 CSV 文件字节比检查代码更有效,因为大多数时候是源文件已经被改坏了。

5.5 相似度计算结果全是 0,或者只有共现没有加权

现象:part-00000里能输出键值对,但推荐列表里排序跟人工判断完全不符,热门烂片排在最前面。

原因:只用共现次数做相似度,对热门电影不友好;另外评分数据如果太稀疏,大多电影对只共现一两次,区分度极低。还有一个隐蔽原因:mapper.py里没有过滤低评分,用户打了 1 星的电影也参与共现,等于把不喜欢的电影也当成了"相似"。

解决:协同过滤里我一般先按评分过滤,比如只保留rating >= 3.0的记录再跑共现;然后在 reducer 里把共现次数换算成 Jaccard 系数或余弦相似度,而不是直接输出次数。如果数据量实在太小,就回去扩大评分数据来源,算法调参解决不了数据稀疏的根本问题。

6. 先做离线评估,再把计算层从 Hadoop 平滑换成 Spark

6.1 用留出法评估推荐质量,别只靠人工看

在收尾之前,建议给推荐结果加一个最简单的离线评估。把评分数据按时间分成训练集和测试集,训练集算相似度,测试集用来检查用户是不是真的看了推荐电影。Top10 推荐里命中测试集的个数除以 10 就是精确率,命中数除以该用户测试集评分总数就是召回率。真实稀疏数据上,Top10 命中两三部已经算正常,数字虚高反而要怀疑数据泄漏。

6.2 保留接口层,用 Spark ALS 替换 Streaming 脚本

如果做完 Hadoop 还想继续延伸,最顺滑的路线是把 Streaming 的 mapper/reducer 替换成 Spark 的 ALS,输出格式保持userId,itemId,rating,SpringBoot 的读取逻辑一行都不用改。数据仍放 HDFS,批量计算改成spark-submit提交,项目就从 Hadoop 特色自然过渡到大数据技术特色,骨架不变。

我自己在这类项目上吃过最大的亏,是急着调算法参数,最后发现是电影 ID 有重复。现在习惯是:先让数据主键、编码、分区目录全部过一遍校验,再开机器学习。数据管道干净了,算法随便调都有好结果。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询