「魔法少女まどか☆マギカ」の台詞をJavaScriptでMapReduceしてGoogle Chart APIでグラフ出力したよ!
- 作者: Tom White,玉川竜司,兼田聖士
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/07/23
- メディア: 大型本
- 購入: 9人 クリック: 182回
- この商品を含むブログ (24件) を見る
概要
- 「魔法少女まどか☆マギカ」の台詞をNLTK(Natural Language Toolkit)で解析する - Yuta.Kikuchiの日記
- SpiderMonkeyでのコマンドラインJavascript - Yuta.Kikuchiの日記
- CentOSでHadoopを使ってみる - Yuta.Kikuchiの日記
以前に「魔法少女まどか☆マギカ」の台詞をNLTK(Natural Language Toolkit)で解析することに挑戦しましたが、解析結果の集計グラフが奇麗に表示されませんでした。今回はそれを改善すべく手法を変えて挑戦します。グラフ化はNLTKではなくGoogle Chart APIを利用します。Google Chart Tools - Google Code またHadoopのMapReduceにJavaScriptを用い、Google Chart APIとのデータ連携をしやすいようにします。以下に大まかな処理の流れを記述します。
- SpiderMonkeyをCentOSに設定
- 魔法少女まどか☆マギカの台詞をPythonでスクレイピング
- NLTKによる分かち書き
- JavaScriptによるMapReduce
- HadoopでMapReduce
- Google Chart APIでMapReduce結果をグラフ化
SpiderMonkeyをCentOSに設定
参考
HadoopのMapReduceをJavaScriptで行うためにCentOSにSpiderMonkeyを設定します。本家サイトのinstall手順を参考にしました。https://developer.mozilla.org/en/Building_only_SpiderMonkey
ソースコード取得/解凍
$ wget http://ftp.mozilla.org/pub/mozilla.org/js/js-1.8.0-rc1.tar.gz $ tar -xzf js-1.8.0-rc1.tar.gzbuild/install
$ cd js/src/ $ make BUILD_OPT=1 -f Makefile.ref $ sudo make BUILD_OPT=1 JS_DIST=/usr/local -f Makefile.ref export
「魔法少女まどか☆マギカ」の台詞をPythonでスクレイピング
スクレイピングPythonコード
pythonコードで魔法少女まどか☆マギカ台詞をスクレイピングします。以前より台詞掲載ページが増えていたのでURLを追加しました。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys,re,urllib,urllib2 urls = ( 'http://www22.atwiki.jp/madoka-magica/pages/170.html', 'http://www22.atwiki.jp/madoka-magica/pages/175.html', 'http://www22.atwiki.jp/madoka-magica/pages/179.html', 'http://www22.atwiki.jp/madoka-magica/pages/180.html', 'http://www22.atwiki.jp/madoka-magica/pages/200.html', 'http://www22.atwiki.jp/madoka-magica/pages/247.html', 'http://www22.atwiki.jp/madoka-magica/pages/244.html', 'http://www22.atwiki.jp/madoka-magica/pages/249.html', 'http://www22.atwiki.jp/madoka-magica/pages/250.html', 'http://www22.atwiki.jp/madoka-magica/pages/252.html', 'http://www22.atwiki.jp/madoka-magica/pages/241.html', 'http://www22.atwiki.jp/madoka-magica/pages/254.html' ) f = open( './madmagi.txt', 'w' ) opener = urllib2.build_opener() ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/ 534.51.22' referer = 'http://www22.atwiki.jp/madoka-magica/' opener.addheaders = [( 'User-Agent', ua ),( 'Referer', referer )] for url in urls: content = opener.open( url ).read() if re.compile( r'<div class="contents".*?>((.|\n)*?)</div>', re.M ).search( content ) is not None: data = re.compile( r'<div class="contents".*?>((.|\n)*?)</div>', re.M ).search( content ).group() if re.compile( r'「(.*?)」', re.M ).search( data ) is not None: lines = re.compile( r'「(.*?)」', re.M ).findall( data ) for line in lines: f.write( line + "\n" ) f.close()抽出データ
以下は抽出したデータの一部です。全部で1259行あります。
んっん…あっ…! あっ…! ひどい… 仕方ないよ。彼女一人では荷が重すぎた でも、彼女も覚悟の上だろう そんな…あんまりだよ、こんなのってないよ 諦めたらそれまでだ でも、君なら運命を変えられる 避けようのない滅びも、嘆きも、全て君が覆せばいい そのための力が、君には備わっているんだから 本当なの? 私なんかでも、本当に何かできるの?こんな結末を変えられるの? もちろんさ。だから僕と契約して、魔法少女になってよ! 私は巴マミ あなたたちと同じ、見滝原中の3年生 そして キュゥべえと契約した、魔法少女よ
NLTKによる分かち書き
NLTKによる分かち書きを行います。分かち書きとは語の区切りに空白を入れて分かりやすくする表記です。正規表現による単語ベースでの分かち書きとMeCabを用いた形態素ベースでの分かち書きの両方をやります。
単語区切り
台詞の単語をスペースで区切ります。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys reload(sys) sys.setdefaultencoding('utf-8') import nltk from nltk.corpus.reader import * from nltk.corpus.reader.util import * from nltk.text import Text jp_sent_tokenizer = nltk.RegexpTokenizer(u'[^ 「」!?。]*[!?。]') jp_chartype_tokenizer = nltk.RegexpTokenizer(u'([ぁ-んー]+|[ァ-ンー]+|[\u4e00-\u9FFF]+|[^ぁ-んァ-ンー\u4e00-\u9FFF]+)') data = PlaintextCorpusReader( './', r'madmagi.txt', encoding='utf-8', para_block_reader=read_line_block, sent_tokenizer=jp_sent_tokenizer, word_tokenizer=jp_chartype_tokenizer ) #ファイル保存 f = open( './word.txt', 'w' ) for i in data.words(): f.write( i + " " ) f.close抽出された状態は以下のようになります。最初にスクレイプした状態からスペース区切りになっている事が分かると思います。これをword.txtとして保存します。
んっん … あっ …! あっ …! ひどい … 仕方 ないよ 。 彼女一人 では 荷 が 重 すぎた でも 、 彼女 も 覚悟 の 上 だろう そんな … あんまりだよ 、 こんなのってないよ 諦 めたらそれまでだ でも 、 君 なら 運命 を 変 えられる 避 けようのない 滅 びも 、 嘆 きも 、 全 て 君 が 覆 せばいい そのための 力 が 、 君 には 備 わっているんだから 本当 なの ? 私 なんかでも 、 本当 に 何 かできるの ? こんな 結末 を 変 えられるの ? もちろんさ 。 だから 僕 と 契約 して 、 魔法少女 になってよ ! 私 は 巴 マミ あなたたちと 同 じ 、 見滝原中 の 3 年生 そして キュゥ べえと 契約 した 、 魔法少女 よMeCabによる形態素解析
MeCabを利用して形態素解析を行い、形態素毎にスペース区切りでデータを保存するようにします。抽出するサンプルコードは次のようになります。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys reload(sys) sys.setdefaultencoding('utf-8') import MeCab mecab = MeCab.Tagger('-Ochasen') data = open( './madmagi.txt' ).read() f = open( './ma.txt', 'w' ) node = mecab.parseToNode( data ) phrases = node.next while phrases: try: k = node.surface f.write( k + " " ) node = node.next except AttributeError: break f.close()抽出した結果は以下のようになります。これをma.txtとして保存します。
ん っ ん … あっ … ! あっ … ! ひどい … 仕方 ない よ 。 彼女 一 人 で は 荷 が 重 すぎ た でも 、 彼女 も 覚悟 の 上 だろ う そんな … あんまり だ よ 、 こんな の って ない よ 諦め たら それ まで だ で も 、 君 なら 運命 を 変え られる 避け よう の ない 滅び も 、 嘆き も 、 全て 君 が 覆せ ば いい その ため の 力 が 、 君 に は 備わっ て いる ん だ から 本当 な の ? 私 なんか でも 、 本当に 何 か できる の ? こんな 結末 を 変え られる の ? もちろん さ 。 だから 僕 と 契約 し て 、 魔法 少女 に なっ て よ ! 私 は 巴 マミ あなた たち と 同じ 、 見滝 原中 の 3 年生 そして キュゥ べ えと 契約 し た 、 魔法 少女 よ
JavaScriptによるMapReduce
MapRuduceクラスの定義
NLTKによって分かち書きされた単語をJavaScriptでMapReduceします。
簡単なMapReduceクラスを以下のように定義します。mapメソッドは単語を整形、reduceは畳み込みを行っています。var MapReduce = function() {}; MapReduce.prototype = { map : function( data ) { var lines = []; re = new RegExp( "\n" ); if( re.test( data ) ) { lines = data.split( "\n" ); } else { lines[0] = data; } for( var j=0; j<lines.length; j++ ) { var words = lines[j].split( " " ); for( var i=0; i<words.length; i++ ) { print( words[i] + " ," + 1 ); } } }, reduce : function( lines ) { var nodes = {}; for( i=0; i<lines.length; i++ ) { var words = lines[i].split( " ," ); var k = words[0]; if( !nodes[k] ) { nodes[k] = 1; } nodes[k] = nodes[k] + 1; } for( var k in nodes ) { print( k + ' : ' + nodes[k] ); } }, };mapper.js
上のMapReduceクラスを呼び出すmapper.jsを次のように定義します。load関数を使って上で定義した外部ファイル(MapReduce.js)を読み込みます。またSpiderMonkeyのreadlineを使って標準入力を読み取ります。
#!/usr/local/bin/js load( "MapReduce.js" ); var MR = new MapReduce(); while( ( line = readline() ) !== null ) { MR.map( line ); }NLTKで抽出したword.txtに対してmapperをかけると以下のような結果を得る事ができます。
$ cat data/word.txt | js mapper.js んっん ,1 … ,1 あっ ,1 …! ,1 ,1 あっ ,1 …! ,1 ,1 ひどい ,1 … ,1 ,1 仕方 ,1 ないよ ,1 。 ,1 彼女一人 ,1 では ,1 荷 ,1 が ,1 重 ,1 すぎた ,1reducer.js
上のMapReduceクラスを呼び出すreducer.jsを次のように定義します。
#!/usr/local/bin/js load( "MapReduce.js" ); var MR = new MapReduce(); lines = []; i = 0; while( ( line = readline() ) !== null ) { lines[i] = line; i++; } MR.reduce( lines );mapperで得た結果に対してreducerをかけると以下のようになります。
$ cat data/word.txt | js mapper.js | js reducer.js んっん : 2 … : 237 あっ : 19 …! : 10 : 2080 ひどい : 4 仕方 : 11 ないよ : 8 。 : 457 彼女一人 : 3 では : 9 荷 : 3 が : 127 重 : 3 すぎた : 3 でも : 23 、 : 1009 彼女 : 18 も : 61 覚悟 : 3
HadoopでMapReduce
HDFSへの登録
折角mapper/reducerを書いたのでHadoop上で実行します。NLTKで解析した単語、形態素の二つのテキストデータをHDFSに登録します。HDFSのディレクトリをmadmagiとして新たに作成します。またHDFSコマンドは長いのでaliasを張ります。ここではalias hdfs='hadoop dfs'としました。
$ alias hdfs='hadoop dfs' $ hdfs -mkdir madmagi $ hdfs -put data/ma.txt madmagi/ $ hdfs -put data/word.txt madmagi/ $ hdfs -ls madmagi/ Found 2 items -rw-r--r-- 1 yuta supergroup 0 2012-03-21 08:04 /user/yuta/madmagi/ma.txt -rw-r--r-- 1 yuta supergroup 0 2012-03-21 08:04 /user/yuta/madmagi/word.txtHadoop実行
HadoopStreamingを使ってMapReduceします。基本的には上で実行したmapper.js/reducer.jsをHadoopで実行しているだけです。まずは単語の結果に対して(word.txt)に対して実行します。
$ hadoop jar /usr/lib/hadoop-0.20/contrib/streaming/hadoop-streaming-0.20.2-cdh3u3.jar -input madmagi_in/word.txt -output madmagi_out_word -mapper /home/yuta/work/dev/hadoop/map_reduce/mapper.js -reducer /home/yuta/work/dev/hadoop/map_reduce/reducer.js -file /home/yuta/work/dev/hadoop/map_reduce/mapper.js -file /home/yuta/work/dev/hadoop/map_reduce/reducer.js /yuta/work/dev/hadoop/map_reduce/map.py, /home/yuta/work/dev/hadoop/map_reduce/reduce.py, /var/lib/hadoop-0.20/cache/yuta/hadoop-unjar2165458985493486154/] [] /tmp/streamjob8394427362728426211.jar tmpDir=null 12/03/26 02:09:13 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable 12/03/26 02:09:13 WARN snappy.LoadSnappy: Snappy native library not loaded 12/03/26 02:09:13 INFO mapred.FileInputFormat: Total input paths to process : 1 12/03/26 02:09:14 INFO streaming.StreamJob: getLocalDirs(): [/var/lib/hadoop-0.20/cache/yuta/mapred/local] 12/03/26 02:09:14 INFO streaming.StreamJob: Running job: job_201203260111_0017 12/03/26 02:09:14 INFO streaming.StreamJob: To kill this job, run: 12/03/26 02:09:14 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job -Dmapred.job.tracker=localhost:8021 -kill job_201203260111_0017 12/03/26 02:09:14 INFO streaming.StreamJob: Tracking URL: http://localhost.localdomain:50030/jobdetails.jsp?jobid=job_201203260111_0017 12/03/26 02:09:15 INFO streaming.StreamJob: map 0% reduce 0% 12/03/26 02:09:41 INFO streaming.StreamJob: map 50% reduce 0% 12/03/26 02:09:42 INFO streaming.StreamJob: map 100% reduce 0% 12/03/26 02:10:08 INFO streaming.StreamJob: map 100% reduce 100% 12/03/26 02:10:16 INFO streaming.StreamJob: Job complete: job_201203260111_0017 12/03/26 02:10:16 INFO streaming.StreamJob: Output: madmagi_out_word同様に形態素解析の結果(ma.txt)もHadoopでMapReduceします。
$ hadoop jar /usr/lib/hadoop-0.20/contrib/streaming/hadoop-streaming-0.20.2-cdh3u3.jar -input madmagi_in/ma.txt -output madmagi_out_ma -mapper /home/yuta/work/dev/hadoop/map_reduce/mapper.js -reducer /home/yuta/work/dev/hadoop/map_reduce/reducer.js -file /home/yuta/work/dev/hadoop/map_reduce/mapper.js -file /home/yuta/work/dev/hadoop/map_reduce/reducer.js 59202/] [] /tmp/streamjob2162462095464428994.jar tmpDir=null 12/03/26 02:10:52 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable 12/03/26 02:10:52 WARN snappy.LoadSnappy: Snappy native library not loaded 12/03/26 02:10:52 INFO mapred.FileInputFormat: Total input paths to process : 1 12/03/26 02:10:53 INFO streaming.StreamJob: getLocalDirs(): [/var/lib/hadoop-0.20/cache/yuta/mapred/local] 12/03/26 02:10:53 INFO streaming.StreamJob: Running job: job_201203260111_0018 12/03/26 02:10:53 INFO streaming.StreamJob: To kill this job, run: 12/03/26 02:10:53 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job -Dmapred.job.tracker=localhost:8021 -kill job_201203260111_0018 12/03/26 02:10:53 INFO streaming.StreamJob: Tracking URL: http://localhost.localdomain:50030/jobdetails.jsp?jobid=job_201203260111_0018 12/03/26 02:10:54 INFO streaming.StreamJob: map 0% reduce 0% 12/03/26 02:11:17 INFO streaming.StreamJob: map 50% reduce 0% 12/03/26 02:11:21 INFO streaming.StreamJob: map 100% reduce 0% 12/03/26 02:11:39 INFO streaming.StreamJob: map 100% reduce 100% 12/03/26 02:11:46 INFO streaming.StreamJob: Job complete: job_201203260111_0018 12/03/26 02:11:46 INFO streaming.StreamJob: Output: madmagi_out_maHDFSからローカルへコピー
HDFS上の結果をローカルにコピーします。成功結果のpart-00000ファイルが出来ている事を確認します。
$ hdfs -get madmagi_out_word madmagi_out_word $ hdfs -get madmagi_out_ma madmagi_out_ma $ ls madmagi_out_word drwxr-xr-x 3 yuta yuta 4096 3月 26 02:48 . drwxr-xr-x 6 yuta yuta 4096 3月 26 02:46 .. -rw-r--r-- 1 yuta yuta 0 3月 26 02:46 _SUCCESS drwxr-xr-x 3 yuta yuta 4096 3月 26 02:46 _logs -rw-r--r-- 1 yuta yuta 59377 3月 26 02:46 part-00000 $ ls madmagi_out_ma drwxr-xr-x 3 yuta yuta 4096 3月 26 02:46 . drwxr-xr-x 6 yuta yuta 4096 3月 26 02:46 .. -rw-r--r-- 1 yuta yuta 0 3月 26 02:46 _SUCCESS drwxr-xr-x 3 yuta yuta 4096 3月 26 02:46 _logs -rw-r--r-- 1 yuta yuta 24294 3月 26 02:46 part-00000part-0000の中身は以下のようになっています。ここではword.txtに対して実行した結果のpart-00000を載せます。
あ ,15 あぁ ,1 あぁっ ,2 ああ ,5 ああそうだったの ,1 ああでもしなきゃ ,1 あいつは ,3 あけみさんは ,1 あげるよ ,1 あたし ,3 あたしが ,1 あたしがこんな ,1 (略)抽出で目立った単語としては以下のようになりました。
Google Chart APIでMapReduce結果をグラフ化
データをJSON形式に変換
part-00000といった結果をJSON形式に変換します。JSONに変換するPythonコードは以下の通りです。結果をtext.jsとして保存します。
#!/usr/bin/env python import json,codecs map = {} for line in codecs.open( 'word.txt', 'r', 'utf-8' ): data = line.split( ':' ) key = data[0] value = data[1] map[ key ] = value str = json.dumps( map ) f = open( 'text.js', 'w' ) f.write( str ) f.close()JSON形式をGoogleChartAPIで出力
GoogleChartAPIに対してimgタグで読み込むスクリプトを次のように定義します。上で保存したtext.jsにて下で定義するdrawImage関数にjsonデータを渡すように修正が必要です。データ量が多いとURL長の制限を超えるので15個データを表示するようにしました。
<html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> </head> <body> <img src='' id='chart' /> </body> <script> var drawImage = function( json ) { var base = 'http://chart.apis.google.com/chart?cht=bhg&chs=500x500'; var count = []; var title = []; var j = 0; for( var i in json ) { title[j] = 't' + encodeURI ( i ) + ',333333,0,' + j + ',13,1'; count[j] = json[i]; j++; if( j == 15 ) { break; } } var titles = title.join( '|' ); var counts = count.join( ',' ); url = base + '&chd=t:' + counts + '&chdlp=tv|r&chxt=y,x&chm=' + titles; var img = document.getElementById( 'chart' ); img.setAttribute( 'src', url ); } </script> <script src='./text.js'></script> </html>