普段正規表現が必要になるケースがそれなりに発生しているものの、体系立てて勉強したことがなかったので整理・まとめておきます。言語はPythonを使います。
結構長めです。これを読んだ皆さんはきっと「Pythonの正規表現完全に理解した」とつぶやいても怒られないはず。
そもそも正規表現って?
英語だとRegular Expression。文字列関係の特殊な操作をやる際に、特定のパターンを指定して色々な処理を行う表現(処理)のことを言います。
普通の文字列関係のビルトイン関数やモジュールなどでは対応が面倒(もしくは対応が難しい)な文字列操作などが正規表現を使うことでシンプルに扱えるケースがあります。
主に以下のような用途で使われます。
- 文字列の特殊な検索(例 : 曖昧検索や表記ぶれなどを含める等)
- 入力値のバリデーション(例 : 入力されたメールアドレスが有効な値になっているか等のチェック)
- 固定パターンの文字列の抽出や置換(例 : CSSのカラーコードを抽出したり、郵便番号から数値部分を抽出したり等)
正規表現の簡単な歴史
概念自体の初出は、読んではいませんが1943年の論文(A logical calculus of the ideas immanent in nervous activity)とのことです(被引用数がこの記事を書いている時点で2万弱・・・)。
正規表現だけでなく、ニューラルネットワークの古典的な論文としても有名なようです。この論文の時点ではまだ正規表現という単語は出てきていません。
初めて正規表現という単語が定義されたのが、1956年の論文(Representation of Events in Nerve Nets and Finite Automata)で、ここでregular setsやregular expressionという単語が出てきたそうです。
なんだか機械学習関係や人口生命(ALife)とかに絡んでいそうな論文が正規表現の元になっているというのは少し不思議な感じですね。
その後UNIXやB言語、Goなどで有名なケン・トンプソンさんによって1968年ごろに大きく正規表現が発展し、さらにPerlによる実装によって大きく普及します。
参考 : ケン・トンプソン - Wikipedia
このPerlによる実装が多くの他の言語などでも利用されるようになり、「Perl風な」正規表現が各言語に実装されていきます。後述するPythonの正規表現のreモジュールもPerl風な実装になっています。
この記事を書いている時点で、80年くらいの歴史があると考えるとなんだか凄いですね・・・。
追記 : この辺りの追加の詳しい話を@ryuta69 さんと@k-takata さんから、コメントでご共有いただきました・・!ありがとうございます(詳細はコメント欄をご確認ください)
使うもの
- Python 3.7.3(Anaconda)
- Windows10
- Pythonビルトインのreモジュール
- Jupyter notebook(マジックコマンド含む)
※pipなどで追加でインストールできる正規表現のライブラリ(例 : regex)などは本記事では扱いません。
正規表現のモジュールに関して
Pythonビルトインでは、reモジュールを使います(regular expressionの略ですね)。
以下のようにインポートできます。
import re
reモジュール 最初の一歩
多くのケースで、正規表現のパターンとそのパターンを反映する文字列の2つが必要になります。
例えば、特定の文章から「猫」という文字を検索する場合、以下のようにsearch関数を使って実現できます(※これだけならreモジュールを使わずとも普通に文字列関係の関数で実現できますが、サンプルとしてreモジュールを使います)。
patternの引数に正規表現のパターン、string引数に反映する文字列を指定します。
re.search(pattern=r'猫', string='吾輩は猫である。名前はまだ無い。')
search関数では、正規表現のパターンにヒットした場合にはMatchクラスのオブジェクトが返ってきます。Matchオブジェクトにはマッチした部分や文字の位置などが格納されています(spanがヒットした位置、matchがパターンにヒットした文字列)。
<re.Match object; span=(3, 4), match='猫'>
パターンの文字列の前のr
表記について
前述のコードのように、パターンの文字列はクォーテーションの前にrの文字を付与することが多く発生します。
これは、文字列を「生の文字列」として扱う表記となります(raw stringのrですね)。
これを使うことで、特殊な挙動をする文字列内のエスケープシーケンス(\
記号)関係がただの文字列になります。
例えば、改行を表す\n
の表記をPythonの文字列で使うと、実際にprintなどで出力した際に改行が含まれた状態で表示されます。
>>> print('吾輩は\n猫である。\n名前はまだ無い。')
吾輩は
猫である。
名前はまだ無い。
代わりに文字列の前にr記号を付与することで、\n
の挙動が無視され、ただの文字列として出力されます。
>>> print(r'吾輩は\n猫である。\n名前はまだ無い。')
吾輩は\n猫である。\n名前はまだ無い。
なぜ正規表現のパターンで生の文字列を使うのか?という点ですが、たとえばC:\Windows\py.exe
というWindowsのパスとかを考えてみます。
Pythonの文字列上で\
という文字列を扱うには、\\
といった具合に表記します(\
の文字自体をエスケープする形です)。
>>> print('C:\\Windows\\py.exe')
C:\Windows\py.exe
これを正規表現で検索などをしようとすると、そのままだと\\
のパターンを表現するのに、パターン側もさらにエスケープする必要があるので、\\\\
と4つ重ねる必要があります。
>>> re.search(pattern='\\\\py.exe', string='C:\\Windows\\py.exe')
<re.Match object; span=(10, 17), match='\\py.exe'>
文字列上では2個、パターンでは4つ、実際の表示上は1つ・・・みたいになっていると、結構混乱するというか、ミスしそうです。できたら、文字列とパターンが同じ感じで使えると楽ですね。
この解決策として、rの記号で生の文字列を指定することで、パターンと文字列の内容を一致させることができます。
以下のように生の文字列を使うことで、パターンも検索対象の文字列も両方\\
の表記で表現でき、分かりやすくなります。
>>> re.search(pattern=r'\\py.exe', string='C:\\Windows\\py.exe')
<re.Match object; span=(10, 17), match='\\py.exe'>
色々な意見があるとは思います(エスケープシーケンスが含まれるパターンにのみr記号を付与すべきだ、もしくは正規表現のパターンにはミスを防ぐために一通りr記号を付与すべきだetc)が、本記事では統一してパターンの文字列にはr記号を付与していきます。
OR条件(文字単体)
文字列の一部で、いずれかにヒットするみたいなOR条件的なパターンを使うには[]
の括弧を利用します(文字の集合といった具合に呼ばれます。英語だとcharacter classなど)。
例えば、「特定の文字部分が猫
もしくは犬
のどちらかにヒットする」といった条件のパターンは以下のように設定できます。
re.search(
pattern=r'吾輩は[猫犬]である。',
string='吾輩は猫である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
犬でもヒットします。
re.search(
pattern=r'吾輩は[猫犬]である。',
string='吾輩は犬である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は犬である。'>
他はもちろんヒットしません(返却値がMatchオブジェクトではなくNoneになります)。
match = re.search(
pattern=r'吾輩は[猫犬]である。',
string='吾輩は兎である。名前はまだ無い。')
>>> print(match)
None
なお、条件は2件だけではなく、どんどん増やすことができます。
re.search(
pattern=r'吾輩は[猫犬兎]である。',
string='吾輩は兎である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は兎である。'>
OR条件(文字列)
先ほどは[]
の括弧を使って文字単体のOR条件を試しました。文字単体ではなく文字列のOR条件を設定したい場合は条件の範囲を()
の括弧で囲み、文字列間に|
を挟みます(英語だとAlternation)。
例えば、猫である
と犬だよ
という文字列でOR条件を設定したい場合には以下のような書き方になります。猫犬両方の文字列でヒットします。
re.search(
pattern=r'吾輩は(猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
re.search(
pattern=r'吾輩は(猫である|犬だよ)。',
string='吾輩は犬だよ。名前はまだ無い。')
<re.Match object; span=(0, 7), match='吾輩は犬だよ。'>
文字単体の時と同様に、3件以上のOR条件を設定したい場合は|
記号による分割を増やしていくことで対応できます。
re.search(
pattern=r'吾輩は(猫である|犬だよ|兎です)。',
string='吾輩は兎です。名前はまだ無い。')
<re.Match object; span=(0, 7), match='吾輩は兎です。'>
特定の文字範囲のルールによるOR条件
例えば、数値ならなんでもOKといった条件を設定する際に、[0123456789]
と書くのは煩雑です。
そういった場合には、範囲の間にハイフンの-
の記号を設定することで対応ができます。
例として3~5の範囲ならなんでもOKとしたい場合は以下のようにハイフンの記号を使って表現できます。
re.search(
pattern=r'[3-5]匹の猫',
string='帰り道に3匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='3匹の猫'>
re.search(
pattern=r'[3-5]匹の猫',
string='帰り道に5匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='5匹の猫'>
範囲外の数値を指定してみると、返却値がNoneになっていることを確認できます。
match = re.search(
pattern=r'[3-5]匹の猫',
string='帰り道に6匹の猫を見かけた。')
print(match)
None
半角だけでなく、全角の数値でも使えます。
re.search(
pattern=r'[3-5]匹の猫',
string='帰り道に3匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='3匹の猫'>
re.search(
pattern=r'[3-5]匹の猫',
string='帰り道に5匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='5匹の猫'>
半角と全角両方対応させたい場合どうすればいいのでしょう?
そういった場合には[3-53-5]
といったように、範囲の指定を連続させて対応することができます。
以下のサンプルでは半角でも全角でもヒットしてくれます。
re.search(
pattern=r'[3-53-5]匹の猫',
string='帰り道に3匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='3匹の猫'>
re.search(
pattern=r'[3-53-5]匹の猫',
string='帰り道に3匹の猫を見かけた。')
<re.Match object; span=(4, 8), match='3匹の猫'>
続いてアルファベットです。数値と同様に、a-z
といった表現で対応できます。
re.search(
pattern=r'[b-d]at',
string='cat')
<re.Match object; span=(0, 3), match='cat'>
re.search(
pattern=r'[b-d]at',
string='bat')
<re.Match object; span=(0, 3), match='bat'>
大文字や全角のケースも数字と同様に設定できます。
re.search(
pattern=r'[B-D]at',
string='Cat')
<re.Match object; span=(0, 3), match='Cat'>
re.search(
pattern=r'[B-D]AT',
string='CAT')
<re.Match object; span=(0, 3), match='CAT'>
数値とアルファベットなど、それぞれの組み合わせを設定することも可能です。
re.search(
pattern=r'紙には[a-zA-Z0-9]',
string='紙にはDと書かれていた。')
<re.Match object; span=(0, 4), match='紙にはD'>
re.search(
pattern=r'紙には[a-zA-Z0-9]',
string='紙には8と書かれていた。')
<re.Match object; span=(0, 4), match='紙には8'>
ひらがなの範囲設定
ひらがなを指定するにはUnicodeのコード番号を指定することで対応ができます。
Python上では、Unicodeのコード番号による文字は\u<Unicode番号>
といったような形で表現できます。
たとえば、「あ」であれば\u3042
、「か」であれば\u304B
といった具合に表現できます。
>>> print('\u3042')
あ
>>> print('\u304B')
か
Unicodeの各コードの定義やひらがなの割り当て範囲はWikipediaにまとまっています。
平仮名 (Unicodeのブロック) - Wikipedia
範囲の指定をする際には[\u3040]-[\u309F]
といったパターンを指定すれば対応ができます。ただし、生の文字(r記号)として扱う場合はそちらを認識してくれなくなる(コード番号がそのまま設定されてしまう)ので、formatや%
などでUnicodeのコードを差し込む必要があります。
pattern = r'紙には「[{hiragana_start}-{hiragana_end}]」'.format(
hiragana_start='\u3040',
hiragana_end='\u309F',
)
print(pattern)
紙には「[-ゟ]」
※ひらがなの範囲は\u3040
~\u309F
ですが、\u3040
は未割当てなようで、
の文字化け時などに表示される記号(俗にいう豆腐化)で表示されます。実害は特にありません。
試してみると、確かにひらがな部分を認識してくれています。
re.search(
pattern=pattern,
string='紙には「か」という文字が書かれていた。')
<re.Match object; span=(0, 6), match='紙には「か」'>
Unicodeのコードをパターンに指定せず、直接文字を指定することでも対応ができます。
例えば、「ぁぃぅぇぉあいうえお」の範囲を指定したい場合は以下のように実現できます。
re.search(
pattern=r'紙には「[ぁ-お]」',
string='紙には「う」という文字が書かれていた。')
※Unicodeの順番的には、ぁ→あ→ぃ→い→...→お
となっています。
範囲外のものを指定するとちゃんとヒットしないことが確認できます。
match = re.search(
pattern=r'紙には「[ぁ-お]」',
string='紙には「か」という文字が書かれていた。')
print(match)
None
カタカナの範囲設定
カタカナもUnicodeのコード範囲が変わるだけで、ひらがなと同じような形で設定できます。
カタカナの範囲は\u30A0
~\u30FF
までが該当します。
pattern = r'紙には「[{katakana_start}-{katakana_end}]」'.format(
katakana_start='\u30A0',
katakana_end='\u30FF',
)
re.search(
pattern=pattern,
string='紙には「ガ」という文字が書かれていた。')
<re.Match object; span=(0, 6), match='紙には「ガ」'>
漢字の範囲設定
Unicodeには全部含めると6万以上の漢字が含まれています。
範囲の指定も色々あって、ひらがなとカタカナと比べると大分複雑です。
日本語圏だけで扱う上では、主だった漢字をカバーする際には主にCJK統合漢字という定義をカバーすれば良さそうではあります。
CJK統合漢字では、中国語と日本語、韓国語に使われる漢字のうち、字形と文字の意味がよく似ているものを同じ漢字として扱う。このため、日本語の文中に中国語を混ぜたい場合などに不都合があることが指摘されている。Unicodeでは全部で6万5536種類の文字を割り当てられるが、CJK統合漢字はこのうちの2万902文字を使用している。
CJK統合漢字
CJK統合漢字Unicodeの範囲は\u4E00
~\u9FFF
となります(参考 : CJK統合漢字 - Wikipedia)。
pattern = r'紙には「[{kanji_start}-{kanji_end}]」'.format(
kanji_start='\u4E00',
kanji_end='\u9FFF',
)
re.search(
pattern=pattern,
string='紙には「猫」という文字が書かれていた。')
<re.Match object; span=(0, 6), match='紙には「猫」'>
漢字関係を全部組み合わせると以下のようになるそうです。大分長めですね。
'[\u2E80-\u2FDF\u3005-\u3007\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\U00020000-\U0002EBEF]+'
※Pythonの正規表現で漢字・ひらがな・カタカナ・英数字を判定・抽出・カウントより引用。
否定を使った特定の文字範囲のルールによるOR条件
今度は否定条件を考えてみます。
[]
の括弧による、文字単体のパターンの場合には、[
の後に^
記号(キャレット記号)を置くことで設定できます。
以下、犬
と兎
ではヒットせず、パターンで指定されていない文字(今回は猫
)を指定した際にヒットするサンプルです。
match = re.search(
pattern=r'吾輩は[^犬兎]である。',
string='吾輩は猫である。名前はまだ無い。')
print(match)
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
match = re.search(
pattern=r'吾輩は[^犬兎]である。',
string='吾輩は兎である。名前はまだ無い。')
print(match)
None
任意の文字にヒットさせる : ドット記号.
ドットの記号をパターン内で使うと、任意の1文字(デフォルトでは改行を除く)でヒットさせることができます。
re.search(
pattern=r'吾輩は.である。',
string='吾輩は猫である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
re.search(
pattern=r'吾輩は.である。',
string='吾輩は犬である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は犬である。'>
他の範囲指定などの場合も同様ですが、連続して記述することで、その文字数分ヒットするようになります。例えば、...
と表記すれば任意の3文字でヒットします。
re.search(
pattern=r'吾輩は...である。',
string='吾輩は野良猫である。名前はまだ無い。')
<re.Match object; span=(0, 10), match='吾輩は野良猫である。'>
文字数が一致していない場合はヒットしません。
match = re.search(
pattern=r'吾輩は...である。',
string='吾輩は猫である。名前はまだ無い。')
print(match)
None
デフォルトの設定では、改行(\n
等)はドットではヒットしません。
match = re.search(
pattern=r'吾輩は.猫である。',
string='吾輩は\n猫である。名前はまだ無い。')
print(match)
None
ただし、後述するフラグ設定を調整することで、改行もヒットするように調整することは可能です。
数値範囲の代替 : \d
と\D
[0-90-9]
といった表記の代わりに、\d
という表記をパターンで使うと、数値の文字に対してヒットしてくれます。範囲で指定するよりも記述がシンプルになります。
半角と全角両方ともヒットします。
re.search(
pattern=r'\d匹の猫',
string='3匹の猫が公園で寝ていた。')
<re.Match object; span=(0, 4), match='3匹の猫'>
re.search(
pattern=r'\d匹の猫',
string='3匹の猫が公園で寝ていた。')
<re.Match object; span=(0, 4), match='3匹の猫'>
数値以外ではヒットしません。
match = re.search(
pattern=r'\d匹の猫',
string='三匹の猫が公園で寝ていた。')
print(match)
None
[]
の括弧で表現していた時には、キャレットの記号^
を使って[^345]
といった形で否定のパターンが表現できました。
\d
表記のような書き方で、数値以外のパターンを表現したい場合には大文字の\D
を使います。
先ほどとは逆に、以下のように漢字などでヒットし、数値はヒットしなくなります。
re.search(
pattern=r'\D匹の猫',
string='三匹の猫が公園で寝ていた。')
<re.Match object; span=(0, 4), match='三匹の猫'>
match = re.search(
pattern=r'\D匹の猫',
string='3匹の猫が公園で寝ていた。')
print(match)
None
スペースや改行、タブなどを一括して扱う : \s
と\S
パターン内で\s
という表記を使うと、スペースや改行、タブや空白などの文字を一括して扱うことができます。
半角スペースのケース :
re.search(
pattern=r'吾輩は\s猫である。',
string='吾輩は 猫である。名前はまだ無い。')
<re.Match object; span=(0, 9), match='吾輩は 猫である。'>
全角スペースのケース :
re.search(
pattern=r'吾輩は\s猫である。',
string='吾輩は 猫である。名前はまだ無い。')
<re.Match object; span=(0, 9), match='吾輩は\u3000猫である。'>
改行のケース :
re.search(
pattern=r'吾輩は\s猫である。',
string='吾輩は\n猫である。名前はまだ無い。')
<re.Match object; span=(0, 9), match='吾輩は\n猫である。'>
タブのケース :
re.search(
pattern=r'吾輩は\s猫である。',
string='吾輩は 猫である。名前はまだ無い。')
<re.Match object; span=(0, 9), match='吾輩は\t猫である。'>
数値のケースと同様に、\s
の代わりに大文字の\S
を使うと否定の表現ができます。
半角スペースなどがヒットしなくなるケース :
match = re.search(
pattern=r'吾輩は\S猫である。',
string='吾輩は 猫である。名前はまだ無い。')
print(match)
None
re.search(
pattern=r'吾輩は\S猫である。',
string='吾輩は子猫である。名前はまだ無い。')
<re.Match object; span=(0, 9), match='吾輩は子猫である。'>
文字や数字、アンダースコア範囲の代替 : \w
と\W
パターン内で\w
と表記すると、任意の文字や数字、アンダースコアにヒットします。
数値でヒットするケース :
re.search(
pattern=r'紙には「\w」',
string='紙には「3」と書かれていた。')
<re.Match object; span=(0, 6), match='紙には「3」'>
アルファベットでヒットするケース :
re.search(
pattern=r'すべてが\w',
string='すべてがFになる')
<re.Match object; span=(0, 5), match='すべてがF'>
全角でもヒットします。
re.search(
pattern=r'To be or \w\w\w',
string='To be or not to be, that is the question.')
<re.Match object; span=(0, 12), match='To\u3000be\u3000or\u3000not'>
ネットの記事では\w
が英数字でヒットする、と定義されていたり、正規表現本でもそう書いてあったのですが、厳密には以下の定義が正しそうなので注意が必要です。
\w
文字、数字、下線
[Python] 正規表現の表記方法のまとめ(reモジュール)
そのため、英数字以外の猫
といった文字でも普通にヒットします。
re.search(
pattern=r'吾輩は\wである。',
string='吾輩は猫である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
その他にも、アンダースコア(下線)にもヒットします。
re.search(
pattern=r'python\wand\wjavascript',
string='python_and_javascript')
<re.Match object; span=(0, 21), match='python_and_javascript'>
他の記号などはヒットしません。
match = re.search(
pattern=r'\wsimonritchie_sd',
string='私のTwitterアカウントのIDは@simonritchie_sdです。')
print(match)
None
スペースなどの空白文字もヒットしません。
match = re.search(
pattern=r'\wand javascript',
string='python and javascript')
print(match)
None
否定も他と同様で、\W
といった具合に大文字にすることで対応できます。
re.search(
pattern=r'\Wand javascript',
string='python and javascript')
<re.Match object; span=(6, 21), match=' and javascript'>
正規表現の数量詞
「任意の数字3文字」といった条件を指定したいとき、\d\d\d
というように記述しても実現できます。
ただ、記述が個数によっては若干煩雑になったりしますし、「任意のn回~m回の範囲の数字」みたいな条件や、「回数制限を設けずに繰り返される数だけ」マッチさせるケースなどには対応できません。
そういった場合には正規表現の数量詞という表現を使うと対応ができます。
数量詞の定義 :
一般に品詞としては形容詞、名詞のほか、「何回」「何倍」のように動詞や形容詞にかかる副詞(または相当する句)などとして使用される。
数量詞 - Wikipedia
英語ではQuantifierとなります。
以降のセクションでは数量詞に関して触れていきます。
文字のオプション指定
特定文字に対して「あっても無くてもOK」という条件をパターンに設定したい場合には、対象の文字の後に?
の数量詞の記号を付与します。
例えば、猫犬
でも猫犬兎
でもどちらでもOK(兎という文字があっても無くてもOK)としたい場合には、猫犬兎?
とパターンに指定します。
re.search(
pattern=r'猫犬兎?',
string='猫犬兎')
<re.Match object; span=(0, 3), match='猫犬兎'>
re.search(
pattern=r'猫犬兎?',
string='猫犬')
<re.Match object; span=(0, 2), match='猫犬'>
0回以上の任意の回数の繰り返し : *
特定の文字列のn回(非固定)の繰り返し部分でマッチして欲しい時にはアスタリスクの記号の数量詞を使います。
例えば、連続する猫
という文字の部分を検索したいときには以下のように設定します。
re.search(
pattern=r'猫*',
string='猫猫猫猫猫犬')
<re.Match object; span=(0, 5), match='猫猫猫猫猫'>
「0回以上」という定義になっているところに注意してください。たとえば、以下のように猫
という文字が含まれていなくても、「マッチした」と判定され、NoneではなくMatchオブジェクトが返ってきます(マッチ部分は空文字になります)。
re.search(
pattern=r'猫*',
string='犬犬兎兎')
<re.Match object; span=(0, 0), match=''>
1回以上の任意の回数の繰り返し : +
アスタリスク(*
)による数量詞にかなり性質が近いですが、0回以上ではなく「1回以上」という条件を設定する場合にはプラスの記号の+
の数量詞を使います。最低1つは対象の文字がないとマッチしません。
以下のように、アスタリスクの時はMatchオブジェクトが返ってきていたのが、Noneが返るようになります。
match = re.search(
pattern=r'猫+',
string='犬犬兎兎')
print(match)
None
対象の文字列が1件以上ある場合の挙動はアスタリスクの数量詞の時と同じです。
re.search(
pattern=r'猫+',
string='猫猫猫猫猫犬')
<re.Match object; span=(0, 5), match='猫猫猫猫猫'>
特定の文字数範囲の指定
n回~m回の範囲の繰り返しといった指定をしたい場合には{n,m}
という
Pythonでの配列のスライスなどのような感覚です。
例えば、2回以上4回以下の文字の繰り返しのみヒットするパターンを書きたい場合は以下のようにパターンを設定します。
re.search(
pattern=r'猫{2,4}',
string='猫猫猫猫猫犬')
<re.Match object; span=(0, 4), match='猫猫猫猫'>
結果のMatchオブジェクトを見て分かる通り、2回以上4回以下としているので、結果のマッチ部分も4文字までになっています。
2回以上と指定しているので、1文字しかないケースなどは、ヒットしません。
match = re.search(
pattern=r'猫{2,4}',
string='猫犬')
print(match)
None
なお、辞書のコードを書くときのように、{2, 4}
といった具合にコンマの後にスペースを入れると認識してくれなくなるので注意してください。エラーなども無く、単純にヒットしなくなります。
先ほどヒットした条件が、スペースが入っていることによってヒットしなくなるケース :
match = re.search(
pattern=r'猫{2, 4}',
string='猫猫猫猫猫犬')
print(match)
None
なお、開始と終了のインデックスを指定する以外にも、配列のインデックスやスライスのように、複数の書き方で挙動が変わります。
単一の数値のみのケース :
{n}
という形で単一の数値のみ記載した場合には、固定で「n回繰り替えされている」場合にのみヒットします。
re.search(
pattern=r'猫{3}',
string='猫猫猫犬')
<re.Match object; span=(0, 3), match='猫猫猫'>
文字数が一致していない場合にはヒットしません。
match = re.search(
pattern=r'猫{3}',
string='猫猫犬')
print(match)
None
開始の値とコンマのセットのケース
開始の値とその後にコンマを付与して{n,}
と書いた場合、「n回以上」という条件になります。
例えば、{3,}
とすれば3回以上で一通りヒットします。
re.search(
pattern=r'猫{3,}',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
re.search(
pattern=r'猫{3,}',
string='猫猫猫猫猫')
<re.Match object; span=(0, 5), match='猫猫猫猫猫'>
開始回数を満たさない場合にはヒットしません。
match = re.search(
pattern=r'猫{3,}',
string='猫猫')
print(match)
None
コンマと終了値のセットのケース :
コンマと終了値のみで{,m}
といった感じに指定した場合、「m回以内の部分」がヒットします。
re.search(
pattern=r'猫{,3}',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
re.search(
pattern=r'猫{,3}',
string='猫猫犬')
<re.Match object; span=(0, 2), match='猫猫'>
パターンで指定した回数よりも多い回数分繰り返されている場合には、指定した回数分までが結果のMatchオブジェクトに含まれるようになります。
re.search(
pattern=r'猫{,3}',
string='猫猫猫猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
なお、以下のように{}
の括弧による指定をしなくても、すでに用意されている正規表現の記号で表現できるケースがあります。そういった場合はそちらの記号を使った方がシンプルで且つぱっと見で意味が把握しやすいケースがあるかもしれません。
例えば、以下のようなケースがあります。
{,1}
と?
記号のケース :
直前の文字がオプション(あっても無くてもOK)という条件の指定です。{,1}
でも?
記号でもどちらも同じ挙動になります。
re.search(
pattern=r'猫犬兎{,1}',
string='猫犬')
<re.Match object; span=(0, 2), match='猫犬'>
re.search(
pattern=r'猫犬兎?',
string='猫犬')
<re.Match object; span=(0, 2), match='猫犬'>
{0,}
と*
記号のケース :
{0,}
と*
も両方とも「0件以上の繰り返し(該当の文字が無くてもMatchオブジェクトが返る)」という条件になります。
re.search(
pattern=r'猫{0,}',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
re.search(
pattern=r'猫{0,}',
string='犬')
<re.Match object; span=(0, 0), match=''>
re.search(
pattern=r'猫*',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
re.search(
pattern=r'猫*',
string='犬')
<re.Match object; span=(0, 0), match=''>
{1,}
と+
記号のケース :
{1,}
と+
記号で、両方とも1件以上の繰り返しの条件になります。
re.search(
pattern=r'猫{1,}',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
match = re.search(
pattern=r'猫{1,}',
string='犬')
print(match)
None
re.search(
pattern=r'猫+',
string='猫猫猫')
<re.Match object; span=(0, 3), match='猫猫猫'>
match = re.search(
pattern=r'猫+',
string='犬')
print(match)
None
欲張りな数量詞と不承不承な数量詞
猫
という文字で始まり、猫
という文字で終わるパターンを考えてみます。
これは、任意の文字単体でヒットするドット記号.
と1件以上の繰り返しのプラス記号+
を組み合わせて、猫.+猫
というパターンで表現することができます。
re.search(
pattern=r'猫.+猫',
string='犬猫犬猫犬猫犬猫犬')
<re.Match object; span=(1, 8), match='猫犬猫犬猫犬猫'>
猫犬猫犬猫犬猫
という部分がマッチしました。ただ、元の文字列を見てみると、猫犬猫
という文字列部分も元の想定したパターンにマッチしています。今回の正規表現のパターンでは、なるべく長い文字列になるようにマッチしています。
このような挙動を正規表現の性質をgreedyと呼ばれたりします。もしくはgreedy quantifier(最大量指定子)などと呼ばれます。greedyを日本語にすると「欲張りな」「貪欲な」といった意味になる点から分かる通り、なるべくパターンにマッチする範囲で「貪欲に」文字数が多くなるようにマッチします。Pythonのreモジュールでは、基本的にデフォルトではこのgreedy側の挙動になります。
反対に、猫犬猫
といったようになるべくマッチ結果が短くなるように設定したい場合には?
の記号を繰り返し指定の後などに設定することで表現できます。今回のサンプルでは繰り返し指定の+
記号の後に入れることで対応できます。
re.search(
pattern=r'猫.+?猫',
string='犬猫犬猫犬猫犬猫犬')
<re.Match object; span=(1, 4), match='猫犬猫'>
このような性質を正規表現界隈では英語reluctantと呼びます。
「控えめな」とか「渋々な」といったような意味になります。もしくはnon-greedyとか呼ばれたりもしますし、最短マッチとも表現されたりもします。
reluctant quantifierで最小量指定子とも呼ばれたりもします。
最短マッチの用途の一例として、HTMLタグのように開始タグと終了タグがあるようなケースがあります。例えば<div>
タグで囲まれた部分を取りたい際に、最短マッチを使うことで個別の<div>
タグ部分を抽出したりすることができます。
re.search(
pattern=r'<div>.+?</div>',
string='<div>猫</div><br><br><div>犬</div>')
<re.Match object; span=(0, 12), match='<div>猫</div>'>
greedyな指定のままだと、余分なところが色々マッチしてしまい、想定した結果になってくれません。
re.search(
pattern=r'<div>.+</div>',
string='<div>猫</div><br><br><div>犬</div>')
<re.Match object; span=(0, 32), match='<div>猫</div><br><br><div>犬</div>'>
なお、この?
記号は、前述のセクションで触れたように、単体で使うと「文字のオプション指定(特定文字に対して「あっても無くてもOK」という条件)」という意味も持ちます。組み合わせによって挙動が変わるので注意が必要です(繰り返し記号と一緒に使われていたりすると最短マッチになります)。
greedyとreluctantに関してですが、挙動の違い以外にもパフォーマンスにも差が出てきます。
例えば、長い分などに対してgreedyな条件で設定していると、「マッチ条件を満たす限り」次の文字に対してチェックをしていき、なるべく多くの文字でマッチしようと1文字1文字チェックされていくので、基本的にgreedyの方が遅くなりがちです。
reluctant(最短マッチ)の方は、最小限のところを満たせばそこでチェックが終わるのでパターンが短ければ処理がgreedyほど遅くはなりにくいという性質があります。
行の先頭を表す^
記号
^
の記号をパターンに設定すると、「行の先頭」の表現になります。
例えば、^猫
とすると、先頭が猫
になっていないとヒットしなくなります。
re.search(pattern=r'^猫', string='猫犬兎')
<re.Match object; span=(0, 1), match='猫'>
以下のケースでは行の先頭が猫
になっていないのでヒットしません。
match = re.search(pattern=r'^猫', string='犬猫兎')
print(match)
None
注意点として、デフォルトだと複数行の文字列の時でも、^
の指定は文字列の先頭しか該当しません。例えば、2行目の先頭が猫
という文字になっているケースでも、ヒットしてくれません。
match = re.search(pattern=r'^猫', string='犬兎\n猫犬')
print(match)
None
複数行を対象にしたい場合には、flags=re.MULTILINE
というフラグ設定のオプションを指定する必要があります。フラグ設定に関しては後で詳細を触れますのでここでは割愛します。
re.search(pattern=r'^猫', string='犬兎\n猫犬', flags=re.MULTILINE)
<re.Match object; span=(3, 4), match='猫'>
行の末尾を表す$
記号
行の先頭を表す^
の逆に、$
の記号は「行末」を表します。例えば、行の最後が猫
になっているケースを検索する際には以下のようになります。
re.search(pattern=r'猫$', string='犬兎猫')
<re.Match object; span=(2, 3), match='猫'>
行末に指定した文字が存在しない場合はマッチしません。
match = re.search(pattern=r'猫$', string='犬猫兎')
print(match)
None
また、^
の時と似たように、デフォルトだと複数行になった時には最後の行がマッチします。^
の記号の時は最初の行のみヒットしますがこちらは最後の行となる点には注意してください。
途中の行末で猫
で終わっているものの、マッチしないケース :
match = re.search(pattern=r'猫$', string='犬兎猫\n犬')
print(match)
None
最終行の最後の文字が猫
なのでマッチするケース :
re.search(pattern=r'猫$', string='犬兎\n犬猫')
<re.Match object; span=(4, 5), match='猫'>
^
記号と同様に、途中の行でも毎回行末でマッチ判定したい場合には、こちらもflags=re.MULTILINE
のフラグを設定します。
re.search(pattern=r'猫$', string='犬兎猫\n犬', flags=re.MULTILINE)
<re.Match object; span=(2, 3), match='猫'>
単語境界を表す\b
と\B
単語境界(Word boundary)という単語だと少し小難しい気がしますが、単語の区切り部分と隣接しているかを示します。
例えば、Java tea
といった文字列を対象とした場合、スペース部分が区切り部分となります。スペース以外にも記号などに関しても区切り部分と判定されます。
Java\b
というパターンを使えば、Java
という単語の後にスペースや記号などで区切られていればマッチする、といった具合です。
Java
の後がスペースによる区切り文字なのでマッチするケース :
re.search(
pattern=r'Java\b',
string='Java tea')
<re.Match object; span=(0, 4), match='Java'>
Java
の後に区切り文字が無くマッチしないケース :
match = re.search(
pattern=r'Java\b',
string='JavaScript')
print(match)
None
Java
の後が記号による区切り文字なのでマッチするケース :
re.search(
pattern=r'Java\b',
string='Java?')
<re.Match object; span=(0, 4), match='Java'>
なお、日本語はスペースなどで単語が区切られない(複雑な形態素解析などが必要になる)ため、Word boundaryという名前ではあるものの単語単位での境界としては処理されません。句読点やスペースなどが区切り文字としてマッチします。
句読点でマッチするケース :
re.search(
pattern=r'こんにちは\b',
string='こんにちは、今日はいい天気ですね')
<re.Match object; span=(0, 5), match='こんにちは'>
スペースでマッチするケース :
re.search(
pattern=r'こんにちは\b',
string='こんにちは 今日はいい天気ですね')
<re.Match object; span=(0, 5), match='こんにちは'>
英語のサンプルと同様に、文字が連続している(区切り文字が無い)場合にはマッチしません。
match = re.search(
pattern=r'こんにちは\b',
string='こんにちは今日はいい天気ですね')
print(match)
None
否定は、他の\d
などの指定と同様に、\B
というように大文字にすることで対応ができます。
以下のサンプルではパターンに\B
を使っているため、先ほどの逆のケースにマッチします。
match = re.search(
pattern=r'こんにちは\B',
string='こんにちは 今日はいい天気ですね')
print(match)
None
re.search(
pattern=r'こんにちは\B',
string='こんにちは今日はいい天気ですね')
<re.Match object; span=(0, 5), match='こんにちは'>
文字列の先頭と最後にマッチする\A
と\Z
行の先頭を表す^
や行末を表す$
の記号に近いものになりますが、\A
で対象の文字列内の先頭を表し、\Z
で対象の文字列内の最後を表します。
^
とは異なり、flags=re.MULTILINE
(詳細は後述)を指定しても、行の先頭基準ではマッチしません。複数行の文字列の場合、本当に先頭の位置のみにヒットします。
re.search(
pattern=r'\A猫',
string='猫が居た。\n犬はいなかった。')
<re.Match object; span=(0, 1), match='猫'>
以下のように、flags=re.MULTILINE
が指定されていても、2行目の先頭でパターンの文字が含まれている場合でもマッチしません。
match = re.search(
pattern=r'\A猫',
string='犬が居た。\n猫はいなかった。',
flags=re.MULTILINE)
print(match)
None
\A
とは逆に、文字列の最後を扱いたい場合には\Z
を使います。アルファベット的に、Aが最初でZが最後といった感じでしょうか。
re.search(
pattern=r'猫\Z',
string='犬犬兎\n兎猫')
<re.Match object; span=(5, 6), match='猫'>
ここまでで色々な正規表現のパターンの書き方や記号などを見てきました。
次のセクションからはPythonのreモジュールについて色々深堀りしていきます。
パターンのコンパイル
reモジュールのpatternの引数には、コンパイル済みのパターンもしくは文字列を指定することができます。今まではサンプルでは一通り文字列を指定する形で対応してきました。
パターンをコンパイルすることで、C言語で書かれた正規表現のエンジンで処理が実行されます。ただ、文字列で指定した場合はコンパイルされずに遅い処理がされるということはなく、文字列で指定した際も内部でコンパイルして処理してくれます。
パターンをコンパイルするには以下のようにre.compile関数を使います。
pattern = re.compile(pattern=r'猫.+犬')
コンパイルしたパターンは、reモジュールにあるようなメソッドが色々用意されています。そちらを使うことで、reモジュールの関数でpatternの引数を指定していた時と異なり、文字列の引数だけで処理ができます。
pattern.search(string='猫猫猫犬犬兎犬犬猫犬')
<re.Match object; span=(0, 10), match='猫猫猫犬犬兎犬犬猫犬'>
reモジュールの関数の引数に指定することもできます。
re.search(pattern=pattern, string='猫猫猫犬犬兎犬犬猫犬')
<re.Match object; span=(0, 10), match='猫猫猫犬犬兎犬犬猫犬'>
上記を踏まえると、正規表現の処理をする際に以下の3つのやり方があることになります。
- [1]. パターンをコンパイルしてそのパターンのメソッドで処理する
- [2]. パターンをコンパイルして、reモジュールの関数の引数に指定する
- [3]. reモジュールで文字列を指定する
[1]~[3]それぞれ、実行結果は一緒になります。ただし、(今回試すサンプルではナノ秒の世界なので大した負荷ではないのですが)3つの方法である程度パフォーマンスは異なるようです。
大規模なデータセットや、かなりの長文に対して実行するような、パフォーマンスが求められるケースではしっかり計測して早いものを選択するといいかもしれません。普段少し使うレベルや、webサイトなどで短い文字列に対してバリデーションで使うとかであれば誤差だとは思います。
[1]と[2]が事前にコンパイルしているので早そうに思えますが、[3]のように文字列を直接パターンに指定するケースでも、実はPython側で自動でコンパイル結果のパターンがメモリ上にキャッシュされるそうなので単純に[3]の文字列を指定するケースが遅くはなりません(初回だけ僅かにコンパイルで時間がかかり、その後はコンパイルがスキップされ少し早くなります)。
実際に私の環境で今回少し試した感じでは[3]の方が[2]よりも早くなっています。
[1]のコンパイル済みのオブジェクトのメソッドを使うケース :
%%timeit
pattern.search(string='猫猫猫犬犬兎犬犬猫犬')
290 ns ± 15.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
[2]のコンパイル済みのオブジェクトをreモジュールの関数の引数に指定するケース :
%%timeit
re.search(pattern=pattern, string='猫猫猫犬犬兎犬犬猫犬')
995 ns ± 50.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
[3]のreモジュールでパターンに文字列を指定するケース :
※キャッシュの都合、一度Jupyterのカーネルを再起動しています。
%%timeit
re.search(pattern=r'猫.+犬', string='猫猫猫犬犬兎犬犬猫犬')
803 ns ± 72.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
なお、キャッシュされたコンパイルパターンは、purge関数でキャッシュをクリアすることができます。
些細なメモリ量なので、あまりに古いPCであったりとか、パターンが膨大になるといったケースを除いて基本的にほぼ使わない気はします。
re.purge()
match関数
match関数はsearch関数と同様に検索系の関数です。search関数と異なり、文字列の「先頭から」一致していないとマッチしません。
文字列の先頭からチェックして、パターンと一致しているのでマッチするケース :
>>> re.match(pattern=r'猫犬', string='猫犬兎猫兎')
<re.Match object; span=(0, 2), match='猫犬'>
文字列の途中からパターンがマッチしているものの、先頭からではないので結果がNoneになるケース :
>>> match = re.match(pattern=r'猫犬', string='犬猫犬兎猫兎')
>>> print(match)
None
※他の言語とかだとPythonで言うsearch関数がmatch関数に該当したりするのケースが結構あるので、うっかりsearchの感覚でmatchを使ってしまったりはご注意ください。
search関数
これまでにもサンプルで色々使ってきたsearch関数も深堀りしておきます。
search関数は、matchとか異なり、文字列の途中でもパターンに該当すればマッチします。
>>> re.search(pattern=r'猫犬', string='犬犬猫犬兎猫兎')
<re.Match object; span=(2, 4), match='猫犬'>
findall関数
findall関数はsearch関数などとは異なり、実行結果がMatchオブジェクトではなく文字列のリストで返却されます。
マッチした個数分のリストになります。
>>> re.findall(pattern=r'猫犬', string='猫犬犬猫犬兎犬猫犬')
['猫犬', '猫犬', '猫犬']
なお、基本的には文字列のリストが返ってくるのですが、パターン中にグループの指定の()
の括弧が複数あるとリストの中身がtupleになるケースがあります。
グループに関しては後のセクションで詳しく触れます。
パターン中に1件のグループの()
の括弧がある場合は文字列のリストのままで、2件以上ある場合はtupleのリストになります。
パターン内のグループの括弧が1件で文字列のリストが返るケース :
>>> re.findall(pattern=r'(猫犬)', string='猫犬犬猫犬犬兎犬猫犬兎')
['猫犬', '猫犬', '猫犬']
パターン内のグループの括弧が2件あってtupleのリストが返るケース :
re.findall(pattern=r'(猫犬)(犬)', string='猫犬犬猫犬犬兎犬猫犬兎')
[('猫犬', '犬'), ('猫犬', '犬')]
finditer関数
finditer関数はfindall関数のように、文字列内でヒットした箇所を全て取得することができます。
違いとして、findall関数では文字列のリストなどで結果が返る一方で、finditer関数はMatchオブジェクトを格納したiterableオブジェクトが返ります。
Matchオブジェクトが取れるので、一致箇所の文字列の位置が取りたい場合などに便利てす。
iterable = re.finditer(pattern=r'猫犬', string='猫犬犬猫犬兎犬猫犬')
for match in iterable:
print(match)
<re.Match object; span=(0, 2), match='猫犬'>
<re.Match object; span=(3, 5), match='猫犬'>
<re.Match object; span=(7, 9), match='猫犬'>
fullmatch関数
fullmatch関数は、パターンが文字列全体でマッチしているかどうかで判定がされます。
パターンにはマッチしているけれども余分な文字列が含まれている場合などにはマッチしません。
例えば、電話番号やメールアドレスのバリデーションで、フォーマットは電話番号やメールアドレスの形式になっているものの、余分な文字列が含まれている場合のチェックなどに便利です。
例えば、以下のように電話番号のフォーマットのチェックなどでMatchオブジェクトが返るかどうかで判定ができます。
re.fullmatch(
pattern=r'\d{2,3}-\d{4}-\d{4}', string='090-1234-5678')
<re.Match object; span=(0, 13), match='090-1234-5678'>
\d
は任意の数字、{}
の括弧は繰り返し数の指定です。
以下のように、フォーマットはマッチしているものの、余分な文字が含まれている場合にはマッチしません。
match = re.fullmatch(
pattern=r'\d{2,3}-\d{4}-\d{4}', string='猫090-1234-5678犬')
print(match)
なお、このfullmatch関数はPython3.4以降での追加となります。もう3.4自体もサポート切れているので、それ以前のバージョンはほとんど使われていないとは思いますが、古いPythonバージョンの場合には最後の位置を表す\Z
と、文字列の最初からマッチしていることが条件となるmatch関数などを組み合わせることでfullmatch関数と同じようなことが実現できます。
re.match(
pattern=r'\d{2,3}-\d{4}-\d{4}\Z', string='090-1234-5678')
<re.Match object; span=(0, 13), match='090-1234-5678'>
match = re.match(
pattern=r'\d{2,3}-\d{4}-\d{4}\Z', string='猫犬090-1234-5678犬猫')
print(match)
None
マッチした文字列の位置を扱う
文字列のマッチしている箇所を扱いたいケースでは、Matchオブジェクトのspanメソッドを使うことで対応できます。
以下のようにmatchオブジェクトをJupyterなどで出力した際やprintなどした際に表示されるspanの表示と同じ値がtupleで取得できます。
値は文字列のスライスで必要なインデックスの開始と終了の値となります。
>>> string = '犬犬猫犬兎'
>>> match = re.search(pattern=r'猫犬', string=string)
>>> match
<re.Match object; span=(2, 4), match='猫犬'>
>>> match.span()
(2, 4)
文字列のスライスで、該当の開始と終了のインデックスを指定することで、該当箇所と一致していることが確認できます。
>>> string[2:4]
'猫犬'
>>> span = match.span()
>>> string[span[0]:span[1]]
'猫犬'
また、spanメソッドを経由しなくても、startメソッドやendメソッドを使うことでも文字列範囲のインデックスを取得することができます。
>>> match.start()
2
>>> match.end()
4
split関数
strクラスによる文字列のsplit関数と同様、reモジュールのsplit関数でもパターンを指定して文字列を分割して結果のリストを取得することができます。strクラスと異なり、こちらは正規表現の色々なパターンが利用できます。結果は文字列のリストで返ります。
>>> re.split(pattern=r'\d', string='猫1猫2猫3猫')
['猫', '猫', '猫', '猫']
maxsplit引数を指定すると、最大の分割数を設定することができます。省略した場合は分割できる限り全て分割されます。
設定した場合は、最大数に達した後は分割されずにそのまま残ります。
>>> re.split(pattern=r'\d', string='猫1猫2猫3猫', maxsplit=2)
['猫', '猫', '猫3猫']
sub関数
sub関数では、正規表現のパターンを使って文字列の置換を行うことができます。
英語のsubstituteという単語の略で、「変える」とか「取り替える」という意味を持ちます。
repl(replacement)の引数には置き換え後の文字列を指定します。
任意の数値(\d
)をハイフンに置換するサンプル :
>>> re.sub(pattern=r'\d', repl='-', string='猫1猫2猫3猫')
'猫-猫-猫-猫'
count引数を指定すると、置換の最大回数を指定できます。省略すると該当するパターンの箇所が全て置換されます。
>>> re.sub(pattern=r'\d', repl='-', string='猫1猫2猫3猫', count=2)
'猫-猫-猫3猫'
また、repl引数には文字列ではなく関数を指定することもできます。該当の関数には第一引数にMatchオブジェクトを受け取るようにし、返却値には置換後の文字列を指定するフォーマットが必要になります。
数値部分(\d
)をアルファベットに置換する関数をrepl引数に指定するサンプル :
※グループ設定のセクションで詳しく触れますが、Matchオブジェクトでマッチしている箇所を取得したい場合はgroup(0)
とすることで取得することができます。
def digit_to_alphabet(match):
matched_str = match.group(0)
if matched_str == '1':
return 'A'
if matched_str == '2':
return 'B'
return 'C'
re.sub(pattern=r'\d', repl=digit_to_alphabet, string='猫1猫2猫3猫')
'猫A猫B猫C猫'
subn関数
subn関数はsub関数にかなり近い挙動をします。返却値がsub関数は置換後の文字列で返るのに対して、subn関数は置換後の文字列と置換された件数がtupleで返却される点が異なります。
数値(\d
)の箇所が三箇所置換されるケース :
>>> re.subn(pattern=r'\d', repl='-', string='猫1猫2猫3猫')
('猫-猫-猫-猫', 3)
ここまでで主だった各関数を見てきました。次のセクションからはフラグ設定に関して触れていきます。
各関数のフラグ設定に関して
各正規表現の関数には、多くのケースで以下のようにflagsという引数を受け付けてくれます。
>>> re.search(pattern=r'^犬', string='猫\n犬\n兎', flags=re.MULTILINE)
<re.Match object; span=(2, 3), match='犬'>
この引数に用意されている定数を指定すると、正規表現の処理をデフォルトから切り替えることができます。
以降のセクションでは用意されている主な各フラグの定数に関して深堀りしていきます。
なお、以前のセクションでreモジュールの関数でパターンに直接文字列を指定した場合には内部で自動でコンパイルされて、且つキャッシュされると述べました。
フラグの値を変更すると、仮にパターンの文字列が一緒でも別のパターンと判定され、別々でキャッシュされます。
速度やメモリは些細なレベルの差ではありますが、大規模に大量のパターンなどを扱う際などは一応その点はご留意ください。
大文字と小文字を区別しないフラグ設定
デフォルトでは大文字小文字を区別して検索などがチェックされます。
小文字の部分のみパターンにマッチするサンプル :
>>> re.findall(pattern=r'abc', string='abcABC')
['abc']
大文字小文字区別せずに処理して欲しい場合には、re.IGNORECASE
のフラグを設定することで実現できます。
>>> re.findall(pattern=r'abc', string='abcABC', flags=re.IGNORECASE)
['abc', 'ABC']
もしくは省略形としてre.I
という定数も用意されています。どちらを使っても同じ挙動になります。
>>> re.findall(pattern=r'abc', string='abcABC', flags=re.I)
['abc', 'ABC']
複数行の文字列をパターンの対象とするフラグ設定
デフォルトでは、行の先頭を表す^
記号や行末を表す$
は、最初の行もしくは最終行の1行でしか判定されません。
行の先頭指定の^
記号がパターンに含まれているので、1件しかマッチしないケース :
>>> re.findall(pattern=r'^猫犬', string='猫犬兎\n猫犬兎\n猫犬兎')
['猫犬']
^
や$
の指定を「行ごとに」判定して欲しい場合には、フラグ設定にre.MULTILINE
の定数を指定します。
re.findall(
pattern=r'^猫犬', string='猫犬兎\n猫犬兎\n猫犬兎',
flags=re.MULTILINE)
['猫犬', '猫犬', '猫犬']
省略形として、re.M
の定数も用意されています。
re.findall(
pattern=r'^猫犬', string='猫犬兎\n猫犬兎\n猫犬兎',
flags=re.M)
['猫犬', '猫犬', '猫犬']
ドット記号で改行を含めるようにするフラグ設定
パターン内のドット記号.
は「任意の1文字」として使われます。ただし、デフォルトでは唯一改行のみ対象外となります。
ドットの指定箇所が改行なのでマッチしないケース :
>>> match = re.search(pattern=r'猫.犬.兎', string='猫\n犬\n兎')
>>> print(match)
None
ドット記号を改行を含めた条件に切り替えたい場合にはフラグ設定にre.DOTALL
を指定します。「ドットが改行も含めて全てに該当するようになる」のでDOTALLといった感じでしょうか。
>>> re.search(pattern=r'猫.犬.兎', string='猫\n犬\n兎', flags=re.DOTALL)
<re.Match object; span=(0, 5), match='猫\n犬\n兎'>
省略形として、re.S
という定数が用意されています。
>>> re.search(pattern=r'猫.犬.兎', string='猫\n犬\n兎', flags=re.S)
<re.Match object; span=(0, 5), match='猫\n犬\n兎'>
パターンの詳細情報を表示するフラグ設定
定義したパターンがどんな感じになっているのか、もしくは定義したパターンがやけに遅い・・・といったときの調査用として、フラグ設定にre.DEBUG
の定数を指定するとパターン情報を色々表示してくれるようになります。
そこまでperlに詳しくはないのですが、perlでuse re 'debug';
的な記述をした時に情報を表示してくれるもののPython版といったところのようです(参考 : How can I debug a regular expression in Python?)。
pattern = re.compile(
pattern=r'abc', flags=re.DEBUG)
LITERAL 97
LITERAL 98
LITERAL 99
0. INFO 12 0b11 3 3 (to 13)
prefix_skip 3
prefix [0x61, 0x62, 0x63] ('abc')
overlap [0, 0, 0]
13: LITERAL 0x61 ('a')
15. LITERAL 0x62 ('b')
17. LITERAL 0x63 ('c')
19. SUCCESS
なにやら色々表示されました。Python公式ドキュメントとか軽く調べてみても、以下のようなライトな説明しかないので全ては把握できていません。
コンパイル済み表現に関するデバッグ情報を表示します。相当するインラインフラグはありません。
re --- 正規表現操作
そのため、(調べるのが大変なため)そこまで深追いはしませんがある程度調べていってみます。
空行の上と下で別の表現で情報が表示されていますが、今回は上の部分のみ触れていきます。
前述のサンプルでは以下のようなLITERAL <番号>
となっています。
LITERAL 97
LITERAL 98
LITERAL 99
この97や98といった番号は、Unicodeでのコードポイント(符号点)と呼ばれます。
符号点は文字を割り当て「うる」点であり、規格によっては、実際に文字を割り当てる以外に、エスケープなどの目的の文字以外の何かが割り当てられることもある。
...
符号空間は1次元のこともあれば、多次元のこともある。その中の符号点は、座標に相当する整数列で特定される。Unicodeのように符号空間が1次元の場合は、長さ1の整数列、つまり、1つの整数となる。Unicodeの用語では「Unicodeスカラ値」と言う。
符号点 - wikipedia
Unicodeの場合各文字に割り振られたID的なものといった程度の認識でとりあえずは大丈夫だと思います。
Pythonだとビルトインのord関数で任意の文字のコードポイントが取れます。
>>> ord('a')
97
>>> ord('b')
98
>>> ord('猫')
29483
re.DEBUG
で表示される情報の上の部分は、コードのように上からどのように正規表現のパターンがチェックされているのか、ということを表しています。
つまり、
-
LITERAL 97
-> 文字が'a'かどうか -
LITERAL 98
-> 文字が'b'かどうか -
LITERAL 99
-> 文字が'c'かどうか
といったことが上から順番に処理されていくというものになります。
他のものも試してみます。「いずれかの文字にマッチする」という条件の[]
の括弧と、「a~cまでの文字範囲」を表すa-c
を使ったパターンを指定してみます。
pattern = re.compile(
pattern=r'[a-c]', flags=re.DEBUG)
IN
RANGE (97, 99)
0. INFO 8 0b100 1 1 (to 9)
in
5. RANGE 0x61 0x63 ('a'-'c')
8. FAILURE
9: IN 5 (to 15)
11. RANGE 0x61 0x63 ('a'-'c')
14. FAILURE
15: SUCCESS
上の方の情報が、
IN
RANGE (97, 99)
に変わりました。IN
のものはPythonのinの指定とか、SQLのINとか、Pandasのisin的なものとほぼ同じです。
特定の範囲のもののなかに含まれているかどうか、といったような判定ですね。
RANGE
は対象のコードポイントの範囲を示しています。
Pythonで書くと以下のようなものに近くなります。
>>> 98 in range(97, 100)
True
今度は任意の数値を表す\d
を指定してみます。
pattern = re.compile(
pattern=r'\d', flags=re.DEBUG)
IN
CATEGORY CATEGORY_DIGIT
0. INFO 7 0b100 1 1 (to 8)
in
5. CATEGORY UNI_DIGIT
7. FAILURE
8: IN 4 (to 13)
10. CATEGORY UNI_DIGIT
12. FAILURE
13: SUCCESS
今度は
IN
CATEGORY CATEGORY_DIGIT
といった内容になりました。「数値のカテゴリに含まれるかどうか」といった感じでしょうか。
続いて「任意の文字」を表すドット.
の記号を使ったパターンを指定してみます。
pattern = re.compile(
pattern=r'a.', flags=re.DEBUG)
LITERAL 97
ANY None
0. INFO 8 0b1 2 2 (to 9)
...
今度はANY
という単語が出てきました。「いずれかの文字列」といったところでしょうか。
続いて「1件以上の繰り返し」を表す+
記号を指定してみます。
pattern = re.compile(
pattern=r'a+', flags=re.DEBUG)
MAX_REPEAT 1 MAXREPEAT
LITERAL 97
...
MAX_REPEATという単語が出てきました。これは、最大量指定子(greedy quantifier)の箇所で触れたように、「可能な限り長くなるように繰り返してチェックする」といった挙動の説明になります。Pythonでいうところのfor文とかでのループに近いイメージです。
後に続く1 MAXREPEAT
の部分は、恐らく「最低1回」~「MAXREPEATという定数の値分」繰り返す、みたいな情報になります。
MAX_REPEAT
とMAXREPEAT
がなんだかそっくりで初見のとき混乱しますね・・・。
Python2系などの古いものでは1 4294967295
といったように、1~4294967295の範囲で繰り返すよ、という記述になります。Python3系になって、繰り返し回数の制限がなくなったのでMAXREPEAT
になった・・・みたいな経緯なのでしょうか?少々資料が見つからなかったのでその辺りは曖昧です。
+
の記号の代わりに「0回以上の繰り返し」となる*
を指定すると、1 MAXREPEAT
という部分が0 MAXREPEAT
となります。
pattern = re.compile(
pattern=r'a*', flags=re.DEBUG)
MAX_REPEAT 0 MAXREPEAT
LITERAL 97
...
また、最短マッチ(reluctant)としての?
記号を付与してみると、MAX_REPEAT
部分がMIN_REPEAT
となることが確認できます。
pattern = re.compile(
pattern=r'a+?', flags=re.DEBUG)
MIN_REPEAT 1 MAXREPEAT
LITERAL 97
さらに複雑なパターンとして、(後々のセクションでグループは詳しく触れますが)グループの()
の記述を入れ子にしたりしてみます。
pattern = re.compile(
pattern=r'([a-z]+([0-9]+))', flags=re.DEBUG)
SUBPATTERN 1 0 0
MAX_REPEAT 1 MAXREPEAT
IN
RANGE (97, 122)
SUBPATTERN 2 0 0
MAX_REPEAT 1 MAXREPEAT
IN
RANGE (48, 57)
...
何やらインデントが増えています。Pythonでループを入れ子にしていくと、指数的に計算時間が伸びたりするのと同様、この正規表現のように表示される結果がインデントが深くなっていると、(特に文字数が多くなってくると)正規表現の処理時間が肥大化してしまうので注意や配慮が必要になってきます。
このように、re.DEBUG
のフラグ設定を使うと、正規表現のパターンをぱっと見ただけでは分かりづらい挙動の部分をまるでコードのように構造をチェックすることができます。大量の文字列などを処理しないといけないケースなどに、パフォーマンスチューニングなどで役立ちます。
複数のフラグ設定を同時に行う場合には
前述のセクションで主なフラグ設定を見てきましたが、引数名がflag
ではなくflags
となっていることから分かる通り、フラグ設定は同時に複数設定することができます。
そういった場合には、複数のフラグの間に|
の記号を挟むことで設定することができます。
複数行対象のフラグre.MULTILINE
とドットの指定で改行も対象にするフラグre.DOTALL
を両方設定するサンプル :
re.search(
pattern=r'^猫.^犬',
string='猫\n犬\n兎',
flags=re.MULTILINE|re.DOTALL)
<re.Match object; span=(0, 3), match='猫\n犬'>
正規表現のグループについて
このセクションから正規表現のグループについて触れていきます。
グループを使うことで、マッチしたパターン内で一部だけを抽出したり、その抽出部分を正規表現の他の処理などで利用したりすることができます。
グループの書き方の基本とMatchオブジェクトのgroupメソッド
グループの指定は()
の括弧を使います。猫(.)犬
グループ設定したい箇所を()
で囲むことで設定できます。
前述の猫(.)犬
というパターンを例に挙げると、猫と犬という文字に囲まれた任意の文字のドット.
部分が()
の括弧で囲まれているので、真ん中のドット部分がグループになっています。
グループを設定していても、Matchオブジェクトの基本(spanやmatch部分)は変わりません。
match = re.search(
pattern=r'猫(.)犬',
string='狐猫兎犬狼')
match
<re.Match object; span=(1, 4), match='猫兎犬'>
グループのデータを取るにはMatchオブジェクトのgroupメソッドを使います。
引数にはグループのインデックス的な値を整数で指定します。
引数の挙動は、
- 0 -> マッチした部分の文字列全体
- 1 -> 1つ目のグループ部分
- 2 -> 2つ目のグループ部分
- ...
といった形になります。
引数を省略するか、第一引数に0を指定するとマッチした部分の文字列全体が返却されます。
>>> match.group()
'猫兎犬'
>>> match.group(0)
'猫兎犬'
引数に1以降を指定すると、1つ目以降のグループ部分の値が取得できます。
>>> match.group(1)
'兎'
複数のグループ設定をしたい場合は、()
の括弧を複数利用することで実現できます。
match = re.search(
pattern=r'(.)猫(.)犬(.)',
string='狐猫兎犬狼')
groupメソッドでの取得時には、順番に1, 2, 3...と設定すれば取得できます。
>>> match.group(1)
'狐'
>>> match.group(2)
'兎'
>>> match.group(3)
'狼'
また、引数を同時に複数指定することもできます。その場合、複数のグループの値がtupleで返却されます。
>>> match.group(1, 3)
('狐', '狼')
リストやtupleなどの変数でインデックスを指定したい場合には、引数指定時に*
の記号を付与することでPython側でリストやtupleを引数に展開してくれます。
>>> group_index_list = [1, 3]
>>> match.group(*group_index_list)
('狐', '狼')
Matchオブジェクトのgroupsメソッド
groupsメソッドでは、設定されているグループの結果の文字列のリストが取得できます。
match = re.search(
pattern=r'(.)猫(.)犬(.)',
string='狐猫兎犬狼')
match.groups()
('狐', '兎', '狼')
defaultという名前のオプションの引数を指定することで、グループ部分が空の時のデフォルト値を指定することができます。
そもそもグループの箇所でヒットしなければ、Matchオブジェクト自体が返ってこないのでは?という感じもしますが、例えば(.)?
みたいに、「あっても無くてもOK」という指定で?
記号を使った場合、グループ部分が無くてもMatchオブジェクトが返ってくるため、そういったケースでデフォルト値が反映されます。
match = re.search(
pattern=r'(.)猫(.)犬(.)?',
string='狐猫兎犬')
match.groups(default='羊')
('狐', '兎', '羊')
文字列のOR条件などを指定する際にもグループの括弧は使われる
結構前に、文字列のOR条件を指定する際には(<文字列1>|<文字列2>|<文字列N>)
といった形で表現すると紹介しました。
例えば、猫である
もしくは犬だよ
のどちらかに該当すればOKとしたい場合には、以下のような記述になります。
re.search(
pattern=r'吾輩は(猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。')
<re.Match object; span=(0, 8), match='吾輩は猫である。'>
フォーマットを見ていただくと分かる通り、グループ指定と同様に、()
の括弧が使われています。
OR条件などでの()
の括弧の利用でも、結果のMatchオブジェクトからはデフォルトではgroupメソッドなどで値が取れるようになります。
match = re.search(
pattern=r'吾輩は(猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。')
match.group(1)
'猫である'
OR条件などで()
の括弧を使う場合は、グループの処理は基本的に不要なケースが大半だと思います。一方で、デフォルトだとグループの処理が実行されるので、無駄が多くなりパフォーマンス的には遅くなります。
グループの括弧を使ったときに、グループの処理を無効化する
前のセクションで、()
の括弧はOR条件など、グループで特定箇所の文字列を抽出する以外にも使われる点と、その際にグループの処理が実行されて処理負荷になりうるという点に触れました。
グループでの文字列抽出の処理が不要な時に、無効化してパフォーマンスを上げたい場合には(
の直後に?:
の記号を付与することで対応ができます。
match = re.search(
pattern=r'吾輩は(?:猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。')
?:
を指定した状態で、Matchオブジェクトで.group(1)
といった具合にグループにアクセスしようとするとエラーで弾かれることが確認できます。
>>> match.group(1)
...
IndexError: no such group
re.DEBUG
のフラグ設定を指定して正規表現の内容を確認してみると、SUBPATTERN 1 0 0
という部分が減って必要な処理が少なくなっていることが確認できます。
re.search(
pattern=r'吾輩は(猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。',
flags=re.DEBUG)
LITERAL 21566
LITERAL 36649
LITERAL 12399
SUBPATTERN 1 0 0
BRANCH
LITERAL 29483
LITERAL 12391
LITERAL 12354
LITERAL 12427
OR
LITERAL 29356
LITERAL 12384
LITERAL 12424
LITERAL 12290
re.search(
pattern=r'吾輩は(?:猫である|犬だよ)。',
string='吾輩は猫である。名前はまだ無い。',
flags=re.DEBUG)
LITERAL 21566
LITERAL 36649
LITERAL 12399
BRANCH
LITERAL 29483
LITERAL 12391
LITERAL 12354
LITERAL 12427
OR
LITERAL 29356
LITERAL 12384
LITERAL 12424
LITERAL 12290
グループの後方参照(back reference)
パターンで設定したグループを、その後の処理で参照することを後方参照(英語でback reference)と言います。
例えば、猫-犬
といったように、ハイフン区切りの文字列で、前後を入れ替えて犬-猫
といったように置換する際などに使えます。
コードで試してみます。まずは普通にパターンを定義してMatchオブジェクトを取って試してみます。
>>> pattern = re.compile(pattern=r'(.)-(.)')
>>> match = pattern.search(string='猫-犬')
>>> match
<re.Match object; span=(0, 3), match='猫-犬'>
>>> match.group(1)
'猫'
>>> match.group(2)
'犬'
後方参照を設定する際には、\1
、\2
といったように\
の記号とグループの番号をセットで指定します。
置換の処理で使うのであれば、置換を扱うsub
関数のrepl
引数内でこの記述を使います。
今回のサンプルではハイフンの前後を入れ替えたいので、\2-\1
といったように指定します。
>>> pattern.sub(repl=r'\2-\1', string='猫-犬')
'犬-猫'
なお、この後方参照のグループは99個まで有効です。ほぼほぼのケースでそんなにグループ使うことは無い印象ですが、万一そういったケースが必要になった場合にはご注意ください。
試しに大量のグループを使ってみましょう。
pattern = re.compile(
pattern=r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)'
r'(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)')
99個目(\99
)までは普通に使えます。
pattern.sub(
repl='\99',
string='猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫')
'猫'
100個目からは、結果が無関係の@
になりました。
pattern.sub(
repl='\100',
string='猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫')
'@'
それ以降はA
, B
, ...とアルファベットが続いていきました。非サポートの境界として@
記号が設定され、それ以降はアルファベットになっています。
pattern.sub(
repl='\101',
string='猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫')
'A'
pattern.sub(
repl='\102',
string='猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫'
'猫猫猫猫猫猫猫猫猫猫')
'B'
\100
以降でなぜこの値になるのか曖昧だったのですが、コメントで@k-takata さんが教えてくださいました・・・!ありがとうございます
これは
\100
が8進数表記のエスケープシーケンスとみなされたからです。@ の文字コードは 0o100 ですので。
名前付きのグループ(named group)
皆さんがPythonでコードを書くとき、引数が多い関数とかではキーワード引数を利用することも多いと思います。
キーワード引数を使うことで、引数の数が増減したり順番が変わったりしたときなどに影響を少なくしたり、可読性が上がったりします。
正規表現のグループも同様で、数件程度なら気になりませんが数が多くなってくると保守の面や可読性の面で難が出てきます。
そういったケースの対策として、グループではキーワード引数のように名前付きのグループ(と後方参照)を利用することができます。
パターンでのフォーマットは、(?P<グループ名>該当パターン)
といった形になります。例えばleft
というグループ名で、任意の文字.
のパターンで設定したい場合には、(?P<left>.)
という形になります。
猫-犬
という文字列を犬-猫
に置換するサンプルで、名前付きのグループで対応するパターンを書くと以下のようになります。
pattern = re.compile(pattern=r'(?P<left>.)-(?P<right>.)')
後方参照で利用する際には、\g<グループ名>
という形になります。前述のパターンであれば、\g<left>
といったような書き方で設定できます。
>>> pattern.sub(repl='\g<right>-\g<left>', string='猫-犬')
'犬-猫'
余談ですが、この?P
のP、なんのPなんだ・・・?という点ですが、実はPython独自拡張のPとなっています。
1997年ごろにPython作者のGuidoさんが、Python1.5などの古いバージョンのころに対応し、その後他の言語などでも使われるように普及しました。
当時のGuidoさんがPerl関係の方々に送ったメールは https://markmail.org/message/oyezhwvefvotacc3 で読むことができます。
一部引用すると、「PerlとPython間の相互交流は良いことだ」「正規表現でなるべくPerlに表現を近づけているけれども、次のPython1.5でPython独自のシンタックスを追加するよ」といったことが書かれています。
I hope that Python and Perl can co-exist in years to come;
cross-pollination can be good for both languages.
...
However, the regex syntax has some Python-specific extensions, which all begin with (?P . Currently there are two of them:
Matchオブジェクトのexpandメソッド
正規表現で置換処理を扱うsub関数にかなり近いもので、expandメソッドというものがあります。
sub関数と似たような挙動ですが、こちらはMatchオブジェクトが持っているメソッドとなっています。
マッチ結果に置換処理などを追加するときなどで利用できます。
pattern = re.compile(pattern=r'(.)-(.)')
match = re.search(pattern=pattern, string='猫-犬')
グループや後方参照の設定は他と同様に\g<グループ番号>
といった形式で利用できます。
sub関数のrepl引数にexpandメソッドのtemplate引数が該当します。
>>> match.expand(template='\g<2>-\g<1>')
'犬-猫'
?P
表記による名前付きのグループの設定も同様に使えます。
>>> pattern = re.compile(pattern=r'(?P<left>.)-(?P<right>.)')
>>> match = re.search(pattern=pattern, string='猫-犬')
>>> match.expand(template='\g<right>-\g<left>')
'犬-猫'
正規表現でエスケープが必要な記号
ここまでで、様々な正規表現のパターンの書き方を見てきました。
色々な記号を使ってきましたが、パターンでそのままその記号を使いたい場合にはその記号にエスケープが必要になるものがそれなりに存在します。
例えば、猫(5歳)
という文字列に対してパターンを設定しようとして、()
の括弧をそのまま使うとそれはグループの処理になってしまうのでヒットしてくれません。
match = re.fullmatch(
pattern=r'.(.歳)',
string='猫(5歳)')
print(match)
None
そのような場合には、該当の記号の直前にエスケープ記号の\
を配置すると記号そのままで判定してくれるようになります。
re.fullmatch(
pattern=r'.\(.歳\)',
string='猫(5歳)')
<re.Match object; span=(0, 5), match='猫(5歳)'>
()
の括弧以外にも、以下の記号のエスケープが必要です。
\
, ^
, $
, .
, |
, ?
, *
, +
, [
, {
[
や{
の閉じ括弧の記号の]
や}
は要らないのかな?と思いましたが、開始の括弧部分がなければ(もしくは適切にエスケープされていれば)、特にエスケープしなくても問題無いようです。
re.fullmatch(
pattern=r'.].',
string='猫]犬')
<re.Match object; span=(0, 3), match='猫]犬'>
escape関数
前述の記号をエスケープするという処理に関して、エスケープ記号(\
)を直接文字列に付与する以外にも、一括でエスケープ後の文字列を作ってくれるescape関数が用意されています。
()
の括弧を使ったパターンで、escape関数を通しているためマッチするケース :
>>> escaped_str = re.escape(pattern='猫(5歳)')
>>> escaped_str
'猫\\(5歳\\)'
re.fullmatch(
pattern=escaped_str,
string='猫(5歳)')
<re.Match object; span=(0, 5), match='猫(5歳)'>
ここまででグループやらエスケープやらに触れてきました。
次のセクションからは、正規表現の先読みと後読みについて色々触れていきます。最後のトピックです。
幅0のアサーションと、正規表現の先読みと後読み
今まで、行の先頭を表す^
や行末を表す$
、文字列の先頭を表す\A
や文字列の最後を表す\Z
といった表現に触れてきました。
これらは位置の指定などに使われますが、マッチ後にそれらの内容は含まれません。
たとえば、^猫
というパターンを設定した場合、マッチ後の結果には猫
だけが残ります。
re.search(
pattern=r'^猫',
string='猫')
<re.Match object; span=(0, 1), match='猫'>
こういった「マッチ結果に1つも文字が追加されない(文字が消費されない・幅が無い)」表現は、幅0のアサーション(英語でzero-width assertions)と呼ばれます。
これから触れる正規表現の先読み(look ahead)と後読み(look behind)と呼ばれる表現も、それらと同じ幅0のアサーションとなります。
^
や$
記号などが位置が固定されているのに対して、先読みと後読みで、前後で特定の文字列が存在することをマッチ条件としつつ、マッチ結果にはそれらを含まない(幅0のアサーション)形で処理をすることができます。
先読みと後読みには以下の4種類があります。
- 肯定先読み
- 否定先読み
- 肯定後読み
- 否定後読み
それぞれ順番に見ていきます。
なお、look aheadとかの英語もそうですが、肯定先読みなどの日本語訳もあまり直観的とは個人的にはあまり感じられません。
そのため、単語そのものよりもそれぞれが「どんな挙動をするのか」「どんな書き方が必要なのか」といったところを中心に見ていただくといいかもしれません。
肯定先読み
英語だとPositive look aheadです。
書き方は(?=パターン)
となります。意味としては「直後に指定パターンが存在する」という条件になります。
たとえば、5歳の猫
という文字列があって、歳の部分だけをマッチ結果に含めたい場合には以下のような肯定先読みを使ったパターンで対応ができます(2桁以降の歳とかは一旦置いておきます)。
re.search(
pattern=r'\d歳(?=の猫)', string='5歳の猫')
<re.Match object; span=(0, 2), match='5歳'>
の猫
部分が肯定先読みで指定されているので、結果のMatchオブジェクトには含まれていないことが確認できます。
また、肯定先読みを使っているので猫以外、例えば5歳の犬
といった文字列はマッチしません。
match = re.search(
pattern=r'\d歳(?=の猫)', string='5歳の犬')
print(match)
None
否定先読み
否定先読み(Negative look ahead)は、肯定先読みと同様に直後のパターンを対象とします。
ただし、「条件に該当しない」ことが条件となるので、「直後に指定パターンが存在しない」という表現になります。
書き方は(?!パターン)
という形で書きます。
肯定先読みのサンプルでヒットしなかった文字列(5歳の犬
)でヒットするようになります。
re.search(
pattern=r'\d歳(?!の猫)', string='5歳の犬')
<re.Match object; span=(0, 2), match='5歳'>
逆に5歳の猫
といった文字列ではヒットしなくなります。
match = re.search(
pattern=r'\d歳(?!の猫)', string='5歳の猫')
print(match)
None
肯定後読み
肯定後読みは、「直前に指定パターンが存在する」という条件になります。先読みと比べてチェックされる位置が変わります。
書き方は(?<=パターン)
となります。
以下のように猫は
という文字列部分に肯定後読みを設定することで、後に続く5歳
という文字列部分のみ抽出ができています。
re.search(
pattern=r'(?<=猫は)\d歳', string='猫は5歳')
<re.Match object; span=(2, 4), match='5歳'>
否定後読み
肯定後読みと否定先読みにすでに触れているので説明はほとんど要らない気もしますが、否定後読みは「直前に指定のパターンが存在しない」という条件になります。
書き方は(?<!パターン>)
という形で書きます。
re.search(
pattern=r'(?<!猫は)\d歳', string='犬は5歳')
<re.Match object; span=(2, 4), match='5歳'>
終わりに
結構記事が長くなった気がしますが、これで主だったPythonの正規表現はぼちぼちカバーできた気がします。お疲れさまでした!
ここまで読んでくださった皆様、LGTM押しておいていただいてもいいのですよ・・・?
他にもPythonなどを中心に色々記事を書いています。そちらもどうぞ!
今までに投稿した主な記事たち
参考文献・参考サイトまとめ
- Mastering Python Regular Expressions
- エンジニアの言う「完全に理解した」「なにもわからない」「チョットデキル」って本当はこういう意味?「わかる」の声多数
- A logical calculus of the ideas immanent in nervous activity
- Representation of Events in Nerve Nets and Finite Automata
- ケン・トンプソン - Wikipedia
- Pythonでエスケープシーケンスを無視(無効化)するraw文字列
- Pythonの正規表現で漢字・ひらがな・カタカナ・英数字を判定・抽出・カウント
- 文字化け現象「豆腐化」とは?
- 平仮名 (Unicodeのブロック) - Wikipedia
- CJK統合漢字
- CJK統合漢字 - Wikipedia
- [Python] 正規表現の表記方法のまとめ(reモジュール)
- 数量詞 - Wikipedia
- 絶対最大量指定子(possessive quantifier)について
- 単語境界(Word boundary): \b
- 電話番号の桁数は10か11桁。9桁はもはや無い
- Pythonの正規表現モジュールreの使い方(match、search、subなど)
- substituteとは
- How can I debug a regular expression in Python?
- re --- 正規表現操作
- 符号点 - wikipedia
- PythonでUnicodeコードポイントと文字を相互変換(chr, ord, \x, \u, \U)
- 後方参照
- Claiming (?P...) regex syntax extensions
- こんどこそわかる(肯|否)定(先|後)読み