【大数据处理与分析】MapReduce:06 MapReduce编程实践

【作者主页】Francek Chen

【专栏介绍】⌈ ⌈ ⌈大数据技术原理与应用 ⌋ ⌋ ⌋专栏系统介绍大数据的相关知识,分为大数据基础篇、大数据存储与管理篇、大数据处理与分析篇、大数据应用篇。内容包含大数据概述、大数据处理架构Hadoop、分布式文件系统HDFS、分布式数据库HBase、NoSQL数据库、云数据库、MapReduce、Hadoop再探讨、数据仓库Hive、Spark、流计算、Flink、图计算、数据可视化,以及大数据在互联网领域、生物医学领域的应用和大数据的其他应用。

【GitCode】专栏资源保存在我的GitCode仓库:https://gitcode.com/Morse_Chen/BigData_principle_application

文章目录


在大数据处理架构 Hadoop 中,我们已经介绍了如何在单台机器上搭建伪分布式 Hadoop 环境,并介绍了如何利用 Hadoop 自带的实例程序来分析数据。现在我们来介绍如何编写基本的 MapReduce 程序帮助自己实现数据分析。下面首先给出基本任务要求,然后阐述如何编写 MapReduce 程序来实现任务要求。这里采用的 Hadoop 版本为 3.1.3。

一、任务要求

在大数据处理架构 Hadoop 中,我们运行 Hadoop 自带的实例程序统计了"input"文件夹下所有文件中每个单词出现的次数。在 MapReduce:04 实例分析:WordCount 中我们介绍了用 MapReduce 程序实现单词出现次数统计的基本思路和具体执行过程。下面我们介绍如何编写具体实现代码以及如何运行程序。

首先,我们在本地创建两个文件,即文件 A 和 B。

文件 A 的内容如下:

text 复制代码
China is my motherland
I love China 

文件 B 的内容如下:

text 复制代码
I am from China

假设 HDFS 中已经创建好了一个 input 文件夹,现在把文件 A 和 B 上传到 HDFS 中的 input 文件夹下(注意,上传之前,请清空 input 文件夹中原有的文件)。现在的目标是统计 input 文件夹下所有文件中每个单词的出现次数,也就是说,程序应该输出如下形式的结果:

text 复制代码
I            2 
is           1 
China        3 
my           1 
love         1 
am           1 
from         1 
motherland   1

接下来,我们编写 MapReduce 程序来实现这个功能,主要包括以下几个步骤。

(1)编写 Map 处理逻辑。

(2)编写 Reduce 处理逻辑。

(3)编写 main 方法。

(4)编译打包代码以及运行程序。

二、编写 Map 处理逻辑

为了把文档处理成我们希望的效果,首先需要对文档进行切分。通过前面的内容我们可以知道,数据处理的第一个阶段是 Map 阶段,在这个阶段中文本数据被读入并进行基本的分析,然后以特定的键值对的形式进行输出,这个输出将作为中间结果,继续提供给 Reduce 阶段作为输入数据。

在本例中,我们通过继承类 Mapper 来实现 Map 处理逻辑。首先,为类 Mapper 设定好输入类型以及输出类型。这里,Map 的输入是 <key,value> 形式,其中,key 是文本文件中一行的行号,value 是该行号对应的文件中的一行内容。实际上,在代码逻辑中,key 值并不需要用到。对于输出的类型,我们希望在 Map 部分完成文本分割工作,因此输出应该为 <单词,出现次数> 的形式。于是,最终确定的输入类型为 <Object,Text>,输出类型为 <Text,IntWritable>。其中,除了 Object 以外,都是 Hadoop 提供的内置类型。为实现具体的分析操作,我们需要重写 Mapper 中的 map 函数。以下为 Mapper 类的具体代码:

java 复制代码
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
    private static final IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public TokenizerMapper() {
    }

    public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
        StringTokenizer itr = new StringTokenizer(value.toString());
        while (itr.hasMoreTokens()) {
            this.word.set(itr.nextToken());
            context.write(this.word, one);
        }
    }
}

在上述代码中,实现 Map 逻辑的类名称为 TokenizerMapper。在 TokenizerMapper 类中,首先将需要输出的两个变量 one 和 word 初始化。对于变量 one,可以将其直接初始化为 1,表示某个单词在文档中出现过。在 Map 函数中,前两个参数是函数的输入,value 为 text 类型,是指每次读入文本的一行,Object 类型的 key 则是指该行数据在文本中的行号。在我们这个简单的示例中,key 其实并没有被明显地用到。然后,通过 StringTokenizer 这个类以及其自带的方法,对 value 变量(即文本中的一行)进行拆分,拆分后的单词存储在 word 中,one 作为单词计数。实际上,在函数的整个执行过程中,one 的值一直为 1。Context 是 Map 函数的一种输出方式,通过写该变量,可以直接将中间结果存储在其中。按照这样的处理逻辑,第二个文件在 Map 后输出的中间结果如下:

text 复制代码
<"I",1> 
<"am",1> 
<"from",1> 
<"China",1>

三、编写 Reduce 处理逻辑

在 Map 部分得到中间结果后,接下来进入 Shuffle 阶段。在这个阶段中 Hadoop 自动将 Map 的输出结果进行分区、排序、合并,然后分发给对应的 Reduce 任务来处理。下面给出 Shuffle 过程后的结果,这也是 Reduce 任务的输入数据:

text 复制代码
<"I",<1,1>> 
<"is",1> 
...... 
<"from",1> 
<"China",<1,1,1>> 

Reduce 阶段需要对上述数据进行处理并得到我们最终期望的结果。其实,在这里已经可以很清楚地看到 Reduce 需要做的事情,就是对输入结果中的数字序列进行求和。下面给出 Reduce 处理逻辑的具体代码:

java 复制代码
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    private IntWritable result = new IntWritable();

    public IntSumReducer() {
    }

    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable val : values) {
            sum += val.get();
        }
        this.result.set(sum);
        context.write(key, this.result);
    }
}

类似于 Map 的实现,这里仍然需要继承 Hadoop 提供的类并实现其接口(重写其方法)。这里编写的类的名字为 IntSumReducer,它继承自类 Reducer。至于 Reduce 过程的输入/输出类型,从上面代码中可以发现,它们与 Map 过程的输出类型本质上是相同的。在代码的开始部分,我们设置变量 result 来记录每个单词的出现次数。为了具体实现 Reduce 部分的处理逻辑,我们需要重写 Reducer 类所提供的 Reduce 函数。在 Reduce 函数中我们可以看到,其输入类型较 Map 过程的输出类型发生了一点小小的变化,即 IntWritable 变量经过 Shuffle 阶段处理后,变为 Iterable 容器。在 Reduce 函数中,我们会遍历这个容器,并对其中的数字进行累加,最终可以得到每次单词总的出现次数。同样,在输出时,我们仍然使用 Context 类型的变量存储信息。当 Reduce 过程结束时,就可以得到最终需要的数据了。

四、编写 main 方法

为了让 TokenizerMapper 和 IntSumReducer 类能够协同工作,我们需要在主函数中通过 Job 类设置 Hadoop 程序运行时的环境变量,以下是具体代码:

java 复制代码
public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    String[] otherArgs = (new GenericOptionsParser(conf, args)).getRemainingArgs();
    if (otherArgs.length < 2) {
        System.err.println("Usage: wordcount <in> [<in>...] <out>");
        System.exit(2);
    }

    Job job = Job.getInstance(conf, "word count"); // 设置环境参数
    job.setJarByClass(WordCount.class); // 设置整个程序的类名
    job.setMapperClass(WordCount.TokenizerMapper.class); // 添加 Mapper 类
    job.setReducerClass(WordCount.IntSumReducer.class); // 添加 Reducer 类
    job.setOutputKeyClass(Text.class); // 设置输出类型
    job.setOutputValueClass(IntWritable.class); // 设置输出类型

    for (int i = 0; i < otherArgs.length - 1; ++i) {
        FileInputFormat.addInputPath(job, new Path(otherArgs[i])); // 设置输入文件
    }

    FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1])); // 设置输出文件
    System.exit(job.waitForCompletion(true) ? 0 : 1);
}

在代码的开始部分,我们通过类 Configuration 获得程序运行时的参数情况,并将它们存储在 String\[\] otherArgs 中。随后,我们通过类 Job 设置环境参数。首先,设置整个程序的类名为 WordCount.class(这个类包含了单词统计的全部实现代码)。然后,添加已经写好的 TokenizerMapper 类和 IntSumReducer 类。接下来,还需要设置整个 Hadoop 程序的输出类型,即 Reduce 输出结果 <key, value> 中 key 和 value 各自的类型。最后,根据之前已经获得的程序运行时的参数,设置输入/输出文件路径。

五、编译打包代码以及运行程序

下面给出 WordCount 类的完整代码:

java 复制代码
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class WordCount {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        String[] otherArgs = (new GenericOptionsParser(conf, args)).getRemainingArgs();
        if (otherArgs.length < 2) {
            System.err.println("Usage: wordcount <in> [<in>...] <out>");
            System.exit(2);
        }

        Job job = Job.getInstance(conf, "word count");
        job.setJarByClass(WordCount.class);
        job.setMapperClass(TokenizerMapper.class);
        job.setCombinerClass(IntSumReducer.class);
        job.setReducerClass(IntSumReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        for (int i = 0; i < otherArgs.length - 1; ++i) {
            FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
        }

        FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1]));
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
        private static final IntWritable one = new IntWritable(1);
        private Text word = new Text();

        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            StringTokenizer itr = new StringTokenizer(value.toString());
            while (itr.hasMoreTokens()) {
                word.set(itr.nextToken());
                context.write(word, one);
            }
        }
    }

    public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        private IntWritable result = new IntWritable();

        public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }
            result.set(sum);
            context.write(key, result);
        }
    }
}

大家可能对程序最初引用的许多外部包有些疑惑,其实它们大部分是 Hadoop 自己的组件,也被称为 Hadoop 的 API,这些包的基本功能见表1。
表1 WordCount类中引用的包的基本功能

功能
org.apache.hadoop.conf 定义了系统参数的配置文件处理方法
org.apache.hadoop.fs 定义了抽象的文件系统 API
org.apache.hadoop.mapreduce Hadoop 分布式计算框架 MapReduce 的实现,包括任务的分发调度等
org.apache.hadoop.io 定义了通用的 I/O API,用于针对网络、数据库、文件等数据对象进行读写操作等

由于在安装 Hadoop 之前,我们已经安装了 Java 程序(JDK),所以这里可以直接用 JDK 包中的工具对代码进行编译。在执行下面操作之前,请把当前工作目录设置为 Hadoop 的安装目录,即"/usr/local/hadoop"目录:

bash 复制代码
cd /usr/local/hadoop 
export CLASSPATH="/usr/local/hadoop/share/hadoop/common/hadoop-common-3.1.3.jar: /usr/local/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-client-core-3.1.3.jar:/usr/local/hadoop/share/hadoop/common/lib/commons-cli-1.2.jar:$CLASSPATH" 
javac WordCount.java

如果系统环境找不到 javac 程序的位置,那么请使用 JDK 中的绝对路径。

编译之后,在文件夹下可以发现有 3 个".class"文件,这是 Java 的可执行文件。此时,我们需要将它们打包并命名为 WordCount.jar,命令如下:

bash 复制代码
jar -cvf WordCount.jar *.class 

到这里,我们就得到像 Hadoop 自带实例一样的 JAR 包了,可以运行得到结果。在运行程序之前,需要启动 Hadoop,包括 HDFS 和 MapReduce。启动 Hadoop 之后,我们可以运行程序,命令如下:

bash 复制代码
./bin/hadoop jar WordCount.jar WordCount input output 

最后,我们可以运行下面命令查看结果:

bash 复制代码
./bin/hadoop fs -cat output/*

小结

本文介绍了基于 Hadoop 3.1.3 编写 WordCount MapReduce 程序的完整流程。任务目标是统计 HDFS 中 input 文件夹下文件 A 和 B 内每个单词的出现次数。实现分为四步:编写 Mapper 类(TokenizerMapper),将每行文本切分为单词,输出 <单词,1> 键值对;编写 Reducer 类(IntSumReducer),对相同单词的计数进行累加求和;编写 main 方法,通过 Job 类设置运行环境,包括指定 Mapper、Reducer、输入输出路径及类型;最后编译打包成 JAR 文件并提交执行。程序使用了 Hadoop 的核心 API 包,如 org.apache.hadoop.mapreduce 等。通过此实例,大家可以掌握 MapReduce 程序的基本编写方法,理解 Map 阶段的数据切分与 Reduce 阶段的聚合汇总逻辑。

欢迎 点赞👍 | 收藏⭐ | 评论✍ | 关注🤗

相关推荐
小马爱打代码1 小时前
Kafka消息队列监控:Topic积压、吞吐量、Broker负载及消费者组全观测
分布式·kafka
王小王-1231 小时前
基于 Hadoop 的二手房数据分析与可视化平台项目展示
大数据·hadoop·数据分析·大数据房价分析·二手房价格预测·hive房价数据分析
FII工业富联科技服务2 小时前
“可持续灯塔工厂”技术解密:AI+IoT如何落地端到端碳管理闭环
大数据·人工智能·物联网·ai·数据分析·自动化·制造
轻口味2 小时前
轻规划鸿蒙开发实战10:分布式数据同步深度博弈,UserId 隔离与并发数据冲突消解机
分布式·华为·harmonyos·鸿蒙
Leo.yuan2 小时前
数据建模怎么做?一文解析8种经典数据建模方法
大数据·数学建模
master3362 小时前
git仓库通过脚本完成多个远程仓库同步
大数据·git·elasticsearch
Solis程序员2 小时前
Raft:分布式系统的定海神针
java·分布式·kafka·rabbitmq·agent·raft
果丁智能2 小时前
物联网智能锁落地实践:破解网约房、民宿身份核验与远程权限管控难题
大数据·人工智能·物联网·智能家居