動手寫的第一個MapReduce程序--wordcount

引語:

    之前運行過了hadoop官方自帶的第一個例子wordcount,這次我們自己手寫一個,這個相當於是編程語言中的helloworld一樣.
首先我們瞭解一下我們要寫的MapReduce是處理的哪個部分,我們知道hadoop處理文件是先將要處理的文件拆分成很多個部分,分別處理完成,最後再將結果給匯聚起來,
形成最終的處理結果.(也就是分治法的思想)我們接下來舉個單詞統計的例子,看看我們寫的代碼是整個MapReduce過程中的哪些部分.

具體MapReduce的過程例子

首先咱們有這麼一個文件,文件內容如下:
hello world hello java
hello hadoop
很簡單的一個文件就兩行.那麼hadoop是怎麼做單詞統計的呢?我們用步驟來描述下:
第一步:讀取這個文件,按行來將這個文件每一行的單詞給拆分出來,然後形成很多key/value的結果,處理完就是這樣
<hello,1>
<world,1>
<hello,1>
<java,1>
<hello,1>
<hadoop,1>
第二步:排序
排序完會變成這樣的結果
<hadoop,1>
<hello,1>
<hello,1>
<hello,1>
<java,1>
<world,1>
第三步:合併
合併後的結果如下
<hadoop,1>
<hello,1,1,1>
<java,1>
<world,1>
第四步:匯聚結果
<hadoop,1>
<hello,3>
<java,1>
<world,1>

到第四步完成,單詞統計其實也就完成了.看完這個具體的實例,想必大家對mapreduce的處理過程有了一個比較清晰的理解.
然後我們要知道第二步和第三步是hadoop框架幫助我們完成的,我們實際上需要寫代碼的地方是第一步和第四步.
第一步對應的就是Map的過程,第四步對應的是Reduce的過程.

編寫mapreduce代碼

現在我們要做的就是完成第一步和第四步的代碼
1.創建項目


創建一個普通的java項目就行,然後一路next點過去,項目名自己取一個.
2.引入到時用到的hadoop的包,我這裏用的是hadoop-3.2.0的版本,需要引入哪些包呢?
要引入的包:
(1).hadoop目錄下share/hadoop/common下的包(除了那個test的包,官方的測試例子,可以不需要引入)
(2).和上一條一樣的common下lib中的包
(3).hadoop目錄下share/hadoop/mapreduce下的包
(4).和上一條一樣mapreduce下的lib中的包
然後在idea中引入這些包,點擊File->Project Structure->Modules
點擊右邊的小加號來引入剛纔說的那些jar包

3.引入包完成以後,我們創建一個叫WordCount的java文件,然後開始敲代碼
這裏直接貼一下代碼,__要注意import的部分,是不是和我一樣?__因爲好些個名字一樣的類,來自於不同的jar,容易弄錯.

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 java.io.IOException;
import java.util.StringTokenizer;

/**
 * @author wxwwt
 * @since 2019-09-15
 */
public class WordCount {

    /**
     * Object      : 輸入文件的內容
     * Text        : 輸入的每一行的數據
     * Text        : 輸出的key的類型
     * IntWritable : 輸出value的類型
     */
    private static class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> {
        @Override
        protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            StringTokenizer itr = new StringTokenizer(value.toString());
            while (itr.hasMoreTokens()) {
                context.write(new Text(itr.nextToken()), new IntWritable(1));
            }
        }
    }

    /**
     * Text         :  Mapper輸入的key
     * IntWritable  :  Mapper輸入的value
     * Text         :  Reducer輸出的key
     * IntWritable  :  Reducer輸出的value
     */
    private static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            int count = 0;
            for (IntWritable item : values) {
                count += item.get();
            }
            context.write(key, new IntWritable(count));
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        // 創建配置
        Configuration configuration = new Configuration();
        // 設置hadoop的作業  jobName是WordCount
        Job job = Job.getInstance(configuration, "WordCount");
        // 設置jar
        job.setJarByClass(WordCount.class);
        // 設置Mapper的class
        job.setMapperClass(WordCountMapper.class);
        // 設置Reducer的class
        job.setReducerClass(WordCountReducer.class);
        // 設置輸出的key和value類型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 設置輸入輸出路徑
        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        // 待job執行完  程序退出
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

}

Mapper程序:

/**
 * Object      : 輸入文件的內容
 * Text        : 輸入的每一行的數據
 * Text        : 輸出的key的類型
 * IntWritable : 輸出value的類型
 */
private static class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> {
    @Override
    protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
        StringTokenizer itr = new StringTokenizer(value.toString());
        while (itr.hasMoreTokens()) {
            context.write(new Text(itr.nextToken()), new IntWritable(1));
        }
    }
}

context是全局的上下文,先使用了StringTokenizer將value(也就是每行的數據)按照空格分成了很多份,StringTokenizer如果沒有傳入指定的分割符的話,默認會將

" \t\n\r\f" 空格製表符換行符等符號作爲分隔符,然後使用nextToken()來遍歷這個按照空格分割的字符串.context.write(new Text(itr.nextToken()), new IntWritable(1));

的意思就是將key/value寫入到上下文中.
注:在hadoop編程中String是Text,Integer是IntWritable.這是hadoop自己封裝的類.記住就好了,使用起來和原來的類差不多
這裏就是寫入了key爲Text的單詞,和value爲Writable的1(統計數量).

Reduce程序:

/**
 * Text         :  Mapper輸入的key
 * IntWritable  :  Mapper輸入的value
 * Text         :  Reducer輸出的key
 * IntWritable  :  Reducer輸出的value
 */
private static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int count = 0;
        for (IntWritable item : values) {
            count += item.get();
        }
        context.write(key, new IntWritable(count));
    }
}

reduce完成的是第四步的內容,我們看看上面的實例過程就回知道此時的輸入參數大概是這樣
<hello,1,1,1>
所以這裏會有一個遍歷values的過程,就是將這三個1給累加起來了.

程序入口:

public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    // 創建配置
    Configuration configuration = new Configuration();
    // 設置hadoop的作業  jobName是WordCount
    Job job = Job.getInstance(configuration, "WordCount");
    // 設置jar
    job.setJarByClass(WordCount.class);
    // 設置Mapper的class
    job.setMapperClass(WordCountMapper.class);
    // 設置Reducer的class
    job.setReducerClass(WordCountReducer.class);
    // 設置輸出的key和value類型
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);

    // 設置輸入輸出路徑
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    // 待job執行完  程序退出
    System.exit(job.waitForCompletion(true) ? 0 : 1);
}

程序入口這裏其實看註釋就已經比較清楚了,都是設置一些mapreduce需要的參數和路徑之類的,
照着寫就行了.這裏稍微要注意一點的就是

FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));

我們回顧一下之前運行hadoop的第一個程序的時候,命令大概是
hadoop jar WordCount.jar /input/wordcount/file1 /output/wcoutput
後面的兩個參數就是文件的輸入路徑和輸出路徑,如果咱們代碼修改了參數的位置或者有其他參數的操作.
就要對應好args下標的位置.

4.指定jar包運行的入口
代碼完成後咱們就可以打包了
先選擇File -> Project Structure -> Artifacts -> + -> JAR -> From modules with dependencies

然後選擇剛纔WordCount的main

再點擊Build -> Build Artifacts

然後會彈出一個框 選擇Build

之後會在項目中生成一個out的目錄,在裏面找到我們需要的WordCount.jar,上傳到hadoop所在的服務器.
到這裏基本上就結束了,因爲後面運行的步驟和我之前的文章時一樣的,可以參考:hadoop運行第一個實例wordcount

### 注意事項:
有可能直接運行

  hadoop jar WordCount.jar /input/wordcount/file1  /output/wcoutput

會失敗,報一個異常:

Exception in thread "main" java.io.IOException: Mkdirs failed to create /XXX/XXX
  at org.apache.hadoop.util.RunJar.ensureDirectory(RunJar.java:106)
  at org.apache.hadoop.util.RunJar.main(RunJar.java:150)

類似上面這樣的.

這時候需要刪除掉jar包裏面的License文件夾和裏面的東西,可以參考這個鏈接:[stackoverflow](https://stackoverflow.com/que...
)
查看下jar中license的文件和文件夾
jar tvf XXX.jar | grep -i license
然後刪除掉 META-INF/LICENSE裏面的內容
zip -d XXX.jar META-INF/LICENSE

總結:

1.瞭解了mapReduce的運行步驟,這樣知道了我們只需要寫map和reduce的過程,中間步驟hadoop框架已經做了處理,以後其他的程序也可以參考這個步驟來寫
2.hadoop中String是Text,Integer是IntWritable這個要記住,用錯了會報異常的
3.報 Mkdirs failed to create /XXX/XXX異常的時候先檢查是不是路徑有問題,沒有的話就刪除掉jar包中的META-INF/LICENSE

參考資料:

1.https://hadoop.apache.org/doc...
2.https://stackoverflow.com/que...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章