Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

「魔法少女まどか☆マギカ」の台詞をJavaScriptでMapReduceしてGoogle Chart APIでグラフ出力したよ!

Hadoop 第2版

Hadoop 第2版

概要

以前に「魔法少女まどか☆マギカ」の台詞をNLTK(Natural Language Toolkit)で解析することに挑戦しましたが、解析結果の集計グラフが奇麗に表示されませんでした。今回はそれを改善すべく手法を変えて挑戦します。グラフ化はNLTKではなくGoogle Chart APIを利用します。Google Chart Tools - Google Code はてなブックマーク - Google Chart Tools - Google Code またHadoopMapReduceJavaScriptを用いGoogle Chart APIとのデータ連携をしやすいようにします。以下に大まかな処理の流れを記述します。

  1. SpiderMonkeyCentOSに設定
  2. 魔法少女まどか☆マギカの台詞をPythonスクレイピング
  3. NLTKによる分かち書き
  4. JavaScriptによるMapReduce
  5. HadoopMapReduce
  6. Google Chart APIMapReduce結果をグラフ化

SpiderMonkeyCentOSに設定

参考

HadoopMapReduceJavaScriptで行うためにCentOSSpiderMonkeyを設定します。本家サイトの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.gz
build/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によって分かち書きされた単語をJavaScriptMapReduceします。
簡単な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)を読み込みます。またSpiderMonkeyreadlineを使って標準入力を読み取ります。

#!/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
すぎた ,1
reducer.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

HadoopMapReduce

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.txt
Hadoop実行

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)もHadoopMapReduceします。

$ 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_ma
HDFSからローカルへコピー

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-00000

part-0000の中身は以下のようになっています。ここではword.txtに対して実行した結果のpart-00000を載せます。

あ  ,15   
あぁ ,1
あぁっ  ,2
ああ  ,5
ああそうだったの  ,1
ああでもしなきゃ  ,1
あいつは  ,3
あけみさんは  ,1
あげるよ  ,1
あたし  ,3
あたしが  ,1
あたしがこんな  ,1
(略)

抽出で目立った単語としては以下のようになりました。

Google Chart APIMapReduce結果をグラフ化

データを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>
GoogleChartAPIの出力結果



文字が化けてしまって巧く表示されないケースもありますが、以前のNLTKで表示したグラフよりは奇麗に書けていると思います。単純にエクセルなどに落とし込んだ方がさらに奇麗に書けそうですが、今回はWebで試す事が目的だったのでこれにて終了です。