原創播客,如需轉載請註明出處。原文地址:http://www.cnblogs.com/crawl/p/7751741.html html
----------------------------------------------------------------------------------------------------------------------------------------------------------面試
筆記中提供了大量的代碼示例,須要說明的是,大部分代碼示例都是本人所敲代碼並進行測試,不足之處,請你們指正~數組
本博客中全部言論僅表明博主本人觀點,如有疑惑或者須要本系列分享中的資料工具,敬請聯繫 qingqing_crawl@163.com緩存
-----------------------------------------------------------------------------------------------------------------------------------------------------------服務器
前言:近日有高中同窗求助,一看題目,正好與樓主所學技術相關,便答應了下來,由於正是興趣所在,也沒有管能不能實現。app
話很少說,先上題:框架
同窗告訴我 ,這是浪潮實習生的面試題。ide
樓主先對題目做簡單的說明,給定了一個 corpus.txt 文件做爲語料處理的源文件,文件大小 30.3M,內容即題目要求中的圖片所示,要求對語料文件中出現的詞進行詞頻統計,並把詞頻相同的詞語用 ## 相連(如 研究##落實 1008 ),並按詞頻從大到小排序。題目要求的是根據所學的 Java I/O 處理、集合框架、字符集與國際化、異常處理等基礎知識完成此題,但同窗代表可使用大數據的相關知識,讓樓主感到興趣的是,最近樓主一直在研究 Hadoop,詞頻統計的題目作了很多,便欣然接受同窗的求助。本身挖的坑總要填的,如果進行簡單的詞頻統計,很簡單,涉及到將相同詞頻的詞語排在一行並用 ## 鏈接,由於樓主水平有限,在實現的過程當中仍是遇到了很多的困難 。工具
剛入手此題目,樓主的思路就是先實現一個簡單的詞頻統計,而後在實現簡單詞頻統計的基礎上,對代碼進行修改,實現相同詞頻的詞語放在 一行使用 ## 鏈接。思路很簡單,簡單的詞頻統計實現的也很是順利,但接下的思路實現起來便沒有那麼容易了。先來看一看簡單的詞頻統計這個功能吧。oop
1. 首先先來寫 Mapper 的功能,上代碼:
1 public class WordHandlerMapper extends Mapper<LongWritable, Text, Text, LongWritable> { 2 3 @Override 4 protected void map(LongWritable key, Text value, Context context) 5 throws IOException, InterruptedException { 6 7 String[] strs = StringUtils.split(value.toString(), "\t"); 8 9 for(String str : strs) { 10 int index = str.indexOf("/"); 11 12 if(index < 0) { 13 index = 0; 14 } 15 16 String word = str.substring(0, index); 17 context.write(new Text(word), new LongWritable(1)); 18 19 } 20 21 } 22 23 }
WordHandlerMapper 類的功能,首先此類實現了 Mapper 類,重寫了 mapper 方法,使用默認的 TextOutputFormat 類,將讀取到的一行數據以形參 value 的形式傳入 mapper 方法,第 7 行對這行數據也就是 value 進行處理,以 \t 進行分割,獲得了一個 String 數組, 數組的形式爲:["足協/j", "盃賽/n", "常/d"] 形式,而後 10 行對數組進行遍歷,而後獲取到 /以前的內容,也就是咱們須要統計詞頻的詞語,如 10 - 16 行所示,而後將獲得的詞語傳入 context 的 write 方法,map 程序進行緩存和排序後,再傳給 reduce 程序。
2. 而後再來看 Reduce 程序:
1 public class WordHandlerReducer extends Reducer<Text, LongWritable, Text, LongWritable> { 2 3 4 @Override 5 protected void reduce(Text key, Iterable<LongWritable> values, Context context) 6 throws IOException, InterruptedException { 7 8 long count = 0; 9 10 for(LongWritable value : values) { 11 count += value.get(); 12 } 13 14 context.write(key, new LongWritable(count)); 15 16 } 17 18 }
WordHandlerReducer 的邏輯很簡單,此類繼承了 Reduce,重寫了 reduce 方法,傳入的 key 即須要統計詞頻的詞語,vaues 爲 {1,1,1} 形式,8 行定義了一個計數器,而後對 values 進行加強 for 循環遍歷,使計數器加 1,而後將詞語和詞頻輸出便可。
3. 而後咱們再定義個類來描述這個特定的做業:
1 public class WordHandlerRunner { 2 3 public static void main(String[] args) throws Exception { 4 5 Configuration conf = new Configuration(); 6 Job wcJob = Job.getInstance(conf); 7 8 wcJob.setJarByClass(WordHandlerRunner.class); 9 10 wcJob.setMapperClass(WordHandlerMapper.class); 11 wcJob.setReducerClass(WordHandlerReducer.class); 12 13 wcJob.setOutputKeyClass(Text.class); 14 wcJob.setOutputValueClass(LongWritable.class); 15 16 wcJob.setMapOutputKeyClass(Text.class); 17 wcJob.setMapOutputValueClass(LongWritable.class); 18 19 FileInputFormat.setInputPaths(wcJob, new Path("/wc/srcdata2/")); 20 21 FileOutputFormat.setOutputPath(wcJob, new Path("/wc/wordhandler/")); 22 23 wcJob.waitForCompletion(true); 24 25 } 26 27 }
第 6 行獲得 Job 對象,以後即是對一些基本參數的設置,基本上就是見方法名而知其意了 。 19 和 21 行定義存放元數據的路徑和輸入結果的路徑,23行提交做業。
在 Eclipse 中將程序打成 jar 包,名爲 wordhandler.jar 導出,上傳到 Linux 服務器 ,將原始的語料處理文件上傳到程序中指定了路徑下,經過
hadoop jar wordhandler.jar com.software.hadoop.mr.wordhandler.WordHandlerRunner 命令執行,很快就會執行完畢,而後到輸出路徑中查看輸出結果(圖片展現部分結果):
到此簡單的詞頻統計功能就到此結束了,觀察可知,MapReduce 默認是按輸出的 key 進行排序的。獲得的數據距離題目要求的結果還有很大的懸殊,那麼剩下須要進一步實現的還有兩處,一處是將詞頻相同的詞語放到一行並用 ## 鏈接,第二處就是對詞頻進行排序(按從大到小) 。
樓主的思路是,排序確定是要放到最後一步實現,若先進行排序,在對相同詞頻的詞語處理的話,頗有可能會打亂以前的排序。那麼,如今就是對詞頻相同的詞語進行處理了,使它們顯示在一行並用 ## 鏈接。在實現這個功能時,樓主遇到了困難,主要是對 TextOutputFarmat 默認只讀取一行數據意識不夠深刻,走了許多的彎路,好比樓主相到修改 Hadoop 的源碼,讀取多行數據等,但因爲樓主水平有限,結果以失敗了結。到此時已是夜裏將近十二點了,由於到次日還要早起,因此就沒再熬夜,暫時放下了。
次日中午,樓主想到了倒排索引,使用倒排索引實現的思路便一點一點造成。把咱們以前獲得的數據讀入,而後將詞頻當作 key,這樣 Mapper 程序便會在 Reduce 執行以前進行緩存和分類,思路來了,便立刻動手實現。
1. 仍是先來 Mapper 的功能:注意,此次讀入的數據是咱們以前獲得的按默認的形式排好序,並統計出詞頻的數據。
1 public class WordHandlerMapper2 extends Mapper<LongWritable, Text, LongWritable, Text> { 2 3 @Override 4 protected void map(LongWritable key, Text value, Context context) 5 throws IOException, InterruptedException { 6 7 String[] strs = StringUtils.split(value.toString(), "\t"); 8 9 String text = strs[0]; 10 11 long count = Long.parseLong(strs[1]); 12 13 context.write(new LongWritable(count), new Text(text)); 14 15 } 16 17 }
Map 的功能很簡單,咱們須要輸出的 key 是 LongWritable 類型,value 是 Text 類型,即 [205, {"檢驗", "加入", "生存"}] 這種類型。第 7 行一樣是對一行的數據進行拆分,而後獲得 詞語(text) 和 詞頻(count),而後 第 13 行進行輸出便可,很簡單。
2. Reduce 程序的功能:
1 public class WordHandlerReducer2 extends Reducer<LongWritable, Text, Text, LongWritable> { 2 3 //key: 3 values: {"研究","落實"} 4 @Override 5 protected void reduce(LongWritable key, Iterable<Text> values, Context context) 6 throws IOException, InterruptedException { 7 8 String result = ""; 9 10 for(Text value : values) { 11 result += value.toString() + "##"; 12 13 } 14 15 context.write(new Text(result), key); 16 17 } 18 19 }
Reduce 的邏輯比以前的稍微複雜一點,從 Mapper 中輸入的數據格式爲 [205, {"檢驗", "加入", "生存"}] 類型,咱們但願輸出的格式爲:[檢驗##加入##生存 205], 重寫的 reduce 方法傳入的 values 即 {"檢驗", "加入", "生存"} 類型,第十行對 values 進行遍歷,11 行向 result 中追加,便可獲得咱們須要的結果,而後 15 行進行輸出。
3. 而後再來定義一個類來描述此做業,
1 public class WordHandlerRunner2 { 2 3 public static void main(String[] args) throws Exception { 4 5 Configuration conf = new Configuration(); 6 Job wcJob = Job.getInstance(conf); 7 8 wcJob.setJarByClass(WordHandlerRunner2.class); 9 10 wcJob.setMapperClass(WordHandlerMapper2.class); 11 wcJob.setReducerClass(WordHandlerReducer2.class); 12 13 wcJob.setOutputKeyClass(Text.class); 14 wcJob.setOutputValueClass(LongWritable.class); 15 16 wcJob.setMapOutputKeyClass(LongWritable.class); 17 wcJob.setMapOutputValueClass(Text.class); 18 19 FileInputFormat.setInputPaths(wcJob, new Path("/wc/wordhandler/")); 20 21 FileOutputFormat.setOutputPath(wcJob, new Path("/wc/wordhandleroutput/")); 22 23 wcJob.waitForCompletion(true); 24 25 } 26 27 }
這個類與以前的那個 Job 描述類很相似,使用的 Job 的方法沒有變化,方法的參數只作稍微修改便可,樓主標紅的行即須要進行修改的行。
而後是一樣的步驟,在 Eclipse 中將程序打成 jar 包導出,也叫 wordhandler.jar,而後上傳到 Linux 服務器中,使用
hadoop jar wordhandler.jar com.software.hadoop.mr.wordhandler2.WordHandlerRunner2 進行運行,同過 Map 和 Reduce 的處理後,進入相應的目錄下,查看結果(圖片展現部分結果):
咱們分析一下獲得的結果,是否是距離題目要求的輸出結果更接近了一步,可是還差點事,一個是每一行的最後多了一個 ##,這個好解決,在生成字符串的時候判斷該詞語是否爲最後一個便可,另外一個就是題目要求詞頻按從大到小的順序輸出,而咱們的輸出順序是從小到大。明確了問題以後,繼續開動吧。
排序問題是使用 Hadoop 進行詞頻處理的常見問題了,實現起來並不困難。說一說思路,由於咱們這裏是默認讀取一行,那麼咱們構造一個 Word 類,此類有屬性 text (內容),和(count)詞頻,此類須要實現 WritableComparable 接口,重寫其中的方法,使用咱們自定義的排序方式便可。既然思路明確了,那咱們一步一步的實現。
1. 先定義一個 word 類:
1 public class Word implements WritableComparable<Word> { 2 3 private String text; 4 5 private long count; 6 7 public Word() {} 8 9 public Word(String text, long count) { 10 super(); 11 this.text = text; 12 this.count = count; 13 } 14 15 public String getText() { 16 return text; 17 } 18 19 public void setText(String text) { 20 this.text = text; 21 } 22 23 public long getCount() { 24 return count; 25 } 26 27 public void setCount(long count) { 28 this.count = count; 29 } 30 31 @Override 32 public void write(DataOutput out) throws IOException { 33 out.writeUTF(text); 34 out.writeLong(count); 35 } 36 37 @Override 38 public void readFields(DataInput in) throws IOException { 39 text = in.readUTF(); 40 count = in.readLong(); 41 } 42 43 @Override 44 public int compareTo(Word o) { 45 return count > o.getCount() ? -1 : 1; 46 } 47 48 }
此類須要實現 WritableComparable 接口,重寫第 32 行的 write 方法,第 38 行的 readFields 方法,第 44 行 compareTo 方法,32 行和 38 行的方法是 Hadoop 中序列化相關的方法,44 行 compareTo 方法纔是咱們自定義排序方式的方法。值的注意的是,write 方法中和 readFields 方法中屬性的序列化和反序列化的順序必須一致,即 3三、34 和 3九、40 行的屬性須要對應。而後 compareTo 中 第 45 行實現自定義的從大到小的排序便可。
2. Mapper 類的功能:
1 public class WordHandlerMapper3 extends Mapper<LongWritable, Text, Word, NullWritable> { 2 3 @Override 4 protected void map(LongWritable key, Text value, Context context) 5 throws IOException, InterruptedException { 6 7 String[] strs = StringUtils.split(value.toString(), "\t"); 8 9 String text = strs[0]; 10 11 long count = Long.parseLong(strs[1]); 12 13 Word word = new Word(text, count); 14 15 context.write(word, NullWritable.get()); 16 17 } 18 19 }
咱們定義 Mapper 的輸出的 key 爲 Word 類型是排序成功的關鍵,在 mapper 方法中常規的拆分一行數據,得到到相應的字段,而後第 13 行封裝爲一個 Word 對象,第 15 行輸出便可,樓主定義 Mapper 的輸出的 vlaue 爲 NullWritable 類型,思路爲只要輸出的 key 爲 Word 型,那麼咱們就能夠獲取到須要的信息了。
2. 再來看 Reduce 的功能:
1 public class WordHandlerReducer3 extends Reducer<Word, NullWritable, Text, LongWritable> { 2 3 @Override 4 protected void reduce(Word key, Iterable<NullWritable> values, Context context) 5 throws IOException, InterruptedException { 6 context.write(new Text(key.getText()), new LongWritable(key.getCount())); 7 } 8 9 }
Reduce 的功能再簡單不過了,獲得的 key 是一個一個的 Word ,6 行獲取相應的字段輸出便可。
3. 再來描述排序這個特定的做業,代碼與以前的相似,只作稍微修改便可,代碼樓主就不貼出來了。這樣咱們排序的功能就實現了,而後在 Linux 中經過命令運行,將獲得的結果導出到 Windows 中,重命名爲 postagmodel.txt 便可,此文件共 1163 行,如今貼一下部分結果的圖片:
結果出來了,基本與題目要求吻合,樓主鬆了一口氣。
在實現功能以後樓主稍微總結了一下:
可能因爲經驗不足,遇到問題不知如何解決,積累經驗尤其重要,畢竟經驗這個問題不是短期內造成的,多學多敲多練是根本;
而後,一個功能或者一個需求的實現,何爲簡單,何爲困難,樓主認爲最終若是咱們實現了這個功能或需求,回過頭來看,它就是簡單的,此時也有多是熟練度的問題,使它蒙上了那層困難的面紗,遇到困難,別放棄,學會短暫性捨棄,過段時間再撿起來,可能靈感就來了。
還須要說一點,此功能的實現樓主的思路和方法可能不是最好的,也有可能會有不妥的地方存在,歡迎你們一塊交流學習,如有不足之處,還請指出,留言、評論、郵箱樓主都可。