Pythonの形態素解析で青空文庫を読みやすくしてみた

MeCabという形態素解析ライブラリはもうお馴染みだと思われます。そして、この形態素解析によって様々な予測ができ、昨今の機械学習などにも役立てられています。

そこで今回は形態素解析を使って、青空文庫の小説を読みやすくしてみました。

サンプルにしたのは佐藤春夫の『田園の憂鬱』で、有名な小説ですが青空文庫には旧仮名遣いしか公開されておらず、なんとなく読みにくさを感じたので、形態素解析を用いて旧仮名遣いを新仮名遣いに置換してみました。

※使用環境
- Webサーバー:Rocky9(Rhel互換)
- 言語：python3.9
- フレームワーク：Django4

# xhtmlファイルから文章のスクレイピングを行う
まずは公式サイトからxhtmlファイルをダウンロードしてスクレイピングします。使用ツールはbeautifulSoup4(**bs4**)です。今回はタグは不要なので、以下のようにget_textメソッドを使用して必要な文字データだけをスクレイピングします。

```py:views.py
    path = f'./static/files/{files}' #公式サイトのxhtmlファイル
    soup = bs4.BeautifulSoup(open(path,encoding='sjis'),'html.parser')
    stream = soup.find(class_="main_text").get_text()[:3000] #とりあえず冒頭から3000文字
```

ここで注意点があります。そのまま形態素解析を実行するだけならこの処理は不要ですが、小説を読みやすくするために改行コード、全角スペース、半角スペースはそのまま残しておく必要があります。しかし、それらが処理の際に干渉してしまうので、これらを予め任意の記号に変更しておきます。

※青空文庫でまず使われないであろう文字に置換しておくといいでしょう。

```py:function.py
  def replace_char(self,stream):
    stream = re.sub(r'\n','@',stream) #改行コード
    stream = re.sub("\u3000","■", stream) #全角スペース
    stream = re.sub("\s","★", stream) #半角スペース
    stream = re.sub(",","●",stream) #カンマ
    return stream
```

# 形態素解析結果をパースする
ここからスクレイピングされたテキストに対し形態素解析を行いライブラリを使ってパースにかけます。利用するツールはMeCabの派生ツール**fugashi**（ライブラリはunidicを使用）です。そして、各文字に対し、以下の情報（品詞、よみがな、語幹など）を敢えて残しておきます。また、ここで先程の作業を実施しないと**テキスト内の改行コードと解析結果の改行コードが混同してしまう**ことになります。

※当初はMeCab(python-mecab)を利用していましたが、pythonの場合はfugashiの方がいいようなので、そっちを使っています。ちなみにfugashiで外部ライブラリを使用する場合はGenericTaggerが必要です。

```py:functions.py
from fugashi import Tagger,GenericTagger

class Analysis:
  def mecab(self,streams):
    tagger = GenericTagger('-d /opt/mecab/lib/mecab/dic/65_novel') #fugashiで外部ライブラリを使用
    tags = []
    moji_info = [] #パースされた解析情報を格納
    taggers = tagger.parse(streams)
    tag_rows = taggers.split('\n') #解析結果の改行コードを基準に分割
    for tag_row,i in enumarate(tag_rows):
      tag_cols = tag_row.split('\t') #タブを基準に分割
      if(tag_cols[0] == 'EOS'):
        break
      else:
        moji_info =[
        tag_cols[0].split("\t")[0],#文字列
        tag_cols[0].split("\t")[0],#文字列
        tag_cols[7] if len(tag_cols) > 7 else "",#語幹
        tag_cols[6] if len(tag_cols) > 6 else "",#読み
        tag_cols[0].split("\t")[1],#品詞
        tag_cols[1], #品詞の種別
        tag_cols[4], #活用
        tag_cols[5], #活用形
        i+1, #連番
        ]
      tags.append(moji_info)
    return tags
```

## 近代文語に対応した形態素解析用ライブラリ
通常のunidicだと古語には対応しておらず、かなり作業が煩雑になってしまったので、近代文語に対応したライブラリがないかを確認したら適切なものがあるみたいなので、ライブラリはこれを実装しておきましょう。

[古文用UniDicS](https://clrd.ninjal.ac.jp/unidic/download_all.html)

この近代文語用をダウンロードします。設置方法は簡単で、公式サイトからダウンロードしたフォルダを対象のパスにセットするだけです。ただ、インストールしてサーバ起動しただけだとdicrcがないと怒られるので、下記ページを参考に**dicrc**を作っておきます。dicrcはMeCabやfugashiなどで出力用フォーマットを決定させるファイルなので、エディタで開いても問題はありません。

[近代文語UnidicをPython3で使おうとして少々大変だった話](https://digitalnagasaki.hatenablog.com/entry/2019/06/20/231405)

ipadic用にインストールしたdicrcをそのままコピー、読み込み対象のライブラリフォルダにペースト、そこからパスをリンクしておくだけで大丈夫でした。

※以前はpipからpython-unidic2udというのをインストールできたようですが、今は権利者関係でリンク切れとなっています。

# 具体的な置換作業
ここから具体的な置換作業に入ります。メインに使用するのは正規表現でのマッチングですが、単純に文字列だけを見ると不要な置換を実行してしまいます。そこで先程の形態素解析によって得られた語幹、読み、品詞といった情報が役立つわけです。

文語において置換対象となるものは以下のようなものがあるので、これらをメソッド化しておきました。
- 旧字体（ゐ→い、ゑ→え）　
- だ行の置換（ぢ・づ→じ・ず）
- は行の置換（合はず→合わず　など）
- 促音便の置換（つ→っ、う→っ）
- ウ音便の置換（かう→こう、さう→そう　けふ→きょう　など）
- 拗音の置換（しや→しゃ　など）
- カタカナ語の置換（シヤアベツト→シャーベット　など）

中でも厄介なのがは行→あ行への置換と促音便の置換であり、変換が必要な場合と不要な場合を見分けないといけません。それで、これらの処理、特に品詞や語幹などのデータが置換の目安となります。また、解析対象の文字列も前後のインデックスから文脈として取得すると効率よく置換できます。

そしてもう一つ便利な目安が**分かち書き**であり、部分的に置換前と置換後を-Owakatiにかけています。すると分かち書きによって語幹が分離してしまう場合は不自然な日本語となっているので、正しい変換か誤った変換かを見分ける目安になります。

```py:functions.py
  #字体変更
class Analysis:
  def sinjitai(self,texts):
    sins = [] #新字体に置換した文章を出力
    for i,txt in enumerate(texts):
      moji = txt[0]
      k_moji = txt[1]
      if( moji in ["■","★","▼","●"]):
        sins.append(moji)
      elif(not(re.match(r'(.*)([うかけさせたぢつづはひふへほやゆよらゐゑを|アイウエオツヤユヨ])(.*)',k_moji))):
        sins.append(txt[1])
      else:
        k = Kana(texts,txt,i) #仮名置換のクラス
        #ゐゑを→わえお
        if(re.match(r'.*[ゐゑを].*',k_moji)):
          k.kyukana()
        #ぢ・づ→じ・ず
        if(re.match(r'.*[ぢづ].*',k_moji)):
          k.dtoz()
        #は行→あ
        if(re.match(r'.*[は-ほ].*',k_moji)):
          k.htoa()
        #拗音
        if(re.match(r'.*[やゆよ].*',k_moji)):
          k.youon()
        #ウ音便
        if(re.match(r'.*[うふ|あかけがさせざただなはばまやらわ].*',k_moji)):
          k.uon()
        #促音便
        if(re.match(r'.*つ.*',k_moji)):
          k.sokuon()
        #カタカナ対応
        if(re.match(r'.*[ア-オツムヤ-ヨ].*',k_moji)):
          k.katakana()
        moji = k.getter()
        texts[i][0] = moji #新字体に変更した形態素
        sins.append(moji)
    texts = [t for t in texts if(not re.match(r'[★|■|▼|●]',t[0]))]　#分析用（余分な記号を排除）
    return texts,sins
```

kanaクラスの各種メソッドは試行錯誤中で、とても公開できたものではないので割愛しますが、コンストラクタにひととおり必要な情報を渡しておいて、各種メソッドで正規表現のマッチングを用いて、該当するものを置換するという作業を実施しています。

# 置換後の文章を読みやすくする
置換が終わったら、特殊記号を元通りにしておきましょう。

```py:
  def revert_char(self,streams):
    sin = ''.join(streams)
    sin = re.sub("▼","\n",sin)
    sin = re.sub("★"," ",sin)
    sin = re.sub("■","　",sin)
    sin = re.sub("●",",",sin)
    return sin

```

あとはテンプレートに返すだけですが、Djangoには便利なテンプレートタグがあるので、そのlinebreaksbrを活用し、改行コードを`<br>`にします。contentsが返された変数です。

```html:analysis.html
<article>
{{ contents | linebreaksbr }}
</article>
```

# このようになりました

# 置換前
![before.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272191/5ea54ba5-546a-225c-609f-913bf8bd08bc.jpeg)

# 置換後
![after.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272191/2dc27e8e-19af-40ad-c728-ffcad511c47b.jpeg)

完璧ではないですが、9割以上は置換に成功しているので、だいぶ読みやすくなりました。もっと工夫すれば、元のルビや小文字もそのまま維持できると思います。他に島崎藤村の『破戒』、太宰治の『津軽』なども旧仮名遣いしかないので、読みやすくできました。

※詳細分析用のテーブルも出力しておくと、後で微妙な調整作業がうまくいくと思います。

### 注

- 上記unidicは一定の利用ライセンスがあるので、商用利用は申請が必要です
- 青空文庫利用に際して原本の改竄は規約違反ですが、あくまで著作者人格権に基づくので、旧字体を新字体に変えて読める機能が大丈夫なら、これも大丈夫だとは思うのですが。やろうと思えば、古語を使っている徳冨蘆花の『不如帰』や森鴎外の『舞姫』なども現代語っぽく変換できると思います。