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

yukiのブログ

このブログの記事はhttps://yuki67.github.io/からコピーしたものです。

青空文庫のテキストを簡略化する

漱石没後100年ということで漱石の文章を解析してみようと思い立ちました。しかし青空文庫の文章にはルビや注があって解析ができる状態ではありません。また漱石の文章をなんの装飾もせず公開している物好きなサイトも見当たりません。

そこで解析のための下準備として、青空文庫のテキストファイルからルビや注を取り除き、さらに一文ごとに改行をいれて解析しやすくするプログラムを描きました。

github.com

めんどくさい部分が多かった割にプログラム的には大したことやってませんが、正規表現の練習になりました。

[2017/1/13追記]

この記事を書いた後大幅にプログラムを改変しました。以下は改変する前のプログラムについての記事です。記事の最後にあるソースコードも改変する前のものです。

動かす

記事の最後にあるソースコードを(例えばaozora.pyとして)保存し、青空文庫のテキストファイル(例えばkokoro.txt)と同じフォルダに保存し、以下のコマンドを実行すると、解析しやすい形になったテキストファイルが作られます。テキストファイルは複数指定することもできます。pythonのバージョンは3.xです

python aozora.py kokoro.txt

[#2字下げ]上 先生と私[#「上 先生と私」は大見出し]


[#5字下げ]一[#「一」は中見出し]

 私《わたくし》はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚《はば》かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執《と》っても心持は同じ事である。よそよそしい頭文字《かしらもじ》などはとても使う気にならない。

上のテキストは下のように変換されます。

上 先生と私
一
 私はその人を常に先生と呼んでいた。
だからここでもただ先生と書くだけで本名は打ち明けない。
これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。
私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。
筆を執っても心持は同じ事である。
よそよそしい頭文字などはとても使う気にならない。

問題点

  • 見出しと本文を区別していない。
  • 引用などで一字下げの文章になっている部分があっても一行ごとに別の段落と認識してしまう。
  • 文章読本を読みながらプログラムを作ったわけでもないので、日本語の文章のルールをなにか取りこぼしているかもしれない。
  • 文章を変換するだけで解析を何もしてない。

コードについて

青空文庫のテキストファイルは段落ごとに改行が入っています。そのため基本的には文字列を一行ずつ読み取り、"。"で文字列を分割するという方法を取りました。この方法をとれば文章全体が長すぎる可能性を考える必要もありません*1

ただし、以下のような理由で多少めんどくさくなりました。

  • 空行がある。
  • 本文の前後に青空文庫の説明がある。
  • 文章中にルビや注が入り込んでいる。
  • 日本語の文は"。」"や"。)"で終わることがある。
  • 見出しの段落には"。"がない。

このうち正規表現が関係するものについて説明します。

ルビと注を消す

注とルビは本文中で《》や[]の括弧*2に囲まれているか、特殊な文字(|, ※)が一文字あるかのどちらかです。ルビと注は削除して良いので*3正規表現にマッチした部分を指定の文字列で置き換える関数を、置き換える文字列を空文字列として使って削除します。

def del_deco(sentence):
    """
    sentenceの装飾を外して素の文章にして返す
    """
    # ルビと注を識別するための正規表現
    decoration = re.compile(r"([[^[]]*])|(《[^《》]*》)|[|※]")
    # ルビと注を見つけて、空文字列と入れ替える
    return re.sub(decoration, "", sentence)

段落を文に分ける。

日本語の文はいつも"。"で終わるわけではなく、"。」"や"。)"、ときには"。……"などで終わることがあります。

幸運にもそれぞれの場合の処理を別々に書く必要はありませんでした。python正規表現を扱うreモジュールを使って正規表現を検索すると、マッチした部分の始まりと終わりの位置が取得できるので、これを使えばすべての場合をまとめて処理できます。

    def next_sentence(self):
        """
        今読んでる段落(self.reading_paragraph)の次の文を返す。
        文を返す前にself.reading_paragraphを次の文に進める
        """
        sentence_separater = re.compile(r"[。][」)]?[….―]*")
        result = re.search(self.sentence_separater, self.reading_paragraph)

        if result is None:
            # "。"が無い(表題など)。
            ans = self.reading_paragraph
            self.__skip_empty_and_read()
            return ans
        else:
            # "。"(など)が段落中にある
            # "。"がreading_paragraphの何文字目か取得
            _, end = result.span()
            if len(self.reading_paragraph) == end:
                # 段落が終わっている
                ans = self.reading_paragraph
                # reading_paragraphを次の段落に進める
                self.__skip_empty_and_read()
                return ans
            else:
                # 段落が終わっていない
                ans = self.reading_paragraph[:end]
                # reading_paragraphを段落内の次の分進める
                self.reading_paragraph = self.reading_paragraph[end:]
                return ans

感想

漱石ぐらいなら「夏目漱石 全文」で検索すればプレーンなテキストがすぐ出てくると思ったんだけどそうでもないんだなぁ。

コード

大きなプログラムを作ってやろうと意気込んで作り始めたのが一つの小さなプログラムになったのでコードがちぐはぐです。

# -*- coding: utf-8 -*-
import re
import argparse


class Aozora(object):
    """
    青空文庫のテキストファイルを読み込んで各種変換を施す
    インスタンス化した次点でファイルをopenするので、
    最後にAozora.close()を呼ぶこと
    """
    # ルビと注を識別するための正規表現
    decoration = re.compile(r"([[^[]]*])|(《[^《》]*》)|[|※\n]")
    # 文章の終わりを識別する正規表現
    sentence_separater = re.compile(r"[。][」)]?[….―]*")

    def __init__(self, filename):
        self.file = open(filename, "r", encoding="shift-jis")
        self.reading_paragraph = None
        self.__skip_intro_and_read()

    @staticmethod
    def del_deco(sentence):
        """
        sentenceの装飾を外して素の文章にして返す
        私《わたくし》はその人を常に先生[何らかの説明]と呼んでいた。

        私はその人を常に先生と呼んでいた。
        """
        return re.sub(Aozora.decoration, "", sentence)

    def __skip_intro_and_read(self):
        """
        最初の説明を飛ばして、最初の段落を読み込む。
        """
        paragraph = self.file.readline()
        while paragraph[:2] != "--":
            if paragraph == "":
                print("Text file has wrong format.")
                exit(0)
            paragraph = self.file.readline()
        paragraph = self.file.readline()
        while paragraph[:2] != "--":
            paragraph = self.file.readline()
        self.__skip_empty_and_read()

    def __skip_empty_and_read(self):
        """
        次の段落をself.reading_paragraphに読み込む
        空の段落は飛ばす
        """
        self.reading_paragraph = self.del_deco(self.file.readline())
        while self.reading_paragraph == "":
            self.reading_paragraph = self.del_deco(self.file.readline())
        if self.reading_paragraph[:3] == "底本:":
            self.reading_paragraph = "\0"

    def next_sentence(self):
        """
        次の文を返す。
        すなわち次の文字から"。"まで、または次の文字から段落の終わりまでの文字列を返す。
        "。」"や"。)"なども考慮する。
        """
        if self.reading_paragraph == "\0":
            return 0
        result = re.search(self.sentence_separater, self.reading_paragraph)

        if result is None:
            # "。"が無い(表題など)。
            ans = self.reading_paragraph
            self.__skip_empty_and_read()
            return ans
        else:
            # "。"が段落中にある
            _, end = result.span()
            if len(self.reading_paragraph) == end:
                # 段落が終わっている
                ans = self.reading_paragraph
                self.__skip_empty_and_read()
                return ans
            else:
                # 段落が終わっていない
                ans = self.reading_paragraph[:end]
                self.reading_paragraph = self.reading_paragraph[end:]
                return ans

    def close(self):
        """
        ファイルを閉じる
        プログラムの最後で必ず呼ばれるべき関数
        """
        self.file.close()


def make_parser():
    """
    パーサーを作って、パーサーを返す。
    返り値に直接parse_arg()することが前提になっている。
    """
    parser = argparse.ArgumentParser(description="青空文庫を解析します。")
    parser.add_argument(dest="filenames", action="store", nargs="*",
                        help="青空文庫ファイルへのパス。例:kokoro.txt")
    return parser


def main():
    """
    関数本体
    """
    args = make_parser().parse_args()
    for filename in args.filenames:
        cvter = Aozora(filename)
        file = open(filename[:-4] + "_pure.txt", "w+", encoding="utf-8")

        sentence = cvter.next_sentence()
        while sentence:
            file.write(sentence + "\n")
            sentence = cvter.next_sentence()
        file.close()
        cvter.close()

main()

*1:全文がメモリに乗りきらない文章なんて存在しないんじゃないかとこの文章を書きながら思っている。

*2:四角カッコ[]が全角なのは間違いではない。なぜか青空文庫はキーボードから打ち込む方法がない記号をわざわざ使っている。

*3:“私はかなめの垣から若い柔らかい葉を※[#「てへん+劣」、第3水準1-84-77]《も》ぎ取って"のように、※は表示できない漢字を表す。そのため削除するべきではないかもしれないが、消さないでどうすればいいのか良くわからなかったので消すことにした。