はじめに
文字列中の半角スペースを正規表現を使って削除する、nikkieです。
先日、はんなりプログラミングの会のボーネンLT会にて文字列の正規化処理を話しました。
その中から、「文字列中の半角スペースの削除」をエントリ化しちゃいます!
※このエントリ中のコードは、Python 3.10.2で動作確認しています
目次
- はじめに
- 目次
- 文字列の正規化処理LTのコンテキスト
- 正規表現を使って半角スペース1つに揃えているとします
- 正規表現を使って、文字列中の半角スペースを削除する実装
- 終わりに
- P.S. 「歩き出そう Dreaming Way」の出典🎀
文字列の正規化処理LTのコンテキスト
形態素解析エンジンとして名を知られるMeCab。
MeCabは辞書を指定して使えます。
MeCabの辞書の1つとして知られるのが、mecab-ipadic-neologd。
この辞書は新語に強い辞書です(が、2020年で更新が止まってしまったと認識しています)。
mecab-ipadic-neologdの辞書データを作る上で実施している文字列の正規化処理は以下のページ「解析前に行うことが望ましい文字列の正規化処理」で確認できます。
以下の部分が「文字列中の半角スペースの削除」に該当します。
「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」間に含まれる半角スペースは削除
「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間に含まれる半角スペースは削除
これは正規表現を使って実装されています(WikiにはPythonの実装例もあります)。
LTやこのエントリでは、それを読み解きます。
なお、mecab-ipadic-neologdを指定して形態素解析をするときには、このWikiの正規化処理を適用しておくとよりよいという理解です。
正規表現を使って半角スペース1つに揃えているとします
文字列の前後の空白文字は、引数を与えないstrip()
メソッド1呼び出しで除けます(上で紹介したWikiにもあります)。
>>> " 前に半角スペース、後ろに全角スペースのあるテキスト ".strip() '前に半角スペース、後ろに全角スペースのあるテキスト'
文字列の中には
- 半角スペースだけでなく全角スペース
- スペース1つだけでなく、複数個の連続
もあるかもしれません。
Wikiにもあるのですが、これは正規表現で半角スペース1つに置換されているとします。
>>> import re >>> # 0x20(SPACE)か0x3000(IDEOGRAPHIC SPACE)の1つ以上の繰り返しを、0x20(SPACE)に置換します >>> re.sub("[ ]+", " ", "全角 や連続するスペース を含んだ 文字列") '全角 や連続するスペース を含んだ 文字列'
それでは、文字列中の半角スペースを削除する方法を見ていきます。
実装できると、最後の例は'全角や連続するスペースを含んだ文字列'
となります。
正規表現を使って、文字列中の半角スペースを削除する実装
以下の半角スペースを削除します。
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」間に含まれる半角スペース
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間に含まれる半角スペース
正規表現で文字の範囲を表す
Wikiにありますが、「半角英数字」と「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」は以下のように表すことができます。
>>> basic_latin = "\u0000-\u007F" # 半角英数字 >>> # 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」 >>> blocks = "".join( ... ( ... "\u4E00-\u9FFF", # CJK UNIFIED IDEOGRAPHS ... "\u3040-\u309F", # HIRAGANA ... "\u30A0-\u30FF", # KATAKANA ... "\u3000-\u303F", # CJK SYMBOLS AND PUNCTUATION ... "\uFF00-\uFFEF", # HALFWIDTH AND FULLWIDTH FORMS ... ) ... )
半角スペースを含む正規表現のパターン
正規表現パターンとしては3通りです。
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」 + 半角スペース + 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」 + 半角スペース + 「半角英数字」
- 「半角英数字」 + 半角スペース + 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」
文字列のformatメソッドを使って表します。
>>> pattern1 = re.compile("([{}]) ([{}])".format(blocks, blocks)) >>> pattern2 = re.compile("([{}]) ([{}])".format(blocks, basic_latin)) >>> pattern3 = re.compile("([{}]) ([{}])".format(basic_latin, blocks))
これらは「一文字 + 半角スペース + 一文字」に該当します。
後方参照を使って置換する
Wikiの例ですが、"アルゴリズム C"
を"アルゴリズムC"
とする事を考えましょう。
pattern2
、「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」 + 半角スペース + 「半角英数字」のケースですね。
「一文字 + 半角スペース + 一文字」だと「ム + 半角スペース + C」と該当します。
>>> # https://docs.python.org/ja/3/library/re.html#re.Pattern.search >>> m = pattern2.search("アルゴリズム C") >>> m <re.Match object; span=(5, 8), match='ム C'> >>> m.group(1) 'ム' >>> m.group(2) 'C'
これを「ムC」としたいわけですよね。
後方参照というもので実現します。
>>> pattern2.sub(r"\1\2", "アルゴリズム C") 'アルゴリズムC'
正規表現リテラルr"\1\2"
の\1
や\2
が後方参照です。
re.sub
のドキュメント2に説明がありました。
\6
のような後方参照は、パターンのグループ 6 がマッチした部分文字列で置換されます。(略)
\g<number>
は対応するグループ番号を使います。よって\g<2>
は\2
と等価ですが、(略)
r"\1\2"
はグループ1とグループ2を(半角スペースは含めずに)並べることになります。
search
したときの例で"アルゴリズム C"
では'ム C'
がマッチし、グループ1が'ム'
、グループ2が'C'
でした。
r"\1\2"
により'ムC'
(半角スペースが削除された文字列)となります!
sub
メソッドではパターンに該当する部分を全て置換できます(詳細は後述)。
>>> pattern2.sub(r"\1\2", "アルゴ B リズム C") 'アルゴB リズムC'
残った半角スペースはpattern3
(「半角英数字」 + 半角スペース + 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」)に該当しますね。
つまり、3つのパターンについて後方参照を使って、半角スペースを除くことで、文字列中の半角スペースは除けます!!
これを実装したのが上で示したgistです。
sub
メソッドはパターンに該当する部分を全て置換できるけれど気をつけて!
以下の例では半角スペースを全て削除できていますよね。
>>> pattern3.sub(r"\1\2", "A アルゴ B リズム C 本") 'Aアルゴ Bリズム C本' >>> pattern1.sub(r"\1\2", "アルゴ リズム 本") 'アルゴリズム本'
では「アイ の 歌声 を 聴か せ て」3ではどうでしょうか?
>>> pattern1.sub(r"\1\2", "アイ の 歌声 を 聴か せ て") 'アイの 歌声を 聴かせ て'
半角スペースが残りましたね😢
残った半角スペースに注目すると「の」「を」「せ」の右側と、両側に半角スペースのあった1文字の単語です。
これは「イ の」かつ「の 歌」と同時にマッチできないことから生じたと理解しました。
sub
メソッドで置き換えたときに、1文字の両側の半角スペースは片側しか置換できません。
なのでWikiではwhile
文で実装していると理解しました4。
>>> text = "アイ の 歌声 を 聴か せ て" >>> while pattern1.search(text): ... text = pattern1.sub(r"\1\2", text) >>> text 'アイの歌声を聴かせて'
終わりに
正規表現を使って、文字列中の半角スペースを削除する方法についてアウトプットしました。
ここで示した実装を使って
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」の間の半角スペース
- 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間の半角スペース
を削除できます!
ポイントは
- Unicodeのコードポイントで文字の範囲を表し
- 後方参照を使って半角スペースをなくした文字列に置き換える
の2点でしょうか。
この記事で段階を追ってまとめるにあたって、なぜwhile
文を使っているかといった発見もありました。
これは言ってみれば分かち書きの逆処理ですよね。
正規表現を使えば実現できる!というのは目からウロコでした。
$ echo '歩き出そう Dreaming Way' | mecab -Owakati 歩き 出 そう Dreaming Way
(gistのコードを-i
オプション付きで実行してから)
normalizer = RemoveWhitespaceNormalizer() normalizer.normalize("歩き 出 そう Dreaming Way") # '歩き出そうDreaming Way'
P.S. 「歩き出そう Dreaming Way」の出典🎀
歌詞をプログラミングの対象にするとめっちゃアガりますね。
- https://docs.python.org/ja/3/library/stdtypes.html#str.strip 「chars が省略されるか None の場合、空白文字が除去されます。」↩
- https://docs.python.org/ja/3/library/re.html#re.sub↩
-
大好きなアニメ映画です。12月は関東の映画館で見られるのでみんな観て!
(12/31 川崎でも!)↩アイの歌声を聴かせては以下の劇場で上映中または上映予定!(12/21 20:00時点に取得した情報です)
— sing_a_bot_of_harmony (@harmonizer_bot) 2022年12月21日
🌕CINEMA Chupki TABATA
🌕CINEMA NEKOhttps://t.co/hwRLkJ0Zqj -
見落としがあるかもしれませんが、
pattern2
,pattern3
はsubメソッド呼び出しが常に1回(1文字の両側の半角スペースというケースがないため)、pattern1
はsubメソッドの呼び出しが多くても2回(1文字の両側の半角スペースは2回のsubで除ける)なのでwhile
を使わずに実装もできると思います↩