HDFS和MapReduce的一些案例
HDFS、HdfsTest、DfsApp 类
HDFS、HdfsTest 和 DfsApp 都是与 Hadoop 分布式文件系统(HDFS)交互的应用程序
HDFS 类
HDFS 类封装了对 Hadoop 文件系统的操作,提供了一系列的方法来执行文件和目录的验证、创建、删除、上传和下载等操作。
PATH_TYPE 枚举:定义了三种类型的路径,分别是 NOT_EXIST(资源不存在)、FILE(资源为文件)和 DIRECTORY(资源为目录)。
config 和 fileSystem:分别用于存储 Hadoop 配置信息和与 HDFS 进行交互的 FileSystem 实例。
构造函数 HDFS(String url, Map<String,String> configItems):初始化 HDFS 类的实例,设置 Hadoop 配置并连接到 HDFS。
exists(String path):检查给定路径的文件或目录是否存在于 HDFS 中。
type(String path):判断给定路径的资源是文件还是目录。
deleteIfExist(String path):如果给定路径的资源存在,就删除它。
mkdirIfNotExist(String path):如果给定路径的目录不存在,就创建它。
uploadFile(String localPath, String dfsPath, boolean recursive, int…maxDepth):将本地文件或目录上传到 HDFS,支持递归上传。
downloadFile(String dfsPath, String localDir):将 HDFS 中的文件下载到本地目录。
close():关闭与 HDFS 的连接。
package com.ybg.dfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
/**
* 封装对于 hadoop 文件系统操作
* Configuration: 为job(driver) 做准备
* FileSystem: 类内共享
* 方法设计
* 目录:验证,创建(递归),删除(递归)
* 文件:上传,下载
*/
public class HDFS {
public enum PATH_TYPE{
NOT_EXIST,FILE,DIRECTORY
}
private Configuration config;
private FileSystem fileSystem;
public HDFS(String url, Map<String,String> configItems) throws IOException, InterruptedException {
config = new Configuration();
if (Objects.nonNull(configItems) && !configItems.isEmpty()) {
configItems.entrySet().forEach(
e->config.set(e.getKey(),e.getValue())
);
}
fileSystem = FileSystem.get(URI.create(url),config,"root");
}
public boolean exists(String path) throws IOException {
return fileSystem.exists(new Path(path));
}
/**
* 获取 hadoop 配置对象
* @return
*/
public Configuration getConfig(){
return config;
}
/**
* 判断参数资源的类型
* @param path
* @return inner enum PATH_TYPE
* NOT_EXIST : 资源不存在
* FILE : 资源为文件
* DIRECTORY : 资源为目录
* @throws IOException
*/
public PATH_TYPE type(String path) throws IOException {
if (!exists(path)) {
return PATH_TYPE.NOT_EXIST;
}
Path res = new Path(path);
if (fileSystem.getFileStatus(res).isFile()) {
return PATH_TYPE.FILE;
}
return PATH_TYPE.DIRECTORY;
}
/**
* 针对文件或目录,存在就删除
* @param path
* @throws IOException
*/
public void deleteIfExist(String path) throws IOException {
Path res = new Path(path);
switch(type(path)){
case FILE:
fileSystem.delete(res,false);
break;
case DIRECTORY:
fileSystem.delete(res,true);
break;
}
}
/**
* 针对目录,不存在则递归创建
* @param path
* @throws IOException
*/
public void mkdirIfNotExist(String path) throws IOException {
if (!exists(path)) {
fileSystem.mkdirs(new Path(path));
}
}
/**
* 递归上传
* @param currDir 当前递归遍历的本地目录
* @param destDir 上传到的目标目录路径
* @param currDepth 当前递归深度
* @param maxDepth 最大递归深度,用于控制递归的深度
* @throws IOException
*/
private void uploadRecursive(File currDir, Path destDir, int currDepth, int maxDepth) throws IOException {
//递归结束的条件
if( maxDepth>0 && currDepth>maxDepth){
return;
}
// 使用listFile()获取当前目录下的文件和子目录
for (File res : currDir.listFiles()) {
if (res.isDirectory()) {
uploadRecursive(res,destDir,currDepth+1,maxDepth);
}else {
fileSystem.copyFromLocalFile(new Path(res.getPath()),destDir);
}
}
}
/**
* 将本地资源(文件or目录)上传到 hadoop 文件系统
* @param localPath 本地要上传的目标路径
* @param dfsPath 文件上传到分布式系统的目标路径
* @param recursive 判断是否可递归
* @param maxDepth :
* 设计为可变参数
* 目录递归上传,最大深度才有用(只有 localPath=directory && recursive=true 时, maxDepth 才有用)
* maxDepth[0]==0 表示全量递归
* @throws IOException
*/
public void uploadFile(String localPath, String dfsPath, boolean recursive, int...maxDepth) throws IOException {
File res = new File(localPath);
if (!res.exists()) {
throw new IOException("you can't upload the localPath that is not exist");
}
if (type(dfsPath)!= PATH_TYPE.DIRECTORY) {
throw new IOException("you can't upload any resource to a file");
}
Path local = new Path(localPath); // local <-->本地
Path remote = new Path(dfsPath); // remote <-->远程
if (res.isFile()) {
fileSystem.copyFromLocalFile(local,remote);
}else if (recursive){
int max = maxDepth[0];
max = max<0 ? 0 : max;
uploadRecursive(res,remote,1,max);
}else{
throw new IOException("you can't upload a directory when parameter recursive was set false");
}
}
/**
* 将 hadoop 文件系统中的资源下载到本地
* @param dfsPath 要下载的文件在分布式系统中的路径
* @param localDir 下载到本地的目标目录路径
* @throws IOException
*/
public void downloadFile(String dfsPath, String localDir) throws IOException {
if (exists(dfsPath)) {
File local;
if ((local=new File(localDir)).exists() && local.isDirectory()) {
Path remote = new Path(dfsPath);
Path localPath = new Path(localDir);
fileSystem.copyToLocalFile(remote, localPath);
}else{
throw new IOException("you can't download a resource to a local file");
}
}else{
throw new IOException("you can't download a remote resource that is not exist");
}
}
/**
* 资源释放
*/
public void close(){
if (Objects.nonNull(fileSystem)) {
try {
fileSystem.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
HdfsTest 类
HdfsTest 类是 HDFS 类的一个测试用例,它演示了如何使用 HDFS 类的方法来执行一系列操作。
创建 Hadoop 配置信息,包括设置副本数和块大小。
创建 HDFS 实例并连接到 HDFS。
检查 /hadoop 目录是否存在,如果不存在则创建 /hadoop/data/del 目录。
上传本地目录 D:\courses\2_phrase_snd\1_hadoop\HomeworkData 到 HDFS 的 /hadoop/data/del 目录,递归深度为 2。
从 HDFS 下载 /hadoop/data/del 目录的内容到本地目录 C:\Users\HUAWEI\我的电脑\temp_dn。
删除 /hadoop/data/del 目录。
异常处理和资源释放。
package com.ybg.dfs;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class HdfsTest {
public static void main(String[] args) {
Map<String,String> config = new HashMap<>(4);
config.put("dfs.replication","1");
config.put("dfs.blocksize","128");
final String URI = "hdfs://single01:9000";
HDFS hdfs = null;
try {
hdfs = new HDFS(URI, config);
System.out.println(hdfs.exists("/hadoop"));
hdfs.mkdirIfNotExist("/hadoop/data/del");
String remote_upload = "/hadoop/data/del";
String local_upload = "D:\\courses\\2_phrase_snd\\1_hadoop\\HomeworkData";
hdfs.uploadFile(local_upload,remote_upload,true,2);
String local_download = "C:\\Users\\HUAWEI\\我的电脑\\temp_dn";
String remote_download = "/hadoop/data/del";
hdfs.downloadFile(remote_download,local_download);
hdfs.deleteIfExist("/hadoop/data/del");
}catch(IOException e){
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(hdfs)) {
hdfs.close();
}
}
}
}
DfsApp 类
DfsApp 类是另一个与 HDFS 交互的应用程序,它演示了如何使用 Hadoop Java API 来列出 HDFS 中的文件和目录。
创建 Hadoop 配置信息,并连接到 HDFS。
检查 /hadoop 目录是否存在,如果存在则列出该目录下的所有文件和子目录,否则打印 “not exist”。
关闭与 HDFS 的连接。
package com.ybg.dfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import java.io.IOException;
import java.net.URI;
public class DfsApp {
public static void main(String[] args) throws IOException, InterruptedException {
final String URL = "hdfs://single01:9000";
Configuration config = new Configuration();
config.set("dfs.replication","1");
config.set("dfs.blocksize","128m");
config.set("hadoop","E:\\hadoop-3.1.3");
FileSystem fs = FileSystem.get(URI.create(URL),config,"root");
if(fs.exists(new Path("/hadoop"))){
System.out.println(fs.mkdirs(new Path("/hadoop/test2/test")));
final RemoteIterator<LocatedFileStatus> it = fs.listFiles(new Path("/hadoop"),true);
while(it.hasNext()){
System.out.println(it.next().getPath().getName());
}
}else{
System.out.println("not exist");
}
fs.close();
}
}
WordCountTest、StoryWordCount 类
WordCountTest 和 StoryWordCount 展示了如何实现一个简单的单词计数器,其中 WordCountTest 是一个单行文本的单词计数器,而 StoryWordCount 是一个使用 Hadoop MapReduce 框架的分布式单词计数器。
WordCountTest 类
WordCountTest 类包含一个 main 方法,用于测试单词计数功能。它使用 StringTokenizer 类来对字符串进行分词,并计算单词的数量。
定义一个包含多个单词的字符串 line。
使用 StringTokenizer 对字符串进行分词。
打印分词后的总数。
遍历分词结果,并打印每个单词。
package com.ybg.dfs;
import java.util.StringTokenizer;
public class WordCountTest {
public static void main(String[] args) {
String line = "I love you baby, are you ok now will you be my girl";
StringTokenizer it = new StringTokenizer(line);
System.out.println(it.countTokens());
while(it.hasMoreElements()){
System.out.println(it.nextToken());
}
}
}
补充:StringTokenizer 的用法
StringTokenizer 是 Java 中的一个类,用于将字符串分解为一系列的标记(Token)。
它允许指定用作分隔符的字符,从而可以灵活地根据需要对字符串进行分词。
StringTokenizer 类位于 java.util 包中。
使用方法
StringTokenizer 的构造函数有两个重载版本:
(1)一个接受字符串和分隔符模式,
(2)另一个只接受字符串作为参数。
(1)分隔符模式可以是单个字符,也可以是两个字符的序列,用来定义分隔符。例如,", " 表示逗号和空格都被视为分隔符。
(2)如果没有提供分隔符模式,StringTokenizer 默认使用空白字符(空格、制表符、换行符等)作为分隔符。
StoryWordCount 类
StoryWordCount 类是一个使用 Hadoop MapReduce 框架的分布式单词计数应用程序。它包含两个内部类 StoryMapper 和 StoryReducer,以及一个 main 方法来配置和运行 MapReduce 作业。
package com.ybg.dfs;
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;
import java.io.IOException;
import java.util.*;
/**
* job ( driver )
*/
public class StoryWordCount {
/**
* Mapper : map(KI, VI){ send(KO,VO) }
* 选择实现 Mapper 类的目的是为了【适配】
* KI : LongWritable 文章中某行的行号
* VI : Text 文章中某行的文本内容
* KO : Text 文章中某行某个单词
* VO : IntWritable 文章中某行某个英文单词出现的次数
*/
static class StoryMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
private Text word = new Text();
private IntWritable count = new IntWritable();
/**
* @param key 输入的键,通常表示输入的数据的偏移位置
* @param value 输入的值,表示一行文本数据
* @param context 上下文对象,提供了与hadoop框架的交互能力,包括写出键值对和设置作业参数
* @throws IOException
* @throws InterruptedException
*/
@Override // 对父类(Mapper类)的重写
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString()
.trim() //移除字符串两端的空白字符
.replaceAll("\\s+", " ") //替换多个连续的空白字符(包括空格、制表符等)为单个空格
.replaceAll("[^a-zA-Z -]", ""); //移除非字母、空格和破折号的字符
// 使用 StringTokenizer 对处理后的文本进行分词
StringTokenizer it = new StringTokenizer(line);
if (it.countTokens()>0) {
Map<String,Integer> wordCount = new HashMap<>(it.countTokens());
while (it.hasMoreElements()) {
String word = it.nextToken();
if (!wordCount.containsKey(word)) {
wordCount.put(word,0);
}
wordCount.put(word,wordCount.get(word)+1);
}
// 输出键值对
wordCount.entrySet().forEach(e->{
word.set(e.getKey());
count.set(e.getValue());
try {
context.write(word,count);// 使用 context.write 方法写出键值对到MapReduce框架,以便后续的处理
} catch (IOException ex) {
ex.printStackTrace();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
});
}
}
}
/**
* Reducer : map(KI, VI){ send(KO,VO) }
* 选择实现 Mapper 类的目的是为了【适配】
* KI : Text 文章中某个单词
* VI : Iterable<IntWritable> values 文章中某单词在【N个段落】中出现的次数
* KO : Text 文章中某行某个单词
* VO : IntWritable 文章中某个单词出现的总次数
*/
static class StoryReducer extends Reducer<Text,IntWritable,Text,IntWritable>{
private IntWritable count = new IntWritable();
@Override
protected void reduce(Text word, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
// 计算单词计数
int num = 0;
Iterator<IntWritable> it = values.iterator();
while (it.hasNext()) {
num += it.next().get();
}
// 设置输出键值对
count.set(num);
context.write(word,count);
}
}
public static void main(String[] args) {
String url = "hdfs://single01:9000";
/**
* 设置 HDFS 配置项
* 这些配置项可以影响到在 HDFS 上存储文件时的数据分布和冗余策略。
* 通过设置 dfs.replication,你可以调整数据的冗余级别,
* 而设置 dfs.blocksize 可以影响到数据块的大小,从而影响到文件的切片大小。
* 这些配置项的选择通常会根据具体的使用场景和需求来进行调整。在这里,items 存储了两个常见的配置项示例。
*/
Map<String,String> items = new HashMap<>(2); //items 是一个用于存储HDFS配置项的HashMap
items.put("dfs.replication","1");
// dfs.replication 是 HDFS 中数据块的复制因子,指定了数据在集群中的复制次数
// 在这里,设置为 "1" 表示不进行额外的数据复制,即只有一个副本
items.put("dfs.blocksize","128M");
// dfs.blocksize 是 HDFS 数据块的大小,这里设置为 "128M" 表示每个数据块的大小为128兆字节
String src = "/hadoop/data/《小王子》英文版.txt";
String dst = "/hadoop/wordcount";
HDFS hdfs = null;
try {
hdfs = new HDFS(url,items);
// 检查文件是否存在并删除目标目录
if(!hdfs.exists(src)){
return;
}
hdfs.deleteIfExist(dst);
/**
* 配置MapReduce任务
* 创建一个新的MapReduce作业(Job对象)并命名为"story_word_count"。
* 设置作业使用的Jar包(setJarByClass),这里指定了包含StoryWordCount类的Jar。
* 设置Reduce任务的数量为1。
* 配置Mapper和Reducer类,以及它们的输出键值对的类型。
*/
Job job = Job.getInstance(hdfs.getConfig(),"story_word_count");
job.setJarByClass(StoryWordCount.class);
job.setNumReduceTasks(1);
job.setMapperClass(StoryMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setReducerClass(StoryReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
/**
* 设置输入输出路径
* 指定输入路径为源文件的路径。
* 指定输出路径为目标目录的路径。
*/
Path srcPath = new Path(url+src);
Path destPath = new Path(url+dst);
FileInputFormat.setInputPaths(job, srcPath);
FileOutputFormat.setOutputPath(job,destPath);
/**
* 提交作业并等待完成
* 提交MapReduce作业,并等待其完成。
* 如果作业成功完成,则退出程序并返回状态码0,否则返回状态码1。
*/
if (job.waitForCompletion(true)) {
System.exit(0);
}else{
System.exit(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(Objects.nonNull(hdfs)){
hdfs.close();
}
}
}
}
StoryMapper
StoryMapper 类继承自 Mapper 类,并实现了 map 方法。它的任务是对输入的文本行进行分词,并为每个单词输出一个键值对,键是单词本身,值是单词出现的次数。
对输入的文本行进行预处理,移除空白字符和非字母字符。
使用 StringTokenizer 对处理后的文本进行分词。
创建一个 HashMap 来存储每个单词及其出现次数。
遍历分词结果,更新单词计数,并输出键值对。
StoryReducer
StoryReducer 类继承自 Reducer 类,并实现了 reduce 方法。它的任务是将所有映射输出的相同单词的计数合并成一个总计数。
对于每个输入单词,计算其在所有输入记录中出现的总次数。
输出单词和其总计数作为最终结果。
main 方法
main 方法负责配置和运行 MapReduce 作业:
设置 Hadoop 配置项,如 dfs.replication 和 dfs.blocksize。
指定输入和输出路径。
创建 Job 对象,设置作业的相关参数,包括 Mapper 和 Reducer 类。
提交作业并等待其完成。
根据作业完成情况退出程序。
释放 HDFS 资源。