SudachiPyで偶然短歌を作った話

#偶然短歌とは
wikipediaの文章の中から、偶然57577になっている部分を抜き出して投稿する[twitterのbot](https://twitter.com/g57577)です。
[こちら](https://inaniwa3.hatenablog.com/entry/2015/01/01/152927)の方が2015年に作成されています。
もとのやつはruby+MeCabで出来ているようなので、今回はこちらをPythonとSudachiで焼き直します。
先輩に感謝と尊敬を。

#環境
Windows10 Home (64bit) 

python 3.7.4
SudachiDict-full==20191224
SudachiPy==0.4.2

環境はpipenvで構築しました

#なぜSudachiなのか
Sudachiでは分割モードの指定というのができて、できるだけ大きな単位で品詞分解することができる。
MeCabを使うとかなり細かく分解されるので、たとえば、上の句に"東京スカイ"、下の句に"ツリー"とかいうおかしな分割がピックアップされる恐れがある。
Sudachiだとこのリスクが軽減される。

以下、[SudachiのGitHub](https://github.com/WorksApplications/Sudachi)より。
>分割モード
>Sudachi では短い方から A, B, C の3つの分割モードを提供します。 A は UniDic 短単位相当、C は固有表現相当、B は A, C の中間的な単位です。
>
>以下に例を示します。
>
>(コア辞書利用時)
>
>A：選挙/管理/委員/会
>B：選挙/管理/委員会
>C：選挙管理委員会
>
>A：客室/乗務/員
>B：客室/乗務員
>C：客室乗務員

体感ですが、SudachiのAがMeCabとおなじくらい。

#SudachiPyのインストール
[SudachiPyのGitHub](https://github.com/WorksApplications/SudachiPy)を参考にSudachiPyと辞書をインストール
辞書にはsmall, core, fullの3種類がある。今回は語彙の多いfullの辞書をインストールした。

```
pip install SudachiPy
pip install https://object-storage.tyo2.conoha.io/v1/nc_2520839e1f9641b08211a5c85243124a/sudachi/SudachiDict_full-20191224.tar.gz
```

#辞書をリンクする
辞書に関しては、ソース内でjsonから指定する方法と、コマンドからデフォルトの辞書をリンクさせる方法があるっぽい。今回は後者にした。
ドキュメントには特に書いていなかったが、普通にやるとPermissionのエラーが出たので、下記のコマンドを**管理者権限のプロンプトから**実行する。ここハマリポイント。
(LinuxやMacだとどうなるのかは未確認です。)

```
sudachipy link -t full
```

#ソースコード
ガガガッと作ったので作り方がナンセンスな部分があるかもしれない。というかあると思う。
モード分けして、俳句でも短歌でも検出できるようにしている。
最初は31音の文章を抽出して、それが57577になるかどうか、ってアプローチだった。でもやってみたら全然検出しなかったので、長い文章の中の一部からでも検索できるようにして、setting.precisionのフラグで切り替えるようにした。
ノイズ少なめのきれいなやつが欲しいときは前者、とにかく数が欲しいときは後者。

基本方針としては、フラグを立てながら読みを数えていき、音の切れ目が57577に収まるものを抽出していくスタイル。579にななったり、57578になったりしたら棄却。句の頭に助詞や助動詞が来た場合も棄却。文章のアタマの部分を削除してもう一度探しなおす。
また、[ャ、ュ、ョ]などは音数としてカウントしない。
importlibを使っているのは、最終的にpyinstallerでexeにしたためです。

```python:search_tanka.py
import re
import importlib
from sudachipy import tokenizer
from sudachipy import dictionary

setting = importlib.import_module('setting')
tokenizer_obj = dictionary.Dictionary().create()
# 分割単位を最長に設定
split_mode = tokenizer.Tokenizer.SplitMode.C
# 読み込むテキストファイルのパス
searchfile_path = "./search_text/" + setting.search_file_name
# 書き出すテキストファイルのパス
savefile_path = "./result_text/" + setting.save_file_name
# 俳句、短歌のモード切替
if setting.mode == 1:
    break_points = [5, 12, 17]
else:
    break_points = [5, 12, 17, 24, 31]
# カタカナの正規表現
re_katakana = re.compile(r'[\u30A1-\u30F4]+')

# テキストファイルオープン
with open(searchfile_path, encoding="utf-8_sig") as f:
    # 全行読み込んでリスト化
    areas = f.readlines()
    for line in areas:
        # "。" または "." または改行で区切る
        sentences = re.split('[.。\n]', line)
        for sentence in sentences:
            # 文章単位で検索する場合はスルー
            if setting.precision == 1:
                pass
            # 短歌、俳句、それぞれの文字数以上の文章は検出対象としない
            else:
                if len(sentence) > break_points[-1]:
                    continue

            # 形態素解析
            m = tokenizer_obj.tokenize(sentence, split_mode)
            # MorphemeListをListにキャスト
            m = list(m)

            retry = True
            while retry:
                break_point_header_flag = True
                retry = False
                counter = 0
                break_point_index = 0
                reading = ""
                surface = ""
                # それぞれの句の区切りで文章が切れているか判別
                for mm in m:
                    if break_point_header_flag == True:
                        text_type = mm.part_of_speech()[0]
                        #　それぞれの句の頭が適切な品詞でない場合は検出対象としない
                        if text_type in setting.skip_text_type:
                            # 長文捜査onの場合はもう一度検索
                            if setting.precision == 1:
                                retry = True
                                del m[0]
                                break
                            else:
                                counter = 0
                                break
                        else:
                            break_point_header_flag = False
                    # 読みを解析
                    reading_text = mm.reading_form()
                    surface_text = mm.surface()
                    if len(reading_text) > 7:
                        # 長文捜査onの場合はもう一度検索
                        if setting.precision == 1:
                            retry = True
                            del m[0]
                            break
                        else:
                            counter = 0
                            break
                    # 解析結果がスキップすべき文字の場合は飛ばす
                    if reading_text in setting.skip_text:
                        sentence = sentence.replace(mm.surface(), "")
                        continue
                    # カタカナの人名が入ってこないので、surfaceで補完する
                    if reading_text == "":
                        text_surface = mm.surface()
                        if re_katakana.fullmatch(text_surface):
                            reading_text = text_surface
                        # 辞書で読めない漢字が出現したらスキップ
                        else:
                            # 長文捜査onの場合はもう一度検索
                            if setting.precision == 1:
                                retry = True
                                del m[0]
                                break
                            else:
                                counter = 0
                                break
                    # 読みの音素数をカウント
                    counter += len(reading_text)
                    reading = reading + reading_text
                    surface = surface + surface_text
                    # カウントしない相性の音素があればカウントをマイナス
                    for letter in setting.skip_letters:
                        if letter in reading_text:
                            counter -= reading_text.count(letter)
                    # それぞれの句の文字数分カウントが進んだか。
                    if counter == break_points[break_point_index]:
                        break_point_header_flag = True
                        # 最後まで来ていなければ次の句へ
                        if counter != break_points[-1]:
                            break_point_index += 1
                            reading = reading + " "
                    # それぞれの句の指定文字数を超えてしまったら弾く。
                    elif counter > break_points[break_point_index]:
                        # 長文捜査onの場合はもう一度検索
                        if setting.precision == 1:
                            retry = True
                            del m[0]
                            break
                        else:
                            counter = 0
                            break

                # 指定文字数ぴったりで検出できたものをピックアップしてファイルに追記
                if counter == break_points[-1]:
                    with open(savefile_path, "a") as f:
                        try:
                            print(surface + " ")
                            print("(" + reading + ")" + "\n")
                            f.write(surface  + "\n")
                            f.write("(" + reading + ")" + "\n")
                            f.write("\n")
                        except Exception as e:
                            print(e)

                if len(m) < len(break_points):
                    break
```
```python:setting.py
# mode (検出モード) 1:俳句 2:短歌
mode = 2
# precision (精度) 1:低 2:高
# 低　検出数:多,ノイズ:多、実行時間:高
# 高　検出数:少,ノイズ:少、実行時間:少
precision = 1
# 音素としてカウントしない文字
skip_letters = ['ャ','ュ','ョ']
# 検出対象とするファイル
search_file_name = "jawiki-latest-pages-articles.xml-001.txt"
# 検出結果を保存するファイル
save_file_name = "result.txt"
# 句の頭に来るべきでない品詞
skip_text_type = ["助詞", "助動詞", "接尾辞", "補助記号"]
# 解析対象に含まない文字
skip_text = ["、", "キゴウ", "=", "・"]
```

#実行結果
wikipediaの文章に対して走査した。以下一部抜粋。

プラトンはイソクラテスの影響を受け中期より文体を変え
(プラトンハ イソクラテスノ エイキョウヲ ウケチュウキヨリ ブンタイヲカエ)

始まりといわれ蕉風発祥の地の碑が立てられている
(ハジマリト イワレショウフウ ハッショウノ チノイシブミガ タテラレテイル)

相承における個別の伝承である血脈を特に重んじ
(ソウショウニ オケルコベツノ デンショウデ アルケツミャクヲ トクニオモンジ)

将来の首都になるべき都市としてヌアクショットが建設された
(ショウライノ シュトニナルベキ トシトシテ ヌアクショットガ ケンセツサレタ)

日本の協力により蛸壺を使う日本式のタコ漁
(ニッポンノ キョウリョクニヨリ タコツボヲ ツカウニッポン シキノタコリョウ)

#今後の展望
もし次またやるなら、対応したいのは以下。

・数詞の読みに関して
Sudachiだと、数詞の読みが現状はうまくいかないらしい。

例)
概ねは50両から150両の間で推移していた 
↓
(オオムネハ ゴレイリョウカラ イチゴレイ リョウノアイダデ スイイシテイタ)

案はあるけど実装は未定とのこと。
頑張ってプラグインの辞書を書くか、正規表現で数詞のみ切り出して、そこだけ別のエンジンで解析するかで対応はできそう。(だが、対応したところでいい短歌が抽出できるようになるかはまた別の話。。。)
・実行速度に関して
予想はしていたが、遅い、、、、、
これはSudachiが遅いとかPythonが遅いとか以前に、わたくしの実装の問題です。ごめんなさい。
愚直にfor文で回しているので、ちゃんと[itertools](https://docs.python.org/ja/3/library/itertools.html)とか使えばもっと速くなるはず。

#感想
結局元のソースはほとんど見ずに作ってしまった(ぉぃ)。
たまにノイズが入っていたり、変なとこで切れてるものが出てくるが、概ね正しく動いてくれた。
最後のピックアップ作業はやはり人力かなと思う。
botでつぶやくとか、引用元を取ってくるのはどうやるのだろうか。

たくさん検出されますが、今のところパッと見て、一番のお気に入りは以下です。

**内容のレスを何度もコピペして書き込むという迷惑行為**
**(ナイヨウノ レスヲナンドモ コピペシテ カキコムトイウ メイワクコウイ)**

それでは。