ちくわ君と学ぶ正規表現(日本語編)

森の研究所で、ちくわ君は日本語のテキストデータに悪戦苦闘していた。机の上には赤い木の実とともに、漢字・ひらがな・カタカナが入りまじった文書が山積みになっている。

「ちくわさん、お疲れさまです!」はんぺん君が研究所に入ってきた。「今日は日本語の資料なんですね」

「そうなんだ、はんぺん君」ちくわ君は腕を組んだ。「前に正規表現を教えただろう。あれは英語みたいに文字の種類が少ない言葉だと、すっきり書けるんだ。でも日本語は手ごわくてね」

「日本語だと、何が違うんですか?」はんぺん君は不思議そうに首をかしげた。

「考えてもごらん」ちくわ君は木の実を一つ口に含んだ。「日本語には『ひらがな』『カタカナ』『漢字』、それに全角や半角の英数字まである。一つの文の中に何種類もの文字が同居しているんだ。だからまず、『この文字はどの仲間か』を見分けるところから始めないといけない」

ひらがなを取り出す

「文字の仲間分けには、前にやった文字クラス『[ ]』が大活躍するんだ」ちくわ君はホワイトボードに向かった。「ひらがなは『あ』から『ん』まで順番に並んでいるよね。だから [あ-ん] と書けば、『ひらがなのどれか1文字』を表せる。うしろに + を付ければ、ひらがなの並びを丸ごと拾えるよ」

import re

text = "これはひらがなのテストです"
print(re.findall(r'[あ-ん]+', text))
# 出力: ['これはひらがなの', 'です']

「あっ、『テスト』のところで切れて、2つに分かれましたね」はんぺん君が気づいた。

「いいところに目をつけたね。『テスト』はカタカナだから、ひらがなのパターンには引っかからない。だからそこで途切れて、前と後ろの2かたまりになったんだ。文字の種類で区切られているのが、ちゃんと見てとれるだろう?」

カタカナと、長音・促音

「じゃあ今度はカタカナだ」ちくわ君は続けた。「同じ要領で [ア-ン] と書けばいい。外来語を拾うのに便利だよ」

text = "コンピューターでプログラミングする"
print(re.findall(r'[ア-ン]+', text))
# 出力: ['コンピュ', 'タ', 'プログラミング']

「あれ……?『プログラミング』はそのままなのに、『コンピューター』が『コンピュ』と『タ』に割れちゃいました」はんぺん君は目をぱちくりさせた。

「ふふ、これが日本語の落とし穴なんだ」ちくわ君は笑った。「よく見てごらん、割れたのはちょうど『ー』のところだろう。『ー』のような長音符や、『ッ』のような小さい文字は、実は『ア-ン』の範囲の外にいるんだ。だから『コンピューター』はそこで途切れてしまう。逆に『プログラミング』は長音符が入っていないから、無事に一かたまりのまま残ったんだよ。長音符や促音もカタカナ語の一部として扱いたいなら、範囲に足してあげればいい」

text = "コンピューターでプログラミングする"
print(re.findall(r'[ア-ンー・ッ]+', text))
# 出力: ['コンピューター', 'プログラミング']

「今度はちゃんと単語になりました!」はんぺん君は安心した。「範囲に『ー』や『ッ』を一文字ずつ加えるだけなんですね」

漢字を取り出す

「最後の文字種、漢字はもう少しやっかいだ」ちくわ君は新しい説明を始めた。「漢字は数がとても多くて、常用漢字だけでも2000字を超える。よく使われる範囲は、ざっくり [一-龯] で表すことが多いよ」

text = "今日は良い天気ですね"
print(re.findall(r'[一-龯]+', text))
# 出力: ['今日', '良', '天気']

「漢字のところだけ、きれいに残りましたね」はんぺん君は感心した。「『は』とか『ですね』みたいな、ひらがなは消えています」

「そう。助詞や語尾のひらがなは外れて、漢字だけが浮かび上がる。文章の“中身”を担う言葉は漢字に多いから、ざっとテーマをつかみたいときに役立つんだ」

3つを合わせて「日本語まるごと」

「ここまでの3つを [ ] の中で全部つなげると、日本語の文字をまとめて拾えるよ」ちくわ君は3つの範囲を一つにした。

text = "私はプログラミングが好きです"
print(re.findall(r'[あ-んア-ン一-龯]+', text))
# 出力: ['私はプログラミングが好きです']

「今度は文まるごと一つになりましたね」はんぺん君が言った。

「文の中身が全部『日本語の文字』だからね。逆に言うと、空白や記号、英数字が混ざっているところでだけ区切られる。どこまでを一かたまりと見なすかは、[ ] の中身しだいなんだ」

日本ならではの形:郵便番号

「文字の種類のほかに、日本特有の“形”もあるよ」ちくわ君は実用的な例を出した。「たとえば郵便番号。『123-4567』のように、3桁・ハイフン・4桁と決まっているね。頭の『〒』は、あってもなくてもいいように ? を付けておこう」

text = "住所は 〒123-4567 東京都渋谷区です"
print(re.findall(r'〒?\d{3}-\d{4}', text))
# 出力: ['〒123-4567']

「数字の桁数を {3}{4} で指定して、『〒』は ? で『あってもなくても』にしたんですね」はんぺん君はうなずいた。「形が決まっているものは、こうやって写し取れるんだ」

応用:文字種の割合を数える

「では今日の総仕上げだ」ちくわ君は分析ツールを書き始めた。「ある文章が、ひらがな・カタカナ・漢字をどんな割合で使っているかを数えてみよう。文章の“雰囲気”を測る、立派なテキストマイニングだよ」

def 文字種の割合(text):
    patterns = {
        'ひらがな': r'[あ-ん]',
        'カタカナ': r'[ア-ン]',
        '漢字':     r'[一-龯]',
    }
    total = len(text)
    for 種類, pat in patterns.items():
        count = len(re.findall(pat, text))
        print(f"{種類}: {count}文字 ({count / total * 100:.1f}%)")

文字種の割合("今日はプログラミングの勉強をしました")
# 出力:
# ひらがな: 7文字 (38.9%)
# カタカナ: 7文字 (38.9%)
# 漢字: 4文字 (22.2%)

「文章ごとに、文字の使われ方が数字で出てくるんですね……!」はんぺん君は画面に見入った。「カタカナが多ければ専門的な話、ひらがなが多ければやさしい文章、なんて見当もつけられそうです」

「その発想がまさにテキストマイニングの入り口だよ」ちくわ君は嬉しそうに目を細めた。

覚えておきたい、ふたつの注意

「最後に、日本語ならではの注意点を伝えておこう」ちくわ君は木の実をかじりながら言った。「まず文字コードだ。今はUTF-8がふつうだけれど、古いデータはShift_JISなどで保存されていることがある。読み込む文字コードを間違えると、文字化けして正規表現どころではなくなるんだ」

「それともうひとつ。正規表現は“形”しか見ないから、漢字の『読み』までは分からない。『山田』と『やまだ』は、別々の文字として扱われる。読みや品詞まで踏み込みたいときは、『形態素解析』という別の道具と組み合わせるといいよ」

夕日が研究所を照らし始めた頃、はんぺん君は満足そうに立ち上がった。「今日は日本語ならではの正規表現が学べました。文字の種類を見分けるところから始まるなんて、考えたこともなかったです」

「日本語は複雑だけれど、その分だけ豊かな情報がつまっている。正規表現を使いこなせれば、その情報を上手にすくい取れるようになるよ」ちくわ君は微笑んだ。「新聞記事でもブログでも、身近な日本語でぜひ練習してみてね」

はんぺん君が帰ったあと、ちくわ君は山積みの日本語文書に向き直った。文字種ごとに整理された結果が、画面の上にすっきりと並んでいく。森の研究所には、今日も日本語データと向き合う静かな喜びが満ちていた。

教育 正規表現 日本語処理 Python