読者です 読者をやめる 読者になる 読者になる

文學ラボ@東京

(文学をなにかと履き違えている)社会人サークルです。第22回文学フリマ東京では、ケ-21で参加します。一緒に本を作りたい方はsoycurd1あっとgmail.comかtwitter:@boonlab999まで(絶賛人員募集中)。

content

PythonとElasticSearch、Kibanaを用いた太宰治小説の可視化

soy-curd 文学フリマ 評論

(以下の文章は秋の文フリの原稿です)

はじめに

読書という行為を、時間を抜きにして語ることはできない。例えば20000字程度の短編小説ならば、仮に分速500文字で読書可能だとすると、40分もの時間をわたしたちはそのテキストと向かい合うこととなる。わたしたちは読書を行っているあいだ、細胞の中でDNAを解読するリボソームのように、テキストをシリアルに視野の中に取り込み、脳の中で解釈していく。この際、解釈の方向は定められている。縦書きであれば上から下に。横書きであれば、(大抵の言語は)左から右に読まれるしかない。テキストは、ごく一部の例外を除けば、一方向にしか読まれ得ない。読書は非常に"時間"に似通った、あるいは随伴した現象だと考えられる。

以前発行した同人誌『実用 どんぐりと山猫』では、pythonとd3.jsを用いて小説中のある値の時間変化を可視化した。昨今、可視化ツールとして、プログラミング不要なKibanaが開発されている。そこで本稿では、小説における時の流れをKibanaを用いて可視化し、より簡便な小説のテキストの解析を目指す。

小説の中の時間構造

小説の中から、客観的な時間の指標を見出すことはできるだろうか。カフカ『城』に流れる停滞した時間。ボルヘス『バベルの図書館』における永遠の提示。桜庭一樹『私の男』で描写される逆行する日々。それら物語によって示される時間について、データとして評価できる形で客観的に抽出することは難しいだろう。そこで本稿では、小説の文字数を時間の指標とし、そこから見えてくる特徴を探していくこととする。

時間を軸としてある値を評価するためには、なんらかの基準が必要だ。一般にはタイムスタンプと呼ばれるものが用いられるが、小説には通常、そのような明確な時間の単位はない。今回はひとまず、これを500文字区切りで1づつ増える値と仮に設定する(文庫で1ページ程度に該当すると思われる)。そして可視化への中間地点として、以下のようなJSONデータを作成していく。

[
  { 'page':1,
    'text':'春はあけぼの、やうやう白くなりゆく、山ぎは...'

  },
  { 'page':2,
    'text':'夏は夜。月のころはさらなり...'

  },
]

解析環境の用意

まずは自分のPCにpython3、ElasticSearch、Kibanaをインストールする。インストール法についての詳細は記述しない。また、解析するテキストについては、インターネット上に青空文庫や小説家になろうといった膨大なテキストデータがあるため、そちらを利用するのが早い。本稿では例として、太宰治人間失格』のテキストを解析していくこととする。

ElastickSearch

ElastickSearchはJava製の全文検索エンジンであり、Apach Solrと並んで人気が非常に高い。導入にあたっては、(http://engineer.wantedly.com/2014/02/25/elasticsearch-at-wantedly-1.html) の内容がわかりやすいと思われる。今回重要な点としては、形態素解析器であるkuromojiプラグインを導入しておくことだ。

ElastickSearchにデータをインポートするにあたって、あらかじめデータのマッピング(どのデータがどんな種類のデータか指定すること)を行う必要がある。マッピングについては、以下の内容のanalyze.jsonを作成すると良い。

{
  "settings": {
    "analysis": {
      "filter": {
        "pos_filter": {
          "type": "kuromoji_part_of_speech",
          "stoptags": [
            "助詞-格助詞-一般",
            "助詞-終助詞"
          ]
        },
        "greek_lowercase_filter": {
          "type": "lowercase",
          "language": "greek"
        }
      },
      "tokenizer": {
        "kuromoji": {
          "type": "kuromoji_tokenizer"
        },
        "ngram_tokenizer": {
          "type": "nGram",
          "min_gram": "2",
          "max_gram": "3",
          "token_chars": [
            "letter",
            "digit"
          ]
        }
      },
      "analyzer": {
        "kuromoji_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "pos_filter",
            "greek_lowercase_filter",
            "cjk_width"
          ]
        },
        "ngram_analyzer": {
          "tokenizer": "ngram_tokenizer"
        }
      }
    }
  },
  "mappings": {
    "novel": {
      "_source": {
        "enabled": true
      },
      "_all": {
        "enabled": true,
        "analyzer": "kuromoji_analyzer"
      },
      "properties": {
        "id": {
          "type": "integer",
          "index": "not_analyzed"
        },
        "page": {
          "type": "integer",
          "index": "not_analyzed"
        },
        "text": {
          "type": "string",
          "index": "analyzed",
          "analyzer": "kuromoji_analyzer"
        }
      }
    }
  }
}

ここで、textフィールドにkuromoji_analyzerを指定している。こうすることで、解析テキストをあらかじめ形態素解析しておくことができる。

解析ファイルのJSON

まず、『人間失格』のテキストデータを含んだningen.txtファイルを用意する。 そして、そのファイルを含んだディレクトリ上で、以下のpythonスクリプトを実行してみよう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json

def main():
    
    with open("./ningen.txt", "r") as f1:
        text = f1.read()
    
    # 1ページの文字数を500文字と仮定
    pseud_char_num_per_page = 500
    
    # ページ毎にテキストを区切っていく
    page_num = len(text) // pseud_char_num_per_page
    
    texts = []
    for x in range(page_num):
        init = x * pseud_char_num_per_page
        text_of_page = text[init: init + pseud_char_num_per_page]
        texts.append({ "index": { "_index": "dazai-demo", "_type": "novel", "_id": x + 1 } })
        texts.append({'id':x + 1, 'text':text_of_page, 'page':x})
    
    bulk = ""
    for x in texts:
        bulk = bulk + json.dumps(x, ensure_ascii=False) + "\n"
    
    with open("./ningen.json", 'w') as f2:
        f2.write(bulk)
        
    print(bulk)

if __name__ == '__main__':
    main()

これを実行すると、カレントディレクトリ配下に以下のようなningen.json(正確には、JSONテキストを結合したもの)が生成される。

{"index": {"_type": "novel", "_id": 150, "_index": "dazai-demo"}}
{"page": 149, "text": "ュックサックを背負って友人の許もとを辞し、れいの喫茶店に立ち寄り、\n「きのうは、どうも。ところで、……」\n (中略)もし、これが全部事実だったら、そうして僕がこのひとの友人だったら、やっぱり脳病院に連れて行きたくなったかも知れない」\n「", "id": 150}

これは、テキストを500字毎に区切り、(架空の)ページ番号を振ったものである。 それでは、これらをElasticSearchにインポートしてみよう。

curl -XPUT localhost:9200/dazai-demo --data-binary @analyze.json
curl -XPOST localhost:9200/_bulk --data-binary @ningen.json 

その後、成功していたら以下のクエリによって検索が可能になっているはずである。

curl -XGET localhost:9200/dazai-demo/novel/_search -d '{"query":{"match":{"text":"東京"}}}'

問題なくデータのインポートは行えたであろうか。次項からは、Kibanaによる可視化を行っていく。

(続きます)