我们要写一个气象数据挖掘的程序。气象数据是通过分布在美国各地区的很多气象传感器每隔一小时进行收集,这些数据是半结构化数据且是按照记录方式存储的,因此非常适合使用 MapReduce 程序来统计分析。
我们使用的数据来自美国国家气候数据中心、美国国家海洋和大气管理局(简称 NCDC NOAA),这些数据按行并以 ASCII 格式存储,其中每一行是一条记录。
1998 # 年
03 # 月
09 # 日
17 # 时
11 # 气温
-100 # 湿度
10237 # 气压
60 # 风向
72 # 风速
0 # 天气状况
0 # 每一小时的降雨量
-9999 # 每一小时的降雨量
MapReduce 任务过程分为两个处理阶段:map 阶段和reduce阶段。每个阶段都以键值对作为输入和输出,其类型由我们自己选择。 我们还需要写两个函数:map 函数和reduce 函数。
在这里,map阶段的输入是NCDC NOAA原始数据。我们选择文本格式作为输入格式,将数据集的每一行作为文本输入。键是某一行起始位置相对于文件起始位置的偏移量,不过我们不需要这个信息,所以将其忽略。
我们的map函数很简单。由于我们只对气象站和气温感兴趣,所以只需要取出这两个字段数据。在本实战中,map 函数只是一个数据准备阶段,通过这种方式来准备数据,使 reducer 函数继续对它进行处理:即统计出每个气象站30年来的平均气温。map 函数还是一个比较合适去除已损记录的地方,在 map 函数里面,我们可以筛掉缺失的或者错误的气温数据。
为了全面了解 map 的工作方式,输入以下数据作为演示
1985 07 31 02 200 94 10137 220 26 1 0 -9999
1985 07 31 03 172 94 10142 240 0 0 0 -9999
1985 07 31 04 156 83 10148 260 10 0 0 -9999
1985 07 31 05 133 78 -9999 250 0 -9999 0 -9999
1985 07 31 06 122 72 -9999 90 0 -9999 0 0
1985 07 31 07 117 67 -9999 60 0 -9999 0 -9999
1985 07 31 08 111 61 -9999 90 0 -9999 0 -9999
1985 07 31 09 111 61 -9999 60 5 -9999 0 -9999
1985 07 31 10 106 67 -9999 80 0 -9999 0 -9999
1985 07 31 11 100 56 -9999 50 5 -9999 0 -9999
这些数据,以键/值对的方式作为 map函数的输入,如下所示
(0, 1985 07 31 02 200 94 10137 220 26 1 0 -9999)
(62, 1985 07 31 03 172 94 10142 240 0 0 0 -9999)
(124,1985 07 31 04 156 83 10148 260 10 0 0 -9999)
(186,1985 07 31 05 133 78 -9999 250 0 -9999 0 -9999)
(248,1985 07 31 06 122 72 -9999 90 0 -9999 0 0)
(310,1985 07 31 07 117 67 -9999 60 0 -9999 0 -9999)
(371,1985 07 31 08 111 61 -9999 90 0 -9999 0 -9999)
(434,1985 07 31 09 111 61 -9999 60 5 -9999 0 -9999)
(497,1985 07 31 10 106 67 -9999 80 0 -9999 0 -9999)
(560,1985 07 31 11 100 56 -9999 50 5 -9999 0 -9999)
键(key)是文件中的偏移量,这里不需要这个信息,所以将其忽略。map 函数的功能仅限于提取气象站和气温信息,并将它们输出;map 函数的输出经由 MapReduce 框架处理后,最后发送到reduce函数。这个处理过程基于键来对键值对进行排序和分组。因此在这个示例中,reduce 函数看到的是如下输入:
(03103,[200,172,156,133,122,117,111,111,106,100])
每个气象站后面紧跟着一系列气温数据,reduce 函数现在要做的是遍历整个列表并统计出平均气温:
03103 132
上面就是最终输出结果即每一个气象站历年的平均气温。
上面已经分析完毕,下面我们就着手实现它。这里需要编写三块代码内容:
下面我们来编写 Mapper 类,实现 map() 函数,提取气象站和气温数据
public static class TemperatureMapper extends Mapper< LongWritable, Text, Text, IntWritable> {
/**
* 解析气象站数据
*/
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 每行气象数据
String line = value.toString();
// 每小时气温值
int temperature = Integer.parseInt(line.substring(14, 19).trim());
// 过滤无效数据
if (temperature != -9999) {
FileSplit fileSplit = (FileSplit) context.getInputSplit();
// 通过文件名称提取气象站id
String weatherStationId = fileSplit.getPath().getName().substring(5, 10);
context.write(new Text(weatherStationId), new IntWritable(temperature));
}
}
这个 Mapper 类是一个泛型类型,它有四个形参类型,分别指定 map 函数的输入键、输入值、输出键和输出值的类型。 就本示例来说,输入键是一个长整数偏移量,输入值是一行文本,输出键是气象站id,输出值是气温(整数)。Hadoop 本身提供了一套可优化网络序列化传输的基本类型,而不是使用 java 内嵌的类型。这些类型都在 org.apache.hadoop.io 包中。 这里使用 LongWritable 类型(相当于 Java 的 Long 类型)、Text 类型(相当于 Java 中的 String 类型)和 IntWritable 类型(相当于 Java 的 Integer 类型)。
map() 方法的输入是一个键(key)和一个值(value),我们首先将 Text 类型的 value 转换成 Java 的 String 类型, 之后使用 substring()方法截取我们业务需要的值。map() 方法还提供了 Context 实例用于输出内容的写入。 在这种情况下,我们将气象站id按Text对象进行读/写(因为我们把气象站id当作键),将气温值封装在 IntWritale 类型中。只有气温数据不缺失,这些数据才会被写入输出记录中。
下面我们来编写 Reducer类,实现reduce函数,统计每个气象站的平均气温。
public static class TemperatureReducer extends Reducer< Text, IntWritable, Text, IntWritable> {
/**
* 统计美国各个气象站的平均气温
*/
public void reduce(Text key, Iterable< IntWritable> values,Context context) throws IOException, InterruptedException {
IntWritable result = new IntWritable();
int sum = 0;
int count = 0;
// 统计每个气象站的气温值总和
for (IntWritable val : values) {
sum += val.get();
count++;
}
// 求每个气象站的气温平均值
result.set(sum / count);
context.write(key, result);
}
}
同样,reduce 函数也有四个形式参数类型用于指定输入和输出类型。reduce 函数的输入类型必须匹配 map 函数的输出类型:即 Text 类型和 IntWritable 类型。 在这种情况下,reduce 函数的输出类型也必须是 Text 和 IntWritable 类型,分别是气象站id和平均气温。在 map 的输出结果中,所有相同的气象站(key)被分配到同一个reduce执行,这个平均气温就是针对同一个气象站(key),通过循环所有气温值(values)求和并求平均数所得到的。
/**
* 任务驱动方法
*
* @param arg0
* @throws Exception
*/
@Override
public int run(String[] arg0) throws Exception {
// TODO Auto-generated method stub
// 读取配置文件
Configuration conf = new Configuration();
Path mypath = new Path(arg0[1]);
FileSystem hdfs = mypath.getFileSystem(conf);
if (hdfs.isDirectory(mypath)) {
hdfs.delete(mypath, true);
}
// 新建一个任务
Job job = new Job(conf, "temperature");
// 设置主类
job.setJarByClass(Temperature.class);
// 输入路径
FileInputFormat.addInputPath(job, new Path(arg0[0]));
// 输出路径
FileOutputFormat.setOutputPath(job, new Path(arg0[1]));
// Mapper
job.setMapperClass(TemperatureMapper.class);
// Reducer
job.setReducerClass(TemperatureReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 提交任务
return job.waitForCompletion(true)?0:1;
}
/**
* main 方法
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// 数据输入路径和输出路径
String[] args0 = {
"hdfs://ljc:9000/buaa/weather/",
"hdfs://ljc:9000/buaa/weatherout/"
};
int ec = ToolRunner.run(new Configuration(), new Temperature(), args0);
System.exit(ec);
}
Configuration 类读取 Hadoop 的配置文件,如 site-core.xml、mapred-site.xml、hdfs-site.xml 等。
Job 对象指定作业执行规范,我们可以用它来控制整个作业的运行。我们在 Hadoop 集群上运行这个作业时,要把代码打包成一个JAR文件(Hadoop在集群上发布这个文件)。 不必明确指定 JAR 文件的名称,在 Job 对象的 setJarByClass 方法中传递一个类即可,Hadoop 利用这个类来查找包含它的 JAR 文件,进而找到相关的 JAR 文件。
构造 Job 对象之后,需要指定输入和输出数据的路径。