173
259

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonでやさしくしっかり学ぶ正規表現

Last updated at Posted at 2020-03-23

普段正規表現が必要になるケースがそれなりに発生しているものの、体系立てて勉強したことがなかったので整理・まとめておきます。言語は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 さんから、コメントでご共有いただきました・・!ありがとうございます:bow:(詳細はコメント欄をご確認ください)

使うもの

  • 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までが該当します。

片仮名 (Unicodeのブロック)

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_REPEATMAXREPEATがなんだかそっくりで初見のとき混乱しますね・・・。
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 さんが教えてくださいました・・・!ありがとうございます:bow:

これは \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押しておいていただいてもいいのですよ・・・?:innocent:


他にもPythonなどを中心に色々記事を書いています。そちらもどうぞ!
今までに投稿した主な記事たち

参考文献・参考サイトまとめ

173
259
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
173
259

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?