Skip to content

项目4 Hadoop MapReduce 进阶实战:电影影评多维度分析教程 ​

教程概述 ​

学习目标 ​

  1. 掌握 MapReduce 输入输出格式的配置与应用,理解序列化的核心优势。
  2. 熟练使用 Hadoop Java API 操作 HDFS 文件系统(文件夹管理、文件读写、序列化文件处理)。
  3. 学会 MapReduce 程序优化技巧(自定义键值类型、Combiner、Partitioner、自定义计数器)。
  4. 掌握在 IntelliJ IDEA 中打包、远程提交 MapReduce 程序的实操流程。
  5. 能够独立完成电影影评多维度统计分析(4 个核心实战任务)。

适用人群 ​

  • 具备 Hadoop 基础环境搭建经验的初学者。
  • 学习大数据处理的高校学生、技术爱好者。
  • 需使用 MapReduce 进行数据统计分析的开发人员。

前置知识 ​

  • 熟悉 Java 基础编程(类、接口、异常处理)。
  • 了解 Hadoop 核心概念(HDFS、MapReduce 工作流程)。
  • 掌握 IntelliJ IDEA 基本操作(项目创建、依赖配置)。

一、项目背景与核心任务 ​

1.1 项目背景 ​

随着电影市场规模扩大,海量用户影评中蕴含着观众偏好、电影口碑等关键信息。传统单一维度分析已无法满足需求,本项目基于 MapReduce 框架,实现电影影评的多维度深度分析,为电影制作方、发行方及播放平台提供决策支持。

1.2 核心分析任务 ​

  1. 统计评分次数最多的 10 部电影。
  2. 分析不同性别的用户评分最高的 10 部电影。
  3. 计算指定电影(评分次数最多)的各年龄段用户平均评分。
  4. 统计各类电影中评分最高的 5 部作品。

1.3 技术框架 ​

  • 核心框架:Hadoop MapReduce 3.x
  • 开发工具:IntelliJ IDEA
  • 编程语言:Java
  • 数据存储:HDFS

二、MapReduce 输入输出格式详解 ​

2.1 核心概念 ​

MapReduce 处理数据时,需通过输入格式(InputFormat)读取 HDFS 数据并拆分输入分片(InputSplit),通过输出格式(OutputFormat)将计算结果写入 HDFS。合理选择输入输出格式可大幅提升数据处理效率,尤其适用于多任务串联的复杂场景。

2.2 常用输入格式 ​

输入格式描述键类型值类型适用场景
TextInputFormat默认格式,按行读取文本LongWritable(行偏移量)Text(行内容)普通文本数据处理
KeyValueTextInputFormat按分隔符解析行为键值对(默认制表符)Text(分隔符前内容)Text(分隔符后内容)键值对结构的文本数据
SequenceFileInputFormat读取 Hadoop 序列化二进制文件用户自定义用户自定义多 MapReduce 任务串联(中间结果存储)

关键特性:Hadoop 序列化优势 ​

  • 紧凑:占用存储空间小,适合大数据存储。
  • 快速:读写额外开销低,提升处理效率。
  • 可扩展:支持兼容旧格式数据。
  • 多语言交互:支持跨语言读写存储数据。

2.3 常用输出格式 ​

输出格式描述适用场景
TextOutputFormat默认格式,按行输出键值对需直观查看结果的场景
SequenceFileOutputFormat输出序列化二进制文件作为后续 MapReduce 任务输入
NullOutputFormat忽略输出,无数据写入仅需中间处理,无需最终输出

2.4 实操:筛选日志并生成序列化文件 ​

任务描述 ​

筛选竞赛网站 2024 年 1-2 月的用户访问日志,以 SequenceFile 格式(序列化)输出至 HDFS,为后续分析做准备。

实现步骤 ​

  1. 创建 Maven 项目,引入 Hadoop 核心依赖(hadoop-common、hadoop-mapreduce-client-core 等)。

  2. 编写 Mapper 类:读取日志行,筛选日期为 2024-01 或 2024-02 的记录。

  3. 配置 Driver 类:设置输入格式为 TextInputFormat,输出格式为 SequenceFileOutputFormat。

  4. 打包程序,上传至 Hadoop 集群,执行命令提交任务:

    bash

hadoop jar LogFilter.jar com.hua shang.LogFilter /input/logs /output/sequence_log


5. 验证结果:通过 HDFS 命令查看输出文件,确认序列化格式正确。

------

## 三、Hadoop Java API 实操指南

### 3.1 FileSystem API 核心作用

FileSystem 是 Hadoop 中操作 HDFS 的通用 API,支持文件夹管理、文件上传下载、数据读写等操作,无需依赖命令行,可直接通过 Java 代码实现 HDFS 交互。

### 3.2 基础操作:文件夹管理

#### 3.2.1 获取 FileSystem 实例

```java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import java.net.URI;

// 方式1:使用默认文件系统(core-site.xml配置fs.defaultFS)
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(conf);

// 方式2:指定URI和用户(推荐,避免权限问题)
FileSystem fs = FileSystem.get(URI.create("hdfs://master:9000"), conf, "root");

3.2.2 常用文件夹操作方法 ​

方法功能参数说明
listStatus(Path f)查看目录下所有文件 / 文件夹f:目标目录路径
mkdirs(Path dir)创建多级目录dir:待创建目录路径
exists(Path f)判断路径是否存在f:目标路径

3.2.3 实操案例:查看指定目录下的文件夹 ​

java
// 查看/user/root目录下的子文件夹
Path dirPath = new Path("/user/root");
FileStatus[] fileStatuses = fs.listStatus(dirPath);
for (FileStatus status : fileStatuses) {
    if (status.isDirectory()) {
        System.out.println("文件夹名称:" + status.getPath().getName());
    }
}

3.3 文件操作:上传、下载与删除 ​

3.3.1 核心方法 ​

方法功能关键参数
copyFromLocalFile(Path src, Path dst)本地文件上传至 HDFSsrc:本地路径;dst:HDFS 路径
copyToLocalFile(Path src, Path dst)HDFS 文件下载至本地src:HDFS 路径;dst:本地路径
delete(Path f, boolean recursive)删除文件 / 目录recursive:是否递归删除(目录需设为 true)

3.3.2 实操案例 ​

java
// 1. 本地文件上传HDFS
Path localPath = new Path("D:/data/raceData.csv");
Path hdfsPath = new Path("/user/root/view_log/raceData.csv");
fs.copyFromLocalFile(localPath, hdfsPath);

// 2. HDFS文件下载至本地
Path downloadLocalPath = new Path("D:/download/raceData.csv");
fs.copyToLocalFile(hdfsPath, downloadLocalPath);

// 3. 删除HDFS文件
fs.delete(hdfsPath, false); // false:非递归(文件专用)

3.4 数据读写:普通文件与序列化文件 ​

3.4.1 读取普通文件(FSDataInputStream) ​

java
// 读取HDFS文本文件
Path filePath = new Path("/user/root/view_log/raceData.csv");
FSDataInputStream in = fs.open(filePath);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}
br.close();
in.close();

3.4.2 写入文件(FSDataOutputStream) ​

java
// 向HDFS写入文本文件
Path outputPath = new Path("/user/root/output/test.txt");
FSDataOutputStream out = fs.create(outputPath); // 创建新文件
out.writeBytes("Hello HDFS!");
out.flush();
out.close();

3.4.3 读取序列化文件(SequenceFile.Reader) ​

序列化文件需使用专属 Reader 读取,核心方法如下:

方法描述
getKeyClassName()获取键类型
getValueClassName()获取值类型
next(Writable key, Writable value)读取下一组键值对

实操案例:

java
// 读取SequenceFile格式文件
Configuration conf = new Configuration();
Path seqPath = new Path("/output/sequence_log/part-r-00000");
SequenceFile.Reader reader = new SequenceFile.Reader(conf, SequenceFile.Reader.file(seqPath));

// 初始化键值对象(需与写入时类型一致)
Writable key = (Writable) ReflectionUtils.newInstance(reader.getKeyClass(), conf);
Writable value = (Writable) ReflectionUtils.newInstance(reader.getValueClass(), conf);

// 遍历读取
while (reader.next(key, value)) {
    System.out.println("键:" + key + ",值:" + value);
}
reader.close();

四、MapReduce 程序优化技巧 ​

4.1 自定义键值类型 ​

Hadoop 内置类型(IntWritable、Text 等)无法满足复杂业务需求时,需自定义键值类型,核心需实现Writable(值类型)或WritableComparable(键类型)接口。

4.1.1 实现规范 ​

接口核心方法作用
Writablewrite(DataOutput out)序列化(写入数据)
WritablereadFields(DataInput in)反序列化(读取数据)
WritableComparablecompareTo(T o)键排序(仅键类型需实现)

4.1.2 实操案例:自定义键类型统计用户访问次数 ​

需求:统计用户每天的访问次数,输入数据含 “用户 ID + 访问日期”。

java
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

// 自定义键类型:用户ID+访问日期
public class UserDateKey implements WritableComparable<UserDateKey> {
    private String userId; // 用户ID
    private String visitDate; // 访问日期

    // 无参构造(Hadoop反序列化必需)
    public UserDateKey() {}

    // 带参构造
    public UserDateKey(String userId, String visitDate) {
        this.userId = userId;
        this.visitDate = visitDate;
    }

    // 序列化:按顺序写入数据
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(userId);
        out.writeUTF(visitDate);
    }

    // 反序列化:按写入顺序读取数据
    @Override
    public void readFields(DataInput in) throws IOException {
        this.userId = in.readUTF();
        this.visitDate = in.readUTF();
    }

    // 排序逻辑:先按用户ID升序,再按日期升序
    @Override
    public int compareTo(UserDateKey o) {
        int userIdCompare = this.userId.compareTo(o.userId);
        if (userIdCompare != 0) {
            return userIdCompare;
        }
        return this.visitDate.compareTo(o.visitDate);
    }

    // 重写toString(便于输出查看)
    @Override
    public String toString() {
        return userId + "\t" + visitDate;
    }

    // getter/setter方法
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getVisitDate() { return visitDate; }
    public void setVisitDate(String visitDate) { this.visitDate = visitDate; }
}

4.2 Combiner:本地聚合优化 ​

4.2.1 核心作用 ​

减少 Map 与 Reduce 之间的数据传输量,在 Map 节点本地对输出结果进行聚合(如求和、计数),降低网络开销,提升程序效率。

4.2.2 使用条件 ​

  1. Reducer 输入键值对类型 = 输出键值对类型(Combiner 本质是本地 Reducer)。
  2. 计算逻辑满足 “可叠加性”(如求和、求最大值,求平均值不适用)。

4.2.3 实操案例:统计用户访问次数(添加 Combiner) ​

运行

java
// 1. Combiner类(直接复用Reducer逻辑,求和场景)
public static class UserVisitCombiner extends Reducer<UserDateKey, IntWritable, UserDateKey, IntWritable> {
    @Override
    protected void reduce(UserDateKey key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        context.write(key, new IntWritable(sum));
    }
}

// 2. 在Driver中配置Combiner
job.setCombinerClass(UserVisitCombiner.class);

4.3 Partitioner:数据分区控制 ​

4.3.1 核心作用 ​

将 Map 输出的键值对按指定规则分发到不同的 Reduce 任务中,实现数据的分区处理(如按月份、地区分区)。

4.3.2 默认分区器:HashPartitioner ​

Hadoop 默认使用 HashPartitioner,分区逻辑:

运行

java
public int getPartition(K2 key, V2 value, int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}

4.3.3 自定义 Partitioner 实操 ​

需求:按访问日期的月份,将数据分发到 2 个 Reduce 任务(1 月、2 月各一个)。

运行

java
// 自定义Partitioner类
public class MonthPartitioner extends Partitioner<UserDateKey, IntWritable> {
    @Override
    public int getPartition(UserDateKey key, IntWritable value, int numReduceTasks) {
        String visitDate = key.getVisitDate();
        String month = visitDate.split("/")[1]; // 日期格式:2024/1/1
        // 1月→分区0,2月→分区1
        return "1".equals(month) ? 0 : 1;
    }
}

// 在Driver中配置
job.setPartitionerClass(MonthPartitioner.class);
job.setNumReduceTasks(2); // 分区数必须与Reduce任务数一致

4.4 自定义计数器 ​

4.4.1 核心作用 ​

统计任务运行过程中的关键指标(如错误数据量、有效记录数),辅助调试和结果分析。Hadoop 提供内置计数器(如输入记录数),也支持自定义。

4.4.2 两种实现方式 ​

  1. 枚举类型定义(固定计数器):
java
// 1. 定义枚举
enum LogCounter {
    INVALID_FORMAT, // 格式错误数据
    NON_NUMERIC // 非数字数据
}

// 2. 在Mapper中使用
context.getCounter(LogCounter.INVALID_FORMAT).increment(1);
  1. 动态计数器(灵活定义分组和名称):
java
// 在Mapper中直接定义
context.getCounter("ErrorGroup", "InvalidDate").increment(1);

4.4.3 查看计数器结果 ​

任务执行完成后,通过命令查看:

bash
hadoop job -counter <job-id> ErrorGroup InvalidDate

五、IntelliJ IDEA 打包与远程提交 ​

5.1 传统流程痛点 ​

传统方式需手动打包 Jar→上传集群→执行hadoop jar命令,步骤繁琐且效率低。本节实现 IDEA 中自动打包、远程连接集群,直接提交任务。

5.2 关键配置:参数传递与 ToolRunner ​

5.2.1 Configuration 参数传递 ​

通过 Configuration 类设置任务参数,在 Mapper/Reducer 中获取:

java
// 1. 在Driver中设置参数
Configuration conf = new Configuration();
conf.set("target.movieId", "2858"); // 指定待分析电影ID

// 2. 在Mapper中获取参数(重写setup方法,仅执行一次)
@Override
protected void setup(Context context) throws IOException, InterruptedException {
    Configuration conf = context.getConfiguration();
    String targetMovieId = conf.get("target.movieId");
}

5.2.2 ToolRunner 简化命令行 ​

实现 Tool 接口,简化参数传递,无需手动解析命令行:

java
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

public class MovieAnalysisDriver extends Configured implements Tool {
    @Override
    public int run(String[] args) throws Exception {
        // 任务配置逻辑(替换原main方法)
        Configuration conf = getConf();
        Job job = Job.getInstance(conf, "MovieAnalysis");
        // ... 其他配置(Mapper、Reducer、输入输出路径等)
        return job.waitForCompletion(true) ? 0 : 1;
    }

    public static void main(String[] args) throws Exception {
        // 调用ToolRunner执行任务
        int exitCode = ToolRunner.run(new MovieAnalysisDriver(), args);
        System.exit(exitCode);
    }
}

5.3 自动打包与远程提交 ​

5.3.1 配置集群连接 ​

  1. 将 Hadoop 集群的core-site.xml、yarn-site.xml文件复制到 IDEA 项目的resources目录。
  2. 编写获取集群配置的方法:
java
public static Configuration getMyConfiguration() {
    Configuration conf = new Configuration();
    // 集群配置(与core-site.xml一致)
    conf.set("fs.defaultFS", "hdfs://master:9000");
    conf.set("yarn.resourcemanager.address", "master:8032");
    return conf;
}

5.3.2 自动打包工具类 ​

编写 JarUtil 类,实现程序自动打包(无需手动编译):

java
import org.apache.hadoop.util.RunJar;
import java.io.File;

public class JarUtil {
    public static void jar(String mainClass, String jarPath, String... sourcePaths) throws Exception {
        // 自动生成Jar包逻辑(简化版,完整代码可参考Hadoop RunJar)
        File jarFile = new File(jarPath);
        if (!jarFile.exists()) {
            jarFile.createNewFile();
        }
        // 调用Hadoop RunJar工具打包
        RunJar.main(new String[]{jarPath, mainClass});
    }
}

5.3.3 直接提交任务 ​

在 Driver 中配置自动打包和远程提交:

java
@Override
public int run(String[] args) throws Exception {
    Configuration conf = getMyConfiguration();
    // 设置自动打包(无需手动生成Jar)
    conf.set("mapreduce.job.jar", JarUtil.jar(MovieAnalysisDriver.class.getName(), "target/MovieAnalysis.jar"));
    // ... 其他任务配置
    return job.waitForCompletion(true) ? 0 : 1;
}

5.4 查看运行日志 ​

IDEA 控制台会直接输出任务运行日志,包括 Map/Reduce 进度、计数器结果、错误信息等,便于实时调试。


六、项目实战:电影影评多维度分析 ​

6.1 数据准备 ​

输入数据文件 ​

  • movies.dat:电影信息(movieID、movieType)。
  • ratings.dat:评分信息(userID、movieID、rate)。
  • users.dat:用户信息(userID、gender、age)。

数据格式示例 ​

  • movies.dat:2858::Comedy|Drama
  • ratings.dat:1::2858::4.5::123456789
  • users.dat:1::F::2::1::100000

数据上传 HDFS ​

bash
hadoop fs -mkdir -p /input/movie
hadoop fs -put movies.dat ratings.dat users.dat /input/movie

6.2 实战任务 1:统计评分次数最多的 10 部电影 ​

任务需求 ​

统计所有电影的评分次数,按次数降序排序,取前 10 部并输出电影 ID、类型、评分次数。

实现思路 ​

  1. Mapper:读取ratings.dat,输出键值对(movieID, 1)。
  2. Reducer:聚合相同 movieID 的计数,输出(movieID, 评分次数)。
  3. 二次排序:通过自定义键类型(包含评分次数)实现降序排序,取前 10。
  4. 关联电影类型:读取movies.dat,关联 movieID 获取类型。

核心代码片段(Reducer 聚合) ​

java
public static class RatingCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int count = 0;
        for (IntWritable value : values) {
            count += value.get();
        }
        context.write(key, new IntWritable(count));
    }
}

输出结果示例 ​

plaintext
Scoringtimes{movieID='2858', movieType='Comedy|Drama', rateNum=3428.0}
Scoringtimes{movieID='260', movieType='Action|Adventure|Fantasy|Sci-Fi', rateNum=2992.0}

6.3 实战任务 2:不同性别用户评分最高的 10 部电影 ​

任务需求 ​

按性别分组,计算每部电影的平均评分,降序排序后取每组前 10 部。

实现思路 ​

  1. MapJoin:在 Mapper 中加载movies.dat,关联 movieID 与类型(避免 Shuffle)。
  2. Mapper:读取ratings.dat和users.dat,输出((gender, movieID), (rate, 1))。
  3. Reducer:按(gender, movieID)分组,计算平均评分,输出(gender, movieID, 平均评分)。
  4. 排序与 Top10:自定义排序逻辑,按性别升序、评分降序,取前 10。

关键提示 ​

平均评分计算需统计总评分和评分次数:avgRate = 总评分 / 评分次数。

6.4 实战任务 3:指定电影的各年龄段平均评分 ​

任务需求 ​

以评分次数最多的电影(movieID=2858)为例,计算各年龄段用户的平均评分。

实现思路 ​

  1. 参数传递:在 Driver 中设置target.movieId=2858。
  2. Mapper:筛选 movieID=2858 的记录,输出(age, (rate, 1))。
  3. Reducer:按年龄分组,计算平均评分,输出(age, 平均评分)。

年龄段划分标准 ​

Age 字段值年龄段描述
018 岁以下
118-24 岁
225-34 岁
335-44 岁
445-49 岁
550-55 岁
656 岁及以上

输出结果示例 ​

plaintext
0	4.4
1	4.5
2	4.3
3	4.2

6.5 实战任务 4:各类电影评分最高的 5 部电影 ​

任务需求 ​

拆分电影类型(一部电影可能多类型),按类型分组,计算每部电影的平均评分,取每组前 5 部。

实现思路 ​

  1. Mapper:读取movies.dat拆分类型(如 Action|Adventure 拆分为 Action、Adventure),输出(movieType, (movieID, rate))。
  2. Reducer:按类型分组,计算每部电影的平均评分,输出(movieType, movieID, 平均评分)。
  3. 分组排序:自定义 Partitioner 按类型分区,Reducer 内按评分降序取前 5。

关键提示 ​

电影类型拆分需注意:一部电影对应多个类型时,需输出多条记录(每个类型一条)。


七、教程总结与拓展 ​

7.1 核心知识点回顾 ​

  1. MapReduce 输入输出格式:SequenceFile 适用于中间结果存储,提升效率。
  2. Hadoop Java API:FileSystem 实现 HDFS 全操作,无需依赖命令行。
  3. 程序优化:Combiner 减少数据传输,Partitioner 实现分区处理,自定义计数器辅助调试。
  4. 实操技巧:IDEA 远程提交简化开发流程,ToolRunner 简化参数传递。
  5. 项目实战:多维度分析需结合数据关联、分组排序、聚合计算等核心能力。

7.2 常见问题与解决方案 ​

  1. 权限问题:获取 FileSystem 时指定用户名(如 root),避免 Permission Denied。
  2. 序列化错误:自定义键值类型必须实现无参构造,readFields 与 write 顺序一致。
  3. Reduce 任务数:Partitioner 分区数需与 Reduce 任务数匹配,否则数据倾斜。
  4. 数据倾斜:可通过增加 Reduce 任务数、数据预处理(过滤异常值)缓解。

7.3 拓展方向 ​

  1. 结合 Hive:将 MapReduce 结果导入 Hive,进行更灵活的 SQL 分析。
  2. 实时分析:基于 Spark Streaming 替代 MapReduce,实现影评实时统计。
  3. 情感分析:结合自然语言处理(NLP),分析影评文本的情感倾向(正面 / 负面)。
  4. 可视化展示:使用 ECharts、Tableau 将分析结果可视化,生成直观报表。

交付物提议 ​

要不要我帮你整理一份MapReduce 核心 API 速查表,包含 FileSystem、输入输出格式、优化组件的关键方法和使用场景,方便你随时查阅?

基于 Vite 强力驱动 | 纯静态轻量托管