#はじめに
最近自然言語処理系の仕事もあり、テキストデータの前処理なとで正規表現がとても便利だなと思いつつ、意外とちゃんと知らないこともあったので、備忘録の意味も踏まえて記事を投稿したいと思います。
#環境
macOS
python3.7
#正規表現とは
簡単にいうと、「文字列を一つの形式(パターン)で表現する手法」です。
パターンというのは「文字のならびの条件」のことで、「文字のならびの条件」を簡単に記述する方法が「正規表現」です。
つまり「文字列を記号などを使って簡単に表現」しようというものです。
これを利用することで文章の中から簡単に文字列を検索したり置換したりすることができます。
#正規表現の構造
正規表現のパターンは、「通常の文字」と「メタ文字」という特殊な役割を与えられた記号を組み合わさって成り立っています。
例えば
#「私」で始まる文字列
^私.*
#「らしい。」で終わる文字列
.*らしい。$
#「〇〇歳」と書かれた数値
[0-9]+歳
途中で入っている「.」「[」「^」「*」などの記号を「メタ文字」と呼びます。エクセルで文字列検索する時に「*」とか使いますね。SQLでいう「%」「_」とかもありますね。
#メタ文字について
###「.(ドット)」
正規表現での最もよく利用するメタ文字の一つである「.(ドット)」。
とにかくなんでもいい一文字を表現できます。
.ご飯
であればマッチする文字列は
朝ご飯、昼ご飯、夜ご飯などがマッチングします。
..ご飯
であればマッチする文字列は
炊込ご飯、混ぜご飯などがマッチングします。
「.」の数だけ何らかの文字を表現することができます。
###「+(プラス)」
一方で何文字入るかを指定しない場合は「+」を付け加えます。
「+(プラス)」は、直前のパターンの1回以上の繰り返しを表します。
直前の正規表現の「パターンの繰り返し回数」を指定するメタ文字を「量指定子」と呼びます。
.+ご飯。
.+にはとにかく文字が入っていればマッチングします。
###「*」
0回の繰り返しを含める表現です。同様に量指定子です。
とっ*ても食べたい
この場合はマッチする文字列は
とても食べたい
とっても食べたい
とっっっっても食べたい
となります。
###「?」
0回か1回だけ繰り返すというのを指定することもできます。
は〜?い
この場合マッチする文字列は
はい
は〜い
となります。
###「{min,max}」
細かい繰り返し回数自体を指定することもできます。「{」「}」波括弧を使って、繰り返し回数の上限、下限を指定できます。
はい!{1,3}
// マッチする文字列
はい!
はい!!
はい!!!
とすることができ
上限、下限は省略することもできます。
は〜{1,}い
この場合マッチする文字列は
は〜い
は〜〜い
は〜〜〜い
は〜〜〜〜い
となります。
は〜{,3}い
この場合マッチする文字列は
は〜い
は〜〜い
は〜〜〜い
となります。
###()
今まで一文字をずっと対象としてきましたが、()を使うことでグループ化することも可能です。
(そろり)+
この場合マッチする文字列は
そろり
そろりそろり
そろりそろりそろり
となります。
(じゃ)+〜ん
この場合マッチする文字列は
じゃ〜ん
じゃじゃじゃ〜ん
じゃじゃじゃじゃじゃ〜ん
となります。
###「|(パイプ)」
()では「|(パイプ)」で区切ることで、「いずれか」という意味合いを持たせることができます。
(塩|醤油|みそ|とんこつ)ラーメン
この場合は、括弧内のいずれかのマッチします。
[ ]
角括弧 [ ] を使って囲んだ文字にマッチさせることができる表現です。正規表現において「文字クラス」と呼ばれるものです
#全角半角数字
[01234567890123456789]
また文字コード(ASCIIコード)上で、連続するコードであれば、「-(ハイフン)」で開始終了位置を指定することができ、短縮して記述することができます。
[0-90-9]
###「否定文字クラス」
括弧内の頭に「^」を加えることで、否定文字クラスと呼ばれる指定した文字以外の一文字を表します。
[^0-90-9]+
数字が入っていない文字列がマッチングします。
###[追記しました]否定的先読み(?!)
(?!hoge)
とすることで、指定した文字列(hoge)を含まないという条件でのマッチングを行うことができます。これを否定的先読みと呼びます。
hogeを含まない文章
^(?!.*hoge).*$
hogeまたはfugaを含まない文章
^(?!.*(hoge|fuga)).*$
hogeという文字列から始まらない文章
^(?!hoge).*$
hogeという文字列で終わらない文章
^(?!.*hoge$).*$
hogeを含むが、fugaを含まない文章
^(?=.*hoge)(?!.*fuga).*$
###位置指定子としての「^(ハット)」
文字列の先頭や末尾は指定することが可能です。
このような位置を指定するメタ文字を「アンカー」や「位置指定子」と呼びます。
^君の名は.+
この場合マッチする文字列は
君の名は山田太郎
となります。
###「$」
行の末尾は「$」で指定できます。
.+太郎$
山田太郎
田中太郎
などなどがマッチします。
###正規表現におけるバックスラッシュ
正規表現においては「\(バックスラッシュ)」はよく使います。この記事でも多数使っています。
####メタ文字のエスケープ
メタ文字をそのまま文字として認識させたい時は「\(バックスラッシュ)」を使ってエスケープします。
https?://google\.com
####エスケープシーケンス
エスケープで開始する特殊なメタ文字「エスケープシーケンス」というものがあります。
エスケープシーケンスを使うとより短縮して記述をすることができます。
表現 | 意味 |
---|---|
\a | ベル文字 |
\cX | Ctrl + X |
\n | 改行コード |
\r | 改行コード |
\f | 改ページ |
\R | すべての改行コード(「\n\r\n\r」) |
\t | タブ |
\v | 垂直タブ |
\s | 空白文字(半角スペース、\t、\n、\r、\f) |
\S | 空白文字以外のすべての文字 |
\d | 数字。[0-9] |
\D | 数字以外の文字列。[^0-9] |
\w | 半角英数字とアンダースコアのうち任意の一文字。[a-zA-Z0-9_] |
\W | 半角英数字とアンダースコア以外の1文字[^a-zA-Z0-9_] |
\l | 半角英小文字のうち1文字 |
\L | 半角英小文字の以外の文字1文字(=英大文字、数字、全角文字) |
\u | 半角英大文字のうち1文字 |
\U | 半角英大文字以外の1文字(=英小文字、数字、全角文字) |
\0 | NULL文字( |
###便利な正規表現例
####郵便番号
[0-9]{3}-?[0-9]{4}
\d{3}-?\d{4}
###携帯電話番号
0[5789]0-?[0-9]{4}
0[5789]0-?\d{4}-?\d{4}
###改行のマッチング
\r|\n\r\|\n
###半角スペース、全角スペースのマッチング
[ ]
#python reモジュール
reモジュールには正規表現を利用した検索、置換、連結、分割などのメソッドが備えられています。
文字列もしくはMatchObject(マッチオブジェクト)インスタンスを返します。
※MatchObjectはマッチした文字列、マッチした文字列の開始位置、終了位置などの情報をもっています。
###注意点
モジュールを使う際には、正規表現パターンを複数回使用するかどうかで、コンパイルするかを判断します。
###コンパイル型
compile() 関数を使用し、正規表現オブジェクトを作成し re モジュール
同じパターンを複数回利用する場合は、コンパイルが1度だけで済むので、事前にコンパイルした方が便利です。
import re
>>> abc = r'abc'
>>> raw_text = "abcdefghijklmn"
>>> pattern = re.compile(raw_abc)
>>> matchobj = pattern.match(text)
>>> print(matchobj.group())
abc
>>> import re
>>> raw_abc = r'abc'
>>> text = "abcdefghijklmn"
>>> matchobj = re.match(raw_abc, text)
>>>
>>> print(matchobj.group())
abc
###raw文字列
正規表現のパターン文字列を定義するときに、クォーテーションの前にrを付けて利用することで、raw文字列として扱うことができ、エスケープシーケンスを無効にすることができます。
文字列の扱い方については[こちら](https://qiita.com/hiroyuki_mrp/items/1504645ab6eb1a6a4103)
>>> raw_abc = r'abc'
###日本語の扱い
日本語を扱う場合は、Unicodeに変換して使用します。unicodeについてはこちら
>>> uni_a_n = u'[ぁ-ん]'
※Python 3.x 系からはすべての文字列は Unicode として扱われるようになりました。
パターン | 説明 |
---|---|
[ぁ-ん] | 任意の全角ひらがな |
[ァ-ン] | 任意の全角カタカナ |
[ヲ-゚] | 任意の半角カタカナ |
[ぁ-んァ-ン] | 任意のひらがなとカタカナ |
[一-龥] | 任意の漢字 |
[A-Z]+ | 任意の全角大文字英語 |
[A-Z]+ | 任意の半角大文字英語 |
[a-z]+ | 任意の全角小文字英語 |
[a-z]+ | 任意の半角小文字英語 |
##reモジュールの使い方
本記事では一部の紹介をいたします。
####find() :文字列を検索
対象文字列.find(見つけたい文字列)
以下はfindメソッドでhappyという単語がtextにあるかどうか判定しています。
>>> text = "I am happy to eat lunch."
>>>
>>> index = text.find("happy")
>>> if index != -1:
... print(str(index)+"番目にあるよ")
... else:
... print("無いよ")
...
5番目にあるよ
このように、文字列が見つかった場合には、文字列の開始位置を返し、見つからなかった場合は -1 を返します。
####search() :文字列を検索
以下のように記述します。
Matchobject = re.search(正規表現, 検索対象の文字列)
第1引数に正規表現パターン、第2引数に検索したい文字列を指定します。
findとは違ってsearchはMatchobjectのインスタンスを返します。文字列が存在しない場合はNoneを返します。
>>> import re
>>> text = "123456abcedf789ghi"
>>> matchobj = re.search(r'[a-z]+', text)
>>> if matchobj:
... print(matchobj.group())
... print(matchobj.start())
... print(matchobj.end())
... print(matchobj.span())
...
abcedf
6
12
(6, 12)
このようにMatchobjectはマッチした文字列、文字列の開始位置、終了位置などの情報をもっています。
メソッド | 意味 |
---|---|
group() | マッチした文字列を返す。 |
start() | マッチした文字列の開始位置を返す。 |
end() | マッチした文字列の終了位置を返す。 |
span() | マッチした文字列の (開始位置, 終了位置) のタプルを返す。 |
※re.search は最初にマッチした文字列の情報しか取得できないことには注意が必要です。
####replace() :文字列の置換
対象の文字列.replace(置換される文字列, 置換する文字列 [, 置換回数])
上記のように記述をします。
以下は小文字cを大文字Cに置換する例です。
>>> raw_abc = r"aaaaabbbbbccccc"
>>> rep_raw_abc = raw_abc.replace("c", "C")
>>> print("変更前:",raw_abc, "変更後:",rep_raw_abc)
変更前: aaaaabbbbbccccc 変更後: aaaaabbbbbCCCCC
>>> raw_abc = r"aaaaabbbbbccccc"
>>> rep_raw_abc = raw_abc.replace("c", "C",2)
>>> print("変更前:",raw_abc, "変更後:",rep_raw_abc)
変更前: aaaaabbbbbccccc 変更後: aaaaabbbbbCCccc
####re.sub():文字列を置換する
re.sub(正規表現, 置換する文字列, 置換される文字列 [, 置換回数])
上記のように記述をします。
第1引数に正規表現パターン、第2引数には置換する文字列、第3引数には置換される文字列を指定します。replaceと同様、第4引数は省略できます。
以下は小文字のアルファベットの連続を0で置換した例です。
>>> import re
>>> raw_text = "abcdefgh01234567"
>>> sub_text = re.sub(r'[a-z]', "x", raw_text)
>>> print(sub_text)
xxxxxxxx01234567
>>> import re
>>> raw_text = "abcdefgh01234567"
>>> sub_text = re.sub(r'[a-z]+', "x", raw_text)
>>> print(sub_text)
xxxxxxxx01234567
####reの後方置後方参照 \1, \2, \3について
後方参照とは:正規表現内ですでにマッチしたテキストと同じもの(括弧で囲われた部分)
がもう一度現れたときに、それを再利用してマッチすることができるという正規表現の機能。
例えば 「<"the the">」という正規表現の「the」を[a-z]でマッチさせた場合に、「<"the the">」の正規表現は「<([a-z]+) \1>」となる。
後方参照をサポートするツールはマッチしたテキストを全て"覚えて"おり、マッチの1番目,2番め,3番目,… はそれぞれ「\1」,「\2」,「\3」, … と表すことができる。
import re
string = 'cat in the the hat'
#マッチしたものが\1に格納され再利用している
#これを利用すれば重複したものを見つけられる
re.findall(r'([a-z]+) \1', 'cat in the the hat')
#第一引数で「特定の単語(括弧で囲われた部分)が続いている文字列を指定」、第二引数で「その特定の単語」で置換
re.sub(r'(\b[a-z]+) \1', r'\1', string)
'cat in the hat'
こんなこともできる
re.sub(r'(\b[a-z]+) \1', r'\1 \1 \1 \1 \1', string)
'cat in the the the the the hat'
string2 = 'hiro yuki hiro hiro yuki yuki'
re.sub(r'(n[a-z]+) \1| (h[a-z]) \2', r'\2', string2)
'hiro yuki yuki yuki'
#終わり