实验要求:
本实验要做在实验5的多个文件数据上执行
-
读取单个文件中的key(可以是从1到100000的随机整数),统计“单个”文件中数据按位分布:
个位数(1-9):?%
十位数(10-99):?%
百位数(100-999):?%
千位数(1000-9999):?%
万位数(10000-99999):?%
十万位数(10000-99999):?%
-
修改上述程序1,输出对所有文件执行程序1汇总后,找到分布中Top10的数据分布以及所在文件信息;
-
修改上述程序1,不区分文件的情况下进行统计所有文件中以上的数据按位分布情况。
实验环境
-
Ubuntu16.04 - VMware Workstation
-
eclipse 3.8.1
-
Java 1.8
-
Hadoop 2.7.1
基础文件
之前生成的随机文件,以key,value的格式存储,key是一个随机整数,value记录了这是第几个文件中的第几条记录,以下是生成代码。
package cn.edu.ncu.young;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
public class RandomFileGenerator {
private static final int NUM_FILES = 100;//最大文件数
private static final int MAX_LINES = 1000;//文件最大行数
private static final int MAX_KEY = 1000000;
private static final int MAX_STRING_LENGTH = 20;
private static final String FILE_PREFIX = "file_";//文件前缀
private static final String FILE_SUFFIX = "";//文件名后缀
private static final String DIRECTORY = "/CloudComputing/code_5/generated_files/";//文件生成路径
public static void main(String[] args) throws IOException {
Random random = new Random();
for (int i = 0; i < NUM_FILES; i++) {
String fileName = DIRECTORY + FILE_PREFIX + i + FILE_SUFFIX;
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
for (int j = 0; j < MAX_LINES; j++) {
int key = random.nextInt(MAX_KEY);
String value = "the recorder #" + j + " in " + FILE_PREFIX + i;
writer.write(key + "," + value);
writer.newLine();
}
writer.close();
System.out.println(fileName+" generate success");
}
}
private static String generateRandomString(Random random, int maxLength) {
int length = random.nextInt(maxLength) + 1;
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = (char) (random.nextInt(26) + 'a');
builder.append(c);
}
return builder.toString();
}
}
查看部分文件内容
1. 统计单个文件分布
Mapper类(DigitMapper):继承自Hadoop的Mapper类,负责将输入的文本数据进行处理。在map()方法中,首先将输入的文本数据进行拆分,得到键和记录值。然后,将键转换为整数,并计算其位数(通过对数运算)。最后,将位数作为键,将固定的值1作为值进行输出。
Reducer类(DigitReducer):继承自Hadoop的Reducer类,负责对Mapper的输出进行汇总和处理。在reduce()方法中,对于每个位数键,遍历对应的值集合,并将它们进行累加得到总数。然后,将位数和总数存储在一个HashMap中。
cleanup()方法:在Reducer类的cleanup()方法中,对HashMap中的数据进行处理,计算每个位数的百分比。首先,计算所有值的总和。然后,遍历HashMap,对每个位数计算其对应值的百分比,并按照位数的不同,将百分比和对应的位数信息输出。
主函数(main):在主函数中,首先配置Hadoop作业的相关参数,包括输入路径、输出路径和作业名称。然后,设置Mapper和Reducer类,指定输出键值对的类型。最后,运行Hadoop作业并等待完成。
package mypackage;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
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;
public class DigitDistribution {
/**
* DigitMapper类
* @author siliyoung
*
*/
public static class DigitMapper extends Mapper<LongWritable, Text, IntWritable, IntWritable> {
private static final IntWritable ONE = new IntWritable(1);
private IntWritable digitCount = new IntWritable();
/**
* map函数 首先将输入的文本数据进行拆分,得到键和记录值。然后,将键转换为整数,并计算其位数(通过对数运算)。最后,将位数作为键,将固定的值1作为值进行输出。
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString().trim();//读取每一行
String[] parts = line.split(",");//将数据按‘,’这个分隔符划分成两部分,前面一部分是key值(随机的整数),后面一部分是value值
if (parts.length == 2) {
String keyValue = parts[0].trim();//这一步只需要关注key值
//String record = parts[1].trim();
int num = Integer.parseInt(keyValue);
digitCount.set((int) Math.log10(num));//获取到数字的位数,0-10->0,11-99->1....
context.write(digitCount, ONE);//写入(位数,1) 传给reducer
}
}
}
/**
* DigitReducer 类
* @author siliyoung
*
*/
public static class DigitReducer extends Reducer<IntWritable, IntWritable, Text, Text> {
//创建一个辅助容器countMap来记录每一个位数对应有多少个记录
private Map<Integer, Integer> countMap;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//初始化countMap
countMap = new HashMap<>();
}
/**
* reducer函数 对于每个位数键,遍历对应的值集合,并将它们进行累加得到总数。然后,将位数和总数存储在一个HashMap中
*/
@Override
protected void reduce(IntWritable key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//统计每一个key(位数)的value(出现次数1)总和
int sum = 0;
for (IntWritable value : values) {//遍历每一个key值的value
sum += value.get();//前面map函数中传递的value值都是1,这一句也可以写成sum+=1,但是写value.get()更加严谨一些
}
countMap.put(key.get(), sum);//将统计得到的数据存入countMap中
}
/**
* cleanup 函数 对HashMap中的数据进行处理,计算每个位数的百分比
*/
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
//计算所有值的总和,即一共有多少条记录
int total = 0;
for (int count : countMap.values()) {
total += count;
}
//设置百分数的格式
DecimalFormat df = new DecimalFormat("0.00%");
//定义每一个位数对应key值,即输出结果的头部
Text zero= new Text("个位数(1-9):");//个位
Text one= new Text("十位数(10-99):");//十位
Text two = new Text("百位数(100-999):");//百位
Text three = new Text("千位数(1000-9999):");//千位
Text four = new Text("万位数(10000-99999):");//万位
Text five = new Text("十万位数(100000-999999):");//十万位
//遍历countMap,得到每一个位数和位数对应的value
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
int digit = entry.getKey();
int count = entry.getValue();
//计算百分比
double percentage = (double) count / total;
//匹配位数并写入文件
if(digit == 0){
context.write(zero, new Text(df.format(percentage)));
}else if(digit == 1){
context.write(one, new Text(df.format(percentage)));
}else if(digit == 2){
context.write(two, new Text(df.format(percentage)));
}else if(digit == 3){
context.write(three, new Text(df.format(percentage)));
}else if(digit == 4){
context.write(four, new Text(df.format(percentage)));
}else if(digit == 5){
context.write(five, new Text(df.format(percentage)));
}
}
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Digit Distribution");
job.setJarByClass(DigitDistribution.class);
job.setMapperClass(DigitMapper.class);
job.setReducerClass(DigitReducer.class);
job.setOutputKeyClass(IntWritable.class);
job.setOutputValueClass(IntWritable.class);
//输入路径,处理单个文件就填一个文件名,处理多个文件就可以填一个文件夹名,结果都没有问题
FileInputFormat.addInputPath(job, new Path("/CloudComputing/code_5/generated_files/file_0"));
//输出路径
FileOutputFormat.setOutputPath(job, new Path("/CloudComputing/code_8/output/output1(0)"));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
输出结果查看
控制台信息
六月 19, 2023 10:31:21 上午 org.apache.hadoop.mapreduce.Job monitorAndPrintJob
信息: Job job_local472126281_0001 completed successfully
六月 19, 2023 10:31:22 上午 org.apache.hadoop.mapreduce.Job monitorAndPrintJob
信息: Counters: 30
File System Counters
FILE: Number of bytes read=89958
FILE: Number of bytes written=586440
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
Map-Reduce Framework
Map input records=1000
Map output records=1000
Map output bytes=8000
Map output materialized bytes=10006
Input split bytes=115
Combine input records=0
Combine output records=0
Reduce input groups=4
Reduce shuffle bytes=10006
Reduce input records=1000
Reduce output records=4
Spilled Records=2000
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=63
Total committed heap usage (bytes)=335683584
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
File Input Format Counters
Bytes Read=34786
File Output Format Counters
Bytes Written=132
输出文件
2. 统计top10
Mapper类(DigitMapper):继承自Hadoop的Mapper类,负责将输入的文本数据进行处理。在map()方法中,首先获取当前输入行所属的文件名。然后,将输入行进行拆分,得到键和记录值。将键转换为整数,并计算其位数。最后,将文件名作为键,位数作为值进行输出。
Reducer类(DigitReducer):继承自Hadoop的Reducer类,负责对Mapper的输出进行汇总和处理。在reduce()方法中,对于每个文件名键,遍历对应的位数值集合,并统计每个位数值的出现次数。将位数值和对应的出现次数存储在countMap中。
汇总所有文件的数据分布:在reduce()方法中,对于每个位数值和其对应的出现次数,将其存储在top10Map中。top10Map是一个TreeMap,它会自动按照出现次数进行排序,并保持大小为10,即只保留Top10的数据分布。
cleanup()方法:在Reducer类的cleanup()方法中,对top10Map进行遍历,并按照出现次数的降序输出Top10的数据分布信息。同时,计算每个数据分布的百分比,通过countMap中的数据来计算总数,然后计算每个数据分布的百分比。
主函数(main):在主函数中,首先配置Hadoop作业的相关参数,包括输入路径、输出路径和作业名称。然后,设置Mapper和Reducer类,指定输出键值对的类型。最后,运行Hadoop作业并等待完成。
package mypackage;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.lang.Integer;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
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;
/**
* 统计top10
* @author siliyoung
*
*/
public class Distribute2 {
/**
* DigitMapper 类
* @author siliyoung
*
*/
public static class DigitMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final IntWritable digitCount = new IntWritable();//记录位数
private final Text fileKey = new Text();//记录文件名
/**
* map 函数 将输入的文本数据进行处理
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String fileName = ((FileSplit) context.getInputSplit()).getPath().getName(); // 获取文件名
fileKey.set(fileName);
String line = value.toString().trim();
String[] parts = line.split(",");//将输入行进行拆分,得到键和记录值
if (parts.length == 2) {
int keyValue = Integer.parseInt(parts[0].trim());
int numDigits = (int) Math.floor(Math.log10(keyValue));//将键转换为整数,并计算其位数
digitCount.set(numDigits);
//将文件名作为键,位数作为值进行输出
context.write(fileKey, digitCount);
}
}
}
/**
* DigitReducer 类 负责对Mapper的输出进行汇总和处理
* @author siliyoung
*
*/
public static class DigitReducer extends Reducer<Text, IntWritable, Text, Text> {
private final Map<Integer, Integer> countMap = new HashMap<>();
private final TreeMap<Integer, String> top10Map = new TreeMap<>();
/**
* reduce 函数
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
countMap.clear();
//对于每个文件名键,遍历对应的位数值集合
for (IntWritable value : values) {
int digit = value.get();
countMap.put(digit, countMap.getOrDefault(digit, 0) + 1);//并统计每个位数值的出现次数,并更新
}
// 汇总所有文件的数据分布
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
int digit = entry.getKey();
int count = entry.getValue();
String zero= new String("个位数(1-9)");//个位
String one= new String("十位数(10-99)");//十位
String two = new String("百位数(100-999)");//百位
String three = new String("千位数(1000-9999)");//千位
String four = new String("万位数(10000-99999)");//万位
String five = new String("十万位数(100000-999999)");//十万位
String distribute = five ;
switch(digit){
case 0:
distribute = zero;
break;
case 1:
distribute = one;
break;
case 2:
distribute = two;
break;
case 3:
distribute = three;
break;
case 4:
distribute = four;
break;
case 5:
distribute = five;
break;
default:
break;
}
top10Map.put(count, distribute + " : " + key.toString());
// 保持Top10的大小为10
if (top10Map.size() > 10) {
top10Map.pollFirstEntry();
}
}
}
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
for (Map.Entry<Integer, String> entry : top10Map.descendingMap().entrySet()) {
int count = entry.getKey();
String info = entry.getValue();
DecimalFormat df = new DecimalFormat("0.00%");
//total
int total = 0;
for (int c : countMap.values()) {
total += c;
}
double percentage = (double) count / total;
context.write(new Text(info), new Text(df.format(percentage)));
}
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Digit Distribution");
job.setJarByClass(Distribute2.class);
job.setMapperClass(DigitMapper.class);
job.setReducerClass(DigitReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path("/CloudComputing/code_5/generated_files/"));
FileOutputFormat.setOutputPath(job, new Path("/CloudComputing/code_8/output/output_2"));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
输出结果
3. 统计所有文件分布
仍然使用实验1的思路和代码,然后将输入文件的目录修改为所有文件目录的文件夹即可。
//输入路径,处理单个文件就填一个文件名,处理多个文件就可以填一个文件夹名,结果都没有问题
FileInputFormat.addInputPath(job, new Path("/CloudComputing/code_5/generated_files"));
输出结果