schedule2020-11-27

【Python】青空文庫の小説を分かち書きして頻出単語を取得する

青空文庫のデータを使った分かち書きと頻出単語の算出をしてみました。

以下の流れで、テキストを取得するところから頻出単語を求めるところまでのコードになっています。

  1. 青空文庫からテキストのzipをダウンロードしてzipを展開する。
  2. ファイルを読み込む。
  3. テキストを前処理する。
  4. janomeで分かち書きをして頻出単語を求める。

将来的に小説から言語モデルを作りたいのでわりとしっかりと作りました(大言)。

環境

  • Python 3.9

リポジトリではDockerでPython環境を作ってます。 その構成も参考になればどうぞ。

Janome

分かち書きには形態素解析で有名なMecabではなくJanomeを利用しました。

Mecabと比べると、Janomeはインストールが簡単で精度が同等ですが、速度はMecabのほうが10倍程度早いです。 MeCabの辞書と言語モデルと同等のものをPythonで実装しているため、そのような特徴を持ちます。

pip install janomeでインストールできます。

janome - github

コード

関数ごとに区切って紹介します。

全体のコードはこちら

1. 青空文庫からテキストのzipをダウンロードしてzipを展開する。

import glob
import os.path
import requests
import zipfile
from typing import Union


def downloadFile(url: str, target_dir='./') -> Union[str, bool]:
    """URL を指定してカレントディレクトリにファイルをダウンロードする
    """
    filename = target_dir + url.split('/')[-1]
    if os.path.exists(filename):
        # サイトに負荷をかけないため
        print(filename, 'is downloaded already.')
        return filename
    r = requests.get(url, stream=True)
    with open(filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)
                f.flush()
        print('{} is downloaded.'.format(filename))
        return filename

    # ファイルが開けなかった場合は False を返す
    return False


def unzip(filename: str, target_dir='./') -> list:
    """ファイル名を指定して zip ファイルを展開する
    """
    if not zipfile.is_zipfile(filename):
        print(filename, 'is not zipfile.')
        return -1
    zfile = zipfile.ZipFile(filename)
    zfile.extractall(target_dir)
    zfile.close()

    # 展開したテキストのリストを取得する
    dst_filenames = glob.glob(target_dir + '*.txt')
    print(dst_filenames)
    return dst_filenames

def if __name__ == "__main__":
    # ドグラ・マグラのリンク先
    URL = 'https://www.aozora.gr.jp/cards/000096/files/2093_ruby_28087.zip'
    tmp_dir = 'tmp/'

    zip_file = downloadFile(URL, tmp_dir)
    filenames = unzip(zip_file, tmp_dir)
    ...

テンプディレクトリにzipと解凍したファイルを展開しています。 zipが既にローカルにある場合は、サイトに負荷をかけないよう手元のファイルを使うようにしました。

unzip()の返り値はリストになってます。

青空文庫のテキスト版アクセスランキングで8位にあって思わずドグラ・マグラを選でます。 読み進めるのがツラい小説ですね。 1ヵ月かけて読んで結局よくわからなかったので、この機にパソコンに何度も読ませたろうかと。。。

2. ファイルを読み込む。

def readSjis(path: str) -> str:
    """ShiftJISのテキストを読み込む
    """
    with open(path, mode="r", encoding='shift_jis') as f:
        text = f.read()
    return text


def if __name__ == "__main__":
    ...

    filenames = unzip(zip_file, tmp_dir)
    # 展開した1つのファイル名を渡す
    text = readSjis(filenames[0])
    
    ...

いくつかの小説をあたってみましたが、青空文庫のテキストファイルはShiftJISのようです。 encoding='shift_jis'で読み込めます。

3. テキストを前処理する。

テキストの冒頭にはタイトル、著者に加えてテキストの記号についての記述があります。

ドグラ・マグラ
夢野久作

-------------------------------------------------------
【テキスト中に現れる記号について】

《》:ルビ
(例)蜜蜂《みつばち》

|:ルビの付く文字列の始まりを特定する記号
(例)大の字|型《なり》に

[#]:入力者注 主に外字の説明や、傍点の位置の指定
   (数字は、JIS X 0213の面区点番号またはUnicode、底本のページと行数)
(例)※[#ローマ数字1、1-13-21]

 [#…]:返り点
 (例)五|月《がつ》於《おいて》[#二]

/\:二倍の踊り字(「く」を縦に長くしたような形の繰り返し記号)
(例)やう/\にして語り出づるやう
*濁点付きの二倍の踊り字は「/″\」
-------------------------------------------------------

[#ページの左右中央]


[#ここから5字下げ]
 巻頭歌


胎児よ

何故躍る

母親の心がわかって

おそろしいのか

テキストビューアーのためのものですが、今回は不要なためルビや注釈を除去していきます。

import re

def preproccessing(text):
    """テキストの前処理をする
    """
    lines = text.split('\n')
    print('処理前 文字数:', len(text), ', 行数:', len(lines))
    # 作品情報
    title = lines[0].strip()
    author = lines[1].strip()
    print(title, author)

    # ルビ、注釈などの除去
    text = re.split(r'\-{5,}', text)[2]
    text = re.split(r'底本:', text)[0]
    text = re.sub(r'《.+?》', '', text)
    text = re.sub(r'[#.+?]', '', text)
    # 全角スペース
    text = re.sub(r'\u3000', '', text)
    # 複数の改行
    text = re.sub(r'\n+', '\n', text)
    text = text.strip()

    lines = text.split('\n')
    print('処理後 文字数:', len(text), ', 行数:', len(lines))
    return text


def if __name__ == "__main__":
    ...

    text = readSjis(filenames[0])
    text = preproccessing(text)

    ...


"""    
処理前 文字数: 468996 , 行数: 3417
ドグラ・マグラ 夢野久作
処理後 文字数: 425638 , 行数: 2959
"""

4. janomeで分かち書きをして頻出単語を求める。

from janome.tokenizer import Tokenizer
import collections

def wakati(text: str):
    """テキストを分かち書きにして頻出単語を求める
    """
    t = Tokenizer()
    # 単語頻度
    c = collections.Counter(t.tokenize(text, wakati=True))
    print(c.most_common()[:15])

    # 特定の品詞のみ
    c = collections.Counter(token.base_form for token in t.tokenize(text)
                            if token.part_of_speech.startswith('名詞,固有名詞'))
    print(c.most_common()[:15])


def if __name__ == "__main__":
    ...

    wakati(text)

"""
# 単語頻度 降順
('の', 16011)
('、', 15485)
('…', 10802)
('に', 10112)
('て', 8831)
('。', 7791)
('を', 7770)
('た', 6648)
('が', 5530)
('と', 5522)
('は', 5029)
('で', 4168)
('し', 3307)
('\n', 2958)
('も', 2542)

# 特定の品詞(名詞,固有名詞)のみ
('呉', 481)
('若林', 346)
('一郎', 340)
('正木', 250)
('青', 86)
('秀', 86)
('斎藤', 68)
('福岡', 58)
('モヨ', 53)
('卓子', 42)
('九大', 35)
('大正', 34)
('姪の浜', 34)
('行衛', 33)
('九州', 32)
"""

t.tokenize(text)で分かち書きしたものの単語とその情報が取り扱えるようになります。

品詞で絞りたい場合はtoken.part_of_speech.startswith('名詞,固有名詞')とするとよい。 品詞はMecabの品詞IDの定義に一覧がある。

頻出単語はcollections.Counterでカウントして、c.most_common()降順に並び替えている。

参考