MapReduce 分析 Youtube 数据
一、实验介绍
1.1 实验内容
MapReduce 是 Hadoop 的计算框架。
在运行一个 MR 程序时,任务过程被分为两个阶段:Map 阶段和 Reduce 阶段,每个阶段都是用键值对作为输入和输出。
本课程的内容包括如何在 eclipse 中搭建 Hadoop 开发环境,并基于 YouTube 数据集根据任务需求编写 MR 程序,然后直接运行和生成 jar 包运行的区别等。
希望本课程能帮助您理解并学会搭建 Hadoop 开发环境和编写 MR 应用的方式。
1.2 先学课程
在学习本课程之前,建议您掌握 Hadoop 的基本使用技巧。以下是推荐的先学课程:
1.3 实验知识点
- eclipse 的 Hadoop 开发环境搭建
- 编写 MR 程序
- 本地运行
- 生成 Jar 包提交 yarn 运行(远程运行)
1.4 实验环境
- Hadoop 2.6.1
- eclipse indigo
- Xfce 终端
1.5 适合人群
本课程属于中级难度级别,适合具有 Hadoop 基础的用户,如果对 MR 计算框架有所了解则能更好地上手本课程。
二、准备工作
2.1 数据集介绍及准备
本课程使用 Youtube 数据集,相关的介绍可以访问 Youtube Dataset 网站查看。目前,一小部分 Youtube 样例数据集整理好并已经上传到实验楼上。
该数据集各个字段的具体含义如下:
字段名 | 解释及数据类型 |
---|---|
video ID | 视频ID:每个视频均有唯一的11位字符串 |
uploader | 上传者用户名:字符串类型 |
age | 视频年龄:整数值,代表视频上传时间与2007年2月15日(Youtube创立日)的时间间隔 |
category | 分类:由上传者选择的视频分类,字符串类型 |
length | 视频长度:整数值 |
views | 浏览量:整数值 |
rate | 视频评分:浮点值 |
ratings | 评分次数:整数值 |
comments | 评论数:整数值 |
related IDs | 相关视频ID:数量不超过20个,每一个相关视频的ID均为单独的一列 |
由于数据集的字段有近20个,为了更直观的显示(一行数据仍旧没有显示完整),附图如下:
每条记录的相关视频数量不尽相同,因此数据不是很整齐。为了完成实验,我们需要过滤掉无效字段,稍后会给出项目详细代码注释。
您可以通过以下方式将数据集文件下载到实验环境中。
请双击打开桌面上的 Xfce 终端,然后输入以下命令:
本实验需要在 hadoop 用户下才能完成,请先切换到 hadoop 用户(命令已在下方提供)。密码为:
hadoop
。
su - l hadoop
wget http://labfile.oss.aliyuncs.com/courses/856/youtubedata.txt
使用 wc -l
命令查看数据行数。
wc -l youtubedata.txt
使用 head -n 1
命令查看数据的第一条记录。
head -n 1 youtubedata.txt
注意:以上仅是一条记录。
2.2 环境准备
由于要写 MapReduce 程序,必然要有 eclispe 下的 Hadoop 开发环境。我们又希望以生成 jar 包的方式提交和运行在 Hadoop 环境中,所以必须安装 Hadoop 。方便的是,实验楼环境里已经设置好了 Hadoop 的相关配置文件,我们只需要格式化 NameNode 即可使用。
双击打开桌面上的 Xfce 终端,按照以下步骤启动:
- 使用
su -l hadoop
命令切换到 hadoop 用户,hadoop 密码为hadoop
(如果已经切换则忽略此步骤)。 - 使用
hadoop namenode -format
命令对 NameNode 进行格式化。
以上命令输出信息太多,最终如下:
使用 start-all.sh
命令启动所有服务。
如果需要输入密码就输入
hadoop
,其他请根据提示进行输入。
使用 jps
命令可以查看到 NameNode、SecondaryNameNode、NodeManager、ResourceManager 和 DataNode 已启动。
三、项目需求描述E
根据已有数据集,编写 MapReduce 程序,分别实现以下功能:
-
从已经上传的视频中,找出评分前10的视频。
示例如下("
\t
"分隔符隔开):CRVmdd_bpA 4.87 BFBixmV-pc 4.81 BArP_-_vXI 4.56 6M41YqM_xk 4.52 CYhSNHbeC8 4.3 8BwggJEPZQ 4.29 FDJ8x3ZKQE 3.75 BbtF1Ysel0 3.4 AdZ6wRHsrE 2.9 AKiJZEOqzQ 1.33
-
从已经上传的视频中,分别统计每一个视频类型下的视频数量。
示例如下("
\t
"分隔符隔开):Science & Technology 45 Sports 23 Travel & Events 144 Nonprofits & Activism 44 People & Blogs 35 ...
四、项目实现
4.1 创建 Java Maven 项目
双击桌面上的图标打开 eclispe,关闭 Welcome 首页。
点击菜单栏中的 File -> New -> Other 。
选择 Maven Project 并点击 Next 按钮,在接下来的页面也点击 Next 按钮。
在 Select an Archetype 步骤时,选择 Artifact Id 为 maven-archetype-quickstart
,并点击 Next 按钮。
分别输入 Group Id、Atifact Id 和 Package 的内容,并点击 Finish 按钮。
右下角会显示项目构建的进度,此过程取决于网速快慢,需要等待一些时间完成。
4.2 修改 pom.xml 文件,添加 Hadoop 依赖
在 pom.xml 文件中添加以下依赖项:
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.1</version>
</dependency>
形成的最终的 pom.xml 文件内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/
2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/
xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>shiyanlou</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>test</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
编写完成后保存。随后会下载依赖,需要 10 分钟左右(取决于网速),请耐心等待完成。
4.3 编写代码
4.3.1 问题 1 :从已经上传的视频中,找出评分前 10 的视频
右键点击 src/main/java 目录下的 shiyanlou.test
包,并在弹出菜单中选择 New -> Class 。
输入包名 shiyanlou.test
和类名 RatingMR
并点击 Finish 按钮。
RatingMR.java
文件的全部代码如下:
package shiyanlou.test;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.IOException;
public class RatingMR {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
@SuppressWarnings("deprecation")
Job job = new Job(conf, "videorating");
job.setJarByClass(RatingMR.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FloatWritable.class);
//设置 map ,reduce 类及 class
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FloatWritable.class);
job.setMapperClass(Map.class);
job.setReducerClass(Reduce.class);
//设置输入输出路径,这里指定的输路径是 HDFS,输出路径是 Linux 的本地文件系统
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
FileInputFormat.addInputPath(job,
new Path("hdfs://localhost:9000/youtubedata.txt"));
FileOutputFormat.setOutputPath(job, new Path("/tmp/hadoop/out"));
job.waitForCompletion(true);
}
//Map类,封装KV键值对 <k1,v1,k2,v2> , k1 是偏移量,v1 是这行数据,输出类型是这行文本 k2 ,浮点类型 v2
public static class Map extends Mapper<LongWritable, Text, Text, FloatWritable> {
//创建Text,FloatWritable 对象
private Text video_name = new Text();
private FloatWritable rating = new FloatWritable();
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] str = line.split("\t");
//使用正则表达式匹配字符串只应包含浮点数,过滤掉小于 7 的无效字段
if (str.length > 7) {
video_name.set(str[0]);
if (str[6].matches("\\d+.+")) {
//强制类型转换
float f = Float.parseFloat(str[6]);
rating.set(f);
}
}
context.write(video_name, rating);
}
}
public static class Reduce extends Reducer<Text, FloatWritable, Text, FloatWritable> {
public void reduce(Text key, Iterable<FloatWritable> values,
Context context) throws IOException, InterruptedException {
float sum = 0;
int l = 0;
for (FloatWritable val : values) {
l += 1;
//累加value
sum += val.get();
}
sum = sum / l;
//取平均值
context.write(key, new FloatWritable(sum));
}
}
}
在终端中输入以下命令,将 youtubedata.txt 上传到 HDFS 文件系统。
hadoop fs -put youtubedata.txt hdfs://localhost:9000/
在 HDFS 文件系统中查看此文件是否上传成功。
hadoop fs -lsr hdfs://localhost:9000/
接下来运行程序,回到 eclipse ,在 src/main/Java
目录下新建一个 log4j.properties
文件用于配置 log4j 的日志输出格式。
注意:是在
src/main/Java
目录而不是shiyanlou.test
包下面新建文件,请注意目录层级。
在新建文件对话框中选择 General -> File ,并点击 Next 按钮。
在 File name 文本框中填入文件名 log4j.properties
,并点击 Finish 按钮。
log4j.properties
文件中应填入的全部内容如下,该文件用于配置 log4j 输出的日志格式:
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
随后,右键点击 RatingMR.java
,在弹出的菜单中选择 Run As -> Java Application 。
在 eclipse 的 console 中查看到任务已经执行完毕后,返回终端查看是否有输出结果。
注意:
/tmp/hadoop/out
目录不需要手动创建,运行 MR 程序会自动产生。
从上图看以看到,用 head 命令只显示了前20行,但其实里面包含了所有的评分数据。那怎样才能获得我们想要的结果呢?我们可以结合 Linux 命令进行获取。
请在终端中输入以下命令。
hadoop fs -cat file:///tmp/hadoop/out/part-r-00000 | sort -n -k 2 -r | head -n 10
注意:
file:///xxx
指定的是本地的 Linux 文件系统。如果不加file:///
,默认会去 HDFS 上去找,而我们在项目代码里指定 Linux 文件系统时则不需要加file:///
,因为只有 HDFS 上不存在指定文件时,才会去本地找。
如结果所示,获得了我们需要的结果,即项目提出的问题 1 得以解决。
4.3.2 问题 2 :从已经上传的视频中,分别统计每一个视频类型下的视频数量
右键点击 shiyanlou.test 包,在弹出的菜单中选择 New -> Class 。
输入类名 VideoCount
并点击 Finish 按钮。
VideoCount.java
文件的全部代码如下:
package shiyanlou.test;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.IOException;
public class VideoCount {
//主函数
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
@SuppressWarnings("deprecation")
Job job = new Job(conf, "categories");
//设置生产 jar 包所使用的类
job.setJarByClass(VideoCount.class);
//设置 Map 类的输入输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置 Reduce 类的输入输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置 Map, Reduce 类的 class 参数
job.setMapperClass(Map.class);
job.setReducerClass(Reduce.class);
//指定格式化用的类型
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
FileInputFormat.addInputPath(job,
new Path("hdfs://localhost:9000/youtubedata.txt"));
FileOutputFormat.setOutputPath(job, new Path("/tmp/hadoop/out2"));
//等待完成
job.waitForCompletion(true);
}
public static class Map extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
//构造文本类Text对象、IntWritable 对象,也可以直接以匿名函数的方式创建
private Text tx = new Text();
//map 的逻辑,使用tab“\ t”分隔符来分割行,并将值存储在String Array中,以使一行中的所有列都存储在字符串数组中
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//拿到每一行数据
String line = value.toString();
String[] str = line.split("\t");
//过滤字段
if (str.length > 5) {
tx.set(str[3]);
}
//输出key,value
context.write(tx, one);
}
}
//编写 reduce,接收 map 阶段传来的 kv 键值对,输出的类型和传进来的类型一致
public static class Reduce extends Reducer<Text, IntWritable, Text, IntWritable> {
//reduce????
public void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
int sum = 0;
//累加求和评分
for (IntWritable v : values) {
sum += v.get()
}
//写出去
context.write(key, new IntWritable(sum));
}
}
}
我们已经上传 youtubedata.txt 到 HDFS 文件系统,因此这里就不需要上传可以直接使用。
仍然是在终端中输入命令,查看 HDFS 中的文件是否上传成功。
hadoop fs -lsr hdfs://localhost:9000/
接下来运行程序,右键点击 VideoCount.java
,在弹出的菜单中选择 Run As -> Java Application 。
返回终端,查看是否有输出结果。
cd /tmp/hadoop/out2/
ls
注意:
/tmp/hadoop/out2
目录不需要手动创建,运行 MR 程序时会自动生成。
我们使用相关的 Linux 命令来查看所有数据:
cat part-r-00000
如上图所示,我们获得了需要的结果,即项目提出的问题 2 得以解决。
4.3.3 知识拓展
如果你是在 Windows 平台写的代码,则可以用通过生成 jar
包的方式提交运行,具体步骤如下:
右键点击项目 test,在弹出的菜单中选择 Export 。
选择 Java -> JAR file 并点击 Next 按钮。
输入 /tmp/hadoop/youtube.jar
并点击 Finish 按钮。
进到终端,查看刚才生成的jar包
这里我们用另外一种方式,即 shell 方式提交和运行程序,不同于 eclispe 。以生成 jar 包的形式提交 hadoop yarn 上运行,语法如下:
hadoop jar 生成的jar包路径 全类名 输入路径input 输出路径output。
由于我们在 eclispe 代码里面输入路径,输出路径已经写死。
FileInputFormat.addInputPath(job, new Path("hdfs://localhost:9000/youtubedata.txt"));
FileOutputFormat.setOutputPath(job, new Path("/tmp/hadoop/out2"));
因此,在提交的时候就可以省略后两项参数。这里我们仍然补齐,但是在这里可以做个改动,尝试将输出路径改为其它目录(hdfs://localhost:9000/output
),并且不提前创建,看是否会报错。
在提交之前,为了避免遇到 Hadoop 经由 Namenode 抛出的 SafeModeException 异常,我们需要先关闭 HDFS 的安全模式。
请在终端中输入以下命令:
hdfs dfsadmin -safemode leave
接着,就可以将 jar 包提交运行了。
hadoop jar /tmp/hadoop/youtube.jar shiyanlou.test.VideoCount hdfs://localhost:9000/youtubedata.txt hdfs://localhost:9000/output
稍作等候,提示 Job 运行成功后,在终端输入如下命令:
hadoop fs -lsr hdfs://localhost:9000/output/
你可能会发现:为什么任务执行成功却找不到文件呢?这是因为在 eclipse 的项目中已经写死路径了,故在命令行执行更换目录的操作并未生效。之前在 eclispe 里面直接运行任务的时候,输出的结果在 Linux 的本地文件系统中。那么这次的输出结果在哪里呢?
可以尝试在终端中使用 HDFS 的操作命令查找,或在 HDFS Web UI 中查找。
经过查找可以发现输出在 HDFS ,使用以下命令查看:
hadoop fs -cat hdfs://localhost:9000/tmp/hadoop/out2/*
可以发现,此处与 eclispe 直接执行的结果完全相同,只是输出路径不同。用 Hadoop 命令执行任务时,输出路径指向 HDFS 。
接下来访问 Hadoop 的 8088 端口来查看任务情况。
双击打开桌面的 Firefox 网络浏览器,输入 localhost:8088
,然后回车。
由于作者在此处提交执行了 3 次,故显示 Application jobs 为 3 。该界面中的信息较多,你还可以尝试使用其他更多的功能。
至此,本课程的实验部分就结束了。
五、实验总结
本课程主要介绍了 Hadoop 开发环境的搭建过程,并基于 YouTube 数据集根据任务需求编写 MR 程序,然后讲解演示了直接运行与生成 jar 包运行 Hadoop 任务的区别等。