R&Dトレンドレポート

第10回 MapReduce処理をやってみよう![実践編]

この記事を読むのに必要な時間:およそ 10.5 分

コンパイル

ソースのコンパイルですが,以下のようにクラスパスを指定してコンパイルします。

$ javac -cp $HADOOP_HOME/hadoop-0.20.2-core.jar:$HADOOP_HOME/myproj/lib/cmecab-1.7.jar  J2ch.java

jarファイルの作成は以下のように行います。

$ jar cvf J2ch.jar J2ch*.class

実行

hadoopコマンドにJ2ch.jarを渡して実行させます。

hadoop jar プログラムのjar メイン関数を含んだクラス 第一引数 第二引数

という構文になります。

$ hadoop jar J2ch.jar J2ch 2ch_4.txt 2ch_4_result

ここでは,第一引数に入力ファイル名,第二引数に出力ディレクトリを指定しています。入力ファイル,出力ディレクトリはいずれもHDFS上となります。出力先ディレクトリがすでにあるとエラーになるので,

$ hadoop dfs -rmr 2ch_4_result

というふうにディレクトリを削除しましょう。

出力データ例

「カイジ 人生逆転ゲーム」の実況スレのデータを処理しました。

※1287144000が21:00を表します。1287151200が23:00を表します(放送開始から放送終了まで⁠⁠。

ざっと眺めてみると,カイジという主人公の名前,利根川という相手役の名前,船井という人物,佐原,ざわざわ,土下座,焼,と原作をご存じの方や映画を見た方はピンとくるキーワードが並んでいるんではないでしょうか?? カット,原作という単語が多いのも原作からカットされているシーンが多いぞ,ということでしょうか。出現回数だけの解析結果としては傾向がつかめていると思います。おもしろいですね !

1287144000	1287144000,53,カイジ
1287144000	1287144000,37,俺
1287144000	1287144000,35,女
1287144000	1287144000,34,カット
1287144000	1287144000,26,出
1287144600	1287144600,118,利根川
1287144600	1287144600,114,これ
1287144600	1287144600,110,www
1287144600	1287144600,108,ざわ
1287144600	1287144600,108,船井
1287145200	1287145200,161,カット
1287145200	1287145200,156,原作
1287145200	1287145200,154,映画
1287145200	1287145200,151,カイジ
1287145200	1287145200,126,展開
1287145800	1287145800,225,カット
1287145800	1287145800,201,ビール
1287145800	1287145800,182,カイジ
1287145800	1287145800,169,ペ
1287145800	1287145800,164,映画
1287146400	1287146400,96,映画
1287146400	1287146400,87,カット
1287146400	1287146400,66,落
1287146400	1287146400,65,これ
1287146400	1287146400,60,原作
1287147000	1287147000,126,落
1287147000	1287147000,101,カイジ
1287147000	1287147000,95,原作
1287147000	1287147000,90,これ
1287147000	1287147000,84,佐原
1287147600	1287147600,135,ざわざわ
1287147600	1287147600,115,ざわ
1287147600	1287147600,69,カード
1287147600	1287147600,54,耳
1287147600	1287147600,53,これ
1287148200	1287148200,105,カイジ
1287148200	1287148200,91,CM
1287148200	1287148200,85,利根川
1287148200	1287148200,81,ざわざわ
1287148200	1287148200,76,顔
1287148800	1287148800,88,遠藤
1287148800	1287148800,82,カイジ
1287148800	1287148800,65,映画
1287148800	1287148800,60,原作
1287148800	1287148800,51,何
1287149400	1287149400,198,キタ
1287149400	1287149400,91,利根川
1287149400	1287149400,85,カイジ
1287149400	1287149400,84,血
1287149400	1287149400,76,ざわ
1287150000	1287150000,91,土下座
1287150000	1287150000,88,焼
1287150000	1287150000,31,カイジ
1287150000	1287150000,29,利根川
1287150000	1287150000,22,演技
1287150600	1287150600,118,カイジ
1287150600	1287150600,116,映画
1287150600	1287150600,93,カット
1287150600	1287150600,84,原作
1287150600	1287150600,62,遠藤
1287151200	1287151200,52,映画
1287151200	1287151200,47,カイジ
1287151200	1287151200,35,原作
1287151200	1287151200,33,思
1287151200	1287151200,32,人

全プログラムリスト

最後に全リストを載せておきます。アスキーアートの除外や意味のない単語の削除など上の例では省いていた関数を載せいてます。

J2ch.java
import java.io.IOException;
import java.util.Hashtable;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.*;

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.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.util.regex.*;

import net.moraleboost.mecab.Node;
import net.moraleboost.mecab.Tagger;
import net.moraleboost.mecab.impl.StandardTagger;
import net.moraleboost.mecab.MeCabException;

public class J2ch
{
	static final int TIMESEC = 600; // 丸める秒数

	static class J2chMapper extends Mapper <LongWritable, Text, IntWritable, Text>
	{
		private Boolean isAA( String s)
		{
			Pattern p = Pattern.compile(".*[/ ̄_\ ]{2}+.*");
			Matcher m = p.matcher(s);

			return m.matches();
		}

		private String noGomi( String s )
		{
			Pattern p = Pattern.compile("(h?ttp://|h?ttps://|sssp://){1}[\\w\\.\\-/:\\#\\?\\=\\&\\;\\%\\~\\+]+|>?>[0-9- ]+");
			Matcher m = p.matcher(s);
			String t = m.replaceAll("");

			return t;
		}


		private Boolean isGomi( String s )
		{
			Pattern p = Pattern.compile("^[!()━・゚?/:;0-9a-zA-Z∀Д\\`=│*1234567890.\"'#~{}&,:;+<>%$_$%”!#&、;:?|{}@`*+_?><|∴▼△▲▽-]$");
			Matcher m = p.matcher(s);

			return m.matches();
		}


		public void map(LongWritable key, Text value, Context context)
			throws IOException, InterruptedException
		{
			// 入力データ
			// 1288400498\t94\t主侍 </b>◆HIPPER/a9g <b>\t+rXxBAvc\t\t\t 友近とアジアンかって
			String line = value.toString();
			String[] arr = line.split("\t");

			int _time = Integer.parseInt(arr[0]);
			int time = (int)Math.floor(_time/TIMESEC)*TIMESEC; // TIMESECで丸める。

			if ( arr.length != 7 ) return;

			String b = arr[6]; // ボディーテキストを取得

			if ( isAA(b) ) return; // AAを含むモノは削除

			String body = noGomi(b); // ゴミを削除(URL,>>123など)

			Tagger tagger = new StandardTagger("UTF-8", "");

			Node node = tagger.parse(body);

			// 名詞,副詞可能,*,*,*,*,本日,ホンジツ,ホンジツ
			while (node.hasNext())
			{

				String surface = node.next();
				String feature = node.feature();
				String featureArr[] = feature.split(",");

				if ( featureArr[0].equals("名詞" ))
				{
					context.write(new IntWritable(time), new Text(surface));
				}

			}
		}
	}

	static class J2chReducer extends Reducer <IntWritable, Text, IntWritable, Text>
	{
		public void reduce(IntWritable key, Iterable<Text> values, Context context)
			throws IOException, InterruptedException
		{
			Hashtable<String, Integer> kvs = new Hashtable<String, Integer>();

			for( Text value : values )
			{
				String k = value.toString();
				if ( kvs.containsKey( k ))
				{
					Integer n = kvs.get(k) + 1;
					kvs.put(k, n);
				}
				else
				{
					kvs.put(k, 1);
				}
			}


			ArrayList entries = new ArrayList(kvs.entrySet());

			Collections.sort(entries, new Comparator(){
				public int compare(Object obj1, Object obj2){
					Map.Entry ent1 =(Map.Entry)obj1;
					Map.Entry ent2 =(Map.Entry)obj2;
					int val1 = Integer.parseInt(ent1.getValue().toString());
					int val2 = Integer.parseInt(ent2.getValue().toString());
					return (val2 - val1);
				}
			});


			for( int i = 0; i < entries.size() && i < 5; i++ )
			{
				String word = (String)((Map.Entry)entries.get(i)).getKey();
				int cnt = Integer.parseInt(((Map.Entry)entries.get(i)).getValue().toString());
				context.write(key, new Text( key + "," + cnt + "," + word));
			}

		}
	}

	public static void main(String[] args ) throws Exception
	{
		if ( args.length != 2 )
		{
			System.err.println("Usage: hogehoge");
			System.exit(-1);
		}

		Job job = new Job();
		job.setJarByClass(J2ch.class);

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

		job.setMapperClass(J2chMapper.class);
		job.setReducerClass(J2chReducer.class);

		job.setOutputKeyClass(IntWritable.class);
		job.setOutputValueClass(Text.class);


		System.exit(job.waitForCompletion(true) ? 0 : 1 );
	}
}

次回はJavaを使わないMapReduceの書き方をご紹介します!

著者プロフィール

脇本武士(わきもとたけし)

都内中小IT企業(メイサンソフト(株))に所属。某大手自動車会社でのシステム開発,運用を経て,現在は研究開発部署に席をお借りしています。DB周りの保守サポート,ウェブ技術開発を主に手がけてきました。現在は大規模計算フレームワークの活用とKVSに注目しています。普段はOS Xを使用していますが一番よく使うアプリはTerminalです(笑)。

R&Dトレンドレポート(てくらぼ)