形態素解析とは、文章を単語などの最小の意味単位に分解し、それぞれの品詞などを分析する技術だ。Pythonを使えば、手軽に形態素解析が可能になる。形態素解析ができると、さまざまな自然言語処理が行える。ここでは、形態素解析を利用して簡単な文書校正ツールを作成しよう。

  • 形態素解析をして簡単な文書校正ツールを作ってみよう

    形態素解析をして簡単な文書校正ツールを作ってみよう

形態素解析とは?

形態素解析を行うと文章を最小の意味単位である「形態素」に分割できる。例えば、「この猫の名前はタマです」という文章は、「この / 猫 / の / 名前 / は / タマ / です」のように分割される。それぞれの形態素に対して、「名詞」や「動詞」といった品詞の解析も行われる。

具体的には次のようになる。形態素ごとに分割されるだけでなく、文章中で語がどのような役割を果たしているのかまで把握できる。

  • 形態素解析の結果

    形態素解析の結果

Pythonでどうやって形態素解析をするの?

Pythonの「janome」というパッケージを使えば、手軽に形態素解析を実行できる。janomeをインストールするには、ターミナル(WindowsはPowerShell、macOSはターミナル.app)を起動し、次のコマンドを実行する。

$ pip install janome

次のようなプログラムを作成することで、形態素解析を実行できる。プログラムは「example.py」という名前で保存しよう。

from janome.tokenizer import Tokenizer

text = "この猫の名前はタマです"

# Tokenizerを生成する --- (※1)
tokenizer = Tokenizer()
# 形態素解析してトークンに分割する --- (※2)
tokens = tokenizer.tokenize(text)
# 結果を取りだして表示 --- (※3)
for token in tokens:
    surface = token.surface
    pos = token.part_of_speech
    print(f"| {surface} | {pos} |")

上記のプログラムを実行するには、下記のようなコマンドを実行する。プログラムを実行すると、「この猫の名前はタマです」という文章を形態素に分割して結果を表示する。

$ python example.py
| この | 連体詞,*,*,* |
| 猫 | 名詞,一般,*,* |
| の | 助詞,連体化,*,* |
| 名前 | 名詞,一般,*,* |
| は | 助詞,係助詞,*,* |
| タマ | 名詞,固有名詞,人名,名 |
| です | 助動詞,*,*,* |

プログラムの各部分を確認してみよう。(※1)では、janomeのTokenizerオブジェクトを生成する。(※2)では、tokenizeメソッドで形態素への分割と語の解析を行う。(※3)では、for ... in ... 文を使って、分割された形態素を画面に出力する。

文章の校正プログラムを作ってみよう

形態素解析を利用して、簡単な文章の校正ツールを作ってみよう。本格的なものを作ると、あっというまに長くなってしまうため、接続詞の繰り返しチェックと、一文の長さをチェックする機能だけを作って90行ちょっとのプログラムを作ってみた。

一気に掲載するには長いため、プログラム全体はこちらのGist( https://gist.github.com/kujirahand/cb811206070506fc4cecee6ff4a62cd3 )にアップしたので後から全体を確認してみよう。

最初に、文章の問題を発見する関数check_textの定義を見てみよう。

import sys
from janome.tokenizer import Tokenizer

# 形態素解析のためのTokenizerのインスタンスを生成 --- (※1)
tokenizer = Tokenizer()
# 接続詞の一覧 --- (※2)
setuzokusi = set()

def check_text(text):
    """テキストをチェックする"""
    errors = []
    # 改行コードを統一する --- (※3)
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    text = text.replace("\n", " ¶ ") # 行数をカウントするため「 ¶」を挿入
    text += " ¶ "
    # 形態素解析してトークンに分割 --- (※4)
    tokens = []
    for t in tokenizer.tokenize(text):
        pos = t.part_of_speech
        # 接続詞を抽出 --- (※5)
        if pos.startswith(("接続詞")):
            setuzokusi.add(t.surface)
        tokens.append(t.surface)
    # 一文の長さをチェックする --- (※6)
    errors += check_length(tokens)
    # 接続詞の繰り返しを検出 --- (※7)
    print("[INFO] 接続詞の繰り返しをチェックしています...", setuzokusi)
    for word in setuzokusi:
        errors += check_repeat(tokens, word, 8)
    errors.sort(key=lambda x: int(x.split(":")[0]))  # 行番号でソート
    if len(errors) == 0:
        print("[OK] 特に問題は見つかりませんでした。")
    else:
        for error in errors:
            print(f"[ERROR] {error}")
    return errors

上記のプログラムを確認しよう。(※1)では、形態素解析を行うために、janomeのTokenizerのインスタンスを作成する。(※2)では、接続詞の一覧を覚えておくためのset型を初期化する。(※3)では、改行コードを統一した後、エラーレポートのために改行コードを便宜的に記号「¶」に置き換えている。

(※4)では形態素解析を行って、文章を形態素に分割する。そして、文章全体を確認して(※5)で接続詞を検出し、変数setuzokusiに追加する。

(※6)では、一文の長さをチェックするために、この後定義した関数check_lengthを呼び出す。そして、(※7)では、関数check_repeatを呼び出して、接続詞の繰り返しがないかをチェックする。

長い一行を検出してエラーにする関数を作ろう

それでは、次に一行の長さをチェックする関数check_lengthを確認してみよう。この関数では句点までの文字数を調べて、100文字以上だった場合に、エラーを返すという仕組みにしている。

def check_length(tokens: list[str], max_length: int = 100):
    """一文の長さをチェックする"""
    errors = []
    s = ""
    line_no = 1
    for t in tokens:
        if t in ["¶", "。"]:
            s_len = len(s)
            if s_len > max_length:
                errors.append(f"{line_no}: 一文が長すぎます({s_len}文字)\n" + \
                    f"  - {s[:30]}…")
            s = ""
            if t == "¶":
                line_no += 1
            continue
        s += t
    return errors

ここでは、一文の長さを調べるために、文章の最初から末尾まで、改行や句点「。」を探すという処理にしている。文の区切りを見つけたら、カウンタ用の変数sをリセットするという処理になっている。そして、一文が100字以上の場合にエラーを出している。

連続する接続詞を検出する関数を作ろう

続いて、連続で出現する接続詞を調べる関数check_repeatを確認しよう。この関数では、8行以内に同じ接続詞が登場したらエラーを返すという仕組みにした。

def check_repeat(tokens: list[str], word: str, limit: int = 8):
    """繰り返しをチェックする"""
    # 近くに同じ接続詞が登場しないかチェック
    last_line = 0
    last_near = ""
    line_no = 1
    errors = []
    for i, t in enumerate(tokens):
        if t == "¶":
            line_no += 1
            continue
        if t != word:
            continue
        if last_line == 0:
            last_line = line_no
        elif line_no - last_line <= limit:
            near = "".join(tokens[i:i+10])
            errors.append(
                f"{line_no}:「{word}」が連続しています\n" + \
                f"  -{last_line:4}行目: {last_near}…\n" + \
                f"  -{line_no:4}行目: {near}…")
        last_line = line_no
        last_near = "".join(tokens[i: i+10])
    return errors

形態素解析で分割した単語(tokens)を、上から順に調べていって該当する接続詞が出て来たら、その位置をメモっておく。そして、その後limit行以内に同じ接続詞を見つけたら、エラーを表示するという処理になっている。

続く部分では、ファイルを読み出して、チェックするという処理になっている。

def check_file(file_path):
    """ファイルをチェックする"""
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
    # テキストをチェックする
    return check_text(text)

if __name__ == "__main__":
    if len(sys.argv) == 2:
        # コマンドライン引数でファイル名が指定された場合
        check_file(sys.argv[1])
    else:
        print("使い方: python proofreading.py [ファイル名]")

プログラムを実行してみよう

ここまで紹介したプログラムを「proofreading.py」という名前で保存したら、ターミナルで次のようなコマンドを実行することで、テキストファイルの問題を洗い出すことができる。例えば以下の実行例では「test.txt」という文章をチェックする。

python proofreading.py test.txt

本連載の第126回の原稿にわざと問題を追加して「test.txt」に保存してから試してみた。正しく問題を報告できた。

  • ターミナルで文章校正を実行したところ

    ターミナルで文章校正を実行したところ

GUIツールを作って利便性アップ

なお、コマンドラインからだと使いづらかったので、適当なGUIの画面を作成した。そのプログラムをこちら( https://gist.github.com/kujirahand/cb811206070506fc4cecee6ff4a62cd3?permalinkcommentid=5591601#gistcomment-5591601 )にアップした。テキストボックスに文章をコピーしてチェックができる。

この記事は
Members+会員の方のみ御覧いただけます

ログイン/無料会員登録

会員サービスの詳細はこちら