自然言語100本ノック 2025 をやってみる(30-39)

の続きです。
こちらの「自然言語100本ノック 2025」の第四章です。
本章の主題
問題30から問題35までは、以下の文章text(太宰治の『走れメロス』の冒頭部分)に対して、言語解析を実施せよ。問題36から問題39までは、国家を説明した文書群(日本語版ウィキペディア記事から抽出したテキスト群)をコーパスとして、言語解析を実施せよ。
text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
メロスには政治がわからぬ。
メロスは、村の牧人である。
笛を吹き、羊と遊んで暮して来た。
けれども邪悪に対しては、人一倍に敏感であった。
"""

形態素解析にはMeCabを使用したいと思います。
まずは基本的なMeCabの使い方を学んでおきます。
また、使用する日本語辞書はunidicにします。
MeCabの基本的な使い方
mecabのインスタンスを生成し、parse関数の引数に文字列を渡すことで、形態素解析してくれます。
# MeCab動作確認
import MeCab
mecab = MeCab.Tagger()
text = "こんにちは!私の名前は山田太郎です。趣味は野球観戦です。"
result = mecab.parse(text)
print(result)
出力
こんにちは コンニチワ コンニチハ 今日は 感動詞-一般 5
! ! 補助記号-句点
私 ワタクシ ワタクシ 私-代名詞 代名詞 0
の ノ ノ の 助詞-格助詞
名前 ナマエ ナマエ 名前 名詞-普通名詞-一般 0
は ワ ハ は 助詞-係助詞
山田 ヤマダ ヤマダ ヤマダ 名詞-固有名詞-人名-姓 0
太郎 タロー タロウ タロウ 名詞-固有名詞-人名-名 1
です デス デス です 助動詞 助動詞-デス 終止形-一般
。 。 補助記号-句点
趣味 シュミ シュミ 趣味 名詞-普通名詞-一般 1
は ワ ハ は 助詞-係助詞
野球 ヤキュー ヤキュウ 野球 名詞-普通名詞-一般 0
観戦 カンセン カンセン 観戦 名詞-普通名詞-サ変可能 0
です デス デス です 助動詞 助動詞-デス 終止形-一般
。 。 補助記号-句点
EOS
上記のように、形態素解析して品詞を分析してくれます。
出力結果は文字列型です。カラム同士はタブ区切り・列はLF区切りになっています。
このままだと扱いにくそうなので、PandasのDataFrameに変換するコードも作成しておきます。
# MeCab出力結果をPandas DataFrameに変換
import pandas as pd
def parse_mecab_output(output):
lines = output.strip().split('\n')
morphs = []
for line in lines:
if line == 'EOS':
break
# MeCabの出力はタブ区切りで、各行は単語とその特徴が含まれる
parts = line.split('\t')
morph = {
'表層形': parts[0],
'読み': parts[1] if len(parts) > 1 else '',
'発音': parts[2] if len(parts) > 2 else '',
'語彙素': parts[3] if len(parts) > 3 else '',
'品詞': parts[4] if len(parts) > 4 else '',
'活用型': parts[5] if len(parts) > 5 else '',
'活用形': parts[6] if len(parts) > 6 else ''
}
morphs.append(morph)
return pd.DataFrame(morphs, columns=['表層形', '読み', '発音', '語彙素', '品詞', '活用型', '活用形'])
mecab = MeCab.Tagger()
text = "こんにちは!私の名前は山田太郎です。趣味は野球観戦です。"
result = mecab.parse(text)
df = parse_mecab_output(result)
df
結果
表層形 読み 発音 語彙素 品詞 活用型 活用形
0 こんにちは コンニチワ コンニチハ 今日は 感動詞-一般
1 ! ! 補助記号-句点
2 私 ワタクシ ワタクシ 私-代名詞 代名詞
3 の ノ ノ の 助詞-格助詞
4 名前 ナマエ ナマエ 名前 名詞-普通名詞-一般
5 は ワ ハ は 助詞-係助詞
6 山田 ヤマダ ヤマダ ヤマダ 名詞-固有名詞-人名-姓
7 太郎 タロー タロウ タロウ 名詞-固有名詞-人名-名
8 です デス デス です 助動詞 助動詞-デス 終止形-一般
9 。 。 補助記号-句点
10 趣味 シュミ シュミ 趣味 名詞-普通名詞-一般
11 は ワ ハ は 助詞-係助詞
12 野球 ヤキュー ヤキュウ 野球 名詞-普通名詞-一般
13 観戦 カンセン カンセン 観戦 名詞-普通名詞-サ変可能
14 です デス デス です 助動詞 助動詞-デス 終止形-一般
15 。 。 補助記号-句点
これで扱いやすくなりました。
実際の文章の中に現れた単語の形そのものを「表層形」というらしいです。
分かち書き
Taggerの引数に"-Owakati"を指定して分かち書きを試してみます。
# MeCab動作確認
import MeCab
mecab = MeCab.Tagger("-Owakati")
text = "こんにちは!私の名前は山田太郎です。趣味は野球観戦です。"
result = mecab.parse(text)
print(result)
結果
こんにちは ! 私 の 名前 は 山田 太郎 です 。 趣味 は 野球 観戦 です 。
"-Owakati"はMeCabの出力形式のオプションの一つで、「分かち書き」形式で出力されます。
分かち書き形式とは、単語ごとにスペースで区切って出力する形式です。

30.動詞
文章textに含まれる動詞をすべて表示せよ。
テキストを形態素解析→DataFrameに変換→品詞が"動詞"で始まるものをフィルタリング
text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
メロスには政治がわからぬ。
メロスは、村の牧人である。
笛を吹き、羊と遊んで暮して来た。
けれども邪悪に対しては、人一倍に敏感であった。
"""
# MeCabのインスタンス生成
mecab = MeCab.Tagger()
# テキストを解析
result = mecab.parse(text)
# MeCabの出力をDataFrameに変換
df = parse_mecab_output(result)
# 動詞だけ抽出
df_verbs = df[df['品詞'].str.startswith('動詞')]
df_verbs
表層形 読み 発音 語彙素 品詞 活用型 活用形
3 し シ スル 為る 動詞-非自立可能 サ行変格 連用形-一般
14 除か ノゾカ ノゾク 除く 動詞-一般 五段-カ行 未然形-一般
17 なら ナラ ナル 成る 動詞-非自立可能 五段-ラ行 未然形-一般
21 し シ スル 為る 動詞-非自立可能 サ行変格 連用形-一般
29 わから ワカラ ワカル 分かる 動詞-一般 五段-ラ行 未然形-一般
39 ある アル アル 有る 動詞-非自立可能 五段-ラ行 終止形-一般
43 吹き フキ フク 吹く 動詞-一般 五段-カ行 連用形-一般
47 遊ん アソン アソブ 遊ぶ 動詞-一般 五段-バ行 連用形-撥音便
49 暮し クラシ クラス 暮らす 動詞-一般 五段-サ行 連用形-一般
51 来 キ クル 来る 動詞-非自立可能 カ行変格 連用形-一般
58 対し タイシ タイスル 対する 動詞-一般 サ行変格 連用形-一般
68 あっ アッ アル 有る 動詞-非自立可能 五段-ラ行 連用形-促音便
抽出できていそうです

31. 動詞の原型
文章textに含まれる動詞と、その原型をすべて表示せよ。
No.30で作成した表からカラムを絞り込めばよさそうです
df_verbs_and_verbs_baseform = df_verbs[['表層形', '語彙素']].rename(columns={'語彙素': '基本形'})
df_verbs_and_verbs_baseform
結果
表層形 基本形
3 し 為る
14 除か 除く
17 なら 成る
21 し 為る
29 わから 分かる
39 ある 有る
43 吹き 吹く
47 遊ん 遊ぶ
49 暮し 暮らす
51 来 来る
58 対し 対する
68 あっ 有る

32. 「AのB」
文章textにおいて、2つの名詞が「の」で連結されている名詞句をすべて抽出せよ。
分かち書きして前後のワード含めて標準出力することにします。
text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
メロスには政治がわからぬ。
メロスは、村の牧人である。
笛を吹き、羊と遊んで暮して来た。
けれども邪悪に対しては、人一倍に敏感であった。
"""
# 分かち書きでテキストを解析して配列にする
mecab = MeCab.Tagger("-Owakati")
result = mecab.parse(text).split()
# 「の」の前後を取得
for index , word in enumerate(result):
if word == 'の':
# 一番最初、一番最後の「の」は無視
if index == 0 or index == len(result) - 1:
continue
# 前後を標準出力
print(f"「{result[index - 1]}」の 「{result[index + 1]}」")

33. 係り受け解析
文章textに係り受け解析を適用し、係り元と係り先のトークン(形態素や文節などの単位)をタブ区切り形式ですべて抽出せよ。
「係り受け解析」とは
「係り受け解析」とは語や文節の関係を解析することです。
たとえば:
「太郎が 花を 見た。」
という文を係り受け解析すると、
- 「太郎が」 → 「見た」に係る(主語と述語の関係)
- 「花を」 → 「見た」に係る(目的語と述語の関係)
と解析することができます。
係り受け解析ツール
係り受け解析を行うための代表的なツールは、CaboCha(MeCabベース)・GiNZA(SpaCyベース)などがあります。
ここまでMeCabを使ってきましたので、今回はCaboChaで解析してみようと思います。
CaboChaをWSLでセットアップしました。
pythonコード
import CaboCha
c = CaboCha.Parser()
text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
メロスには政治がわからぬ。
メロスは、村の牧人である。
笛を吹き、羊と遊んで暮して来た。
けれども邪悪に対しては、人一倍に敏感であった。
"""
tree = c.parse(text)
# チャンク数を取得
chunk_count = tree.chunk_size()
# チャンクを文字列に変換して返却する関数
def chunk_text(chunk):
return ''.join(
tree.token(j).surface
for j in range(chunk.token_pos, chunk.token_pos + chunk.token_size)
)
for i in range(chunk_count):
from_chunk = tree.chunk(i)
to_index = from_chunk.link
if to_index == -1:
continue # 係り先がない場合はスキップ
to_chunk = tree.chunk(to_index)
print(chunk_text(from_chunk), '\t', chunk_text(to_chunk))
出力結果
メロスは 激怒した。
激怒した。 決意した。
必ず、 除かなければならぬと
かの 邪智暴虐の
邪智暴虐の 王を
王を 除かなければならぬと
除かなければならぬと 決意した。
決意した。 わからぬ。
メロスには わからぬ。
政治が わからぬ。
わからぬ。 牧人である。
メロスは、 牧人である。
村の 牧人である。
牧人である。 暮して来た。
笛を 吹き、
吹き、 暮して来た。
羊と 遊んで
遊んで 暮して来た。
暮して来た。 敏感であった。
けれども 敏感であった。
邪悪に対しては、 敏感であった。
人一倍に 敏感であった。