Python 正規表現

正規表現は、テキスト パターンを照合するための強力な言語です。このページでは、正規表現自体について、Python の演習で十分かつ基本的な概要を説明し、Python での正規表現の仕組みを紹介します。Python の「re」モジュールは正規表現をサポートしています。

Python で正規表現検索は通常次のように記述します。

match = re.search(pat, str)

re.search() メソッドは、正規表現パターンと文字列を受け取り、文字列内でそのパターンを検索します。検索が成功した場合、search() は一致オブジェクトを返し、それ以外の場合は None を返します。そのため、通常は検索の直後に if 文が続き、検索が成功したかどうかテストします。次の例では、「word:」というパターンの後に 3 文字の単語が続きます(詳細は後述します)。

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

コード match = re.search(pat, str) は、検索結果を「match」という名前の変数に格納します。次に if-statement は一致をテストします。もし true の場合、検索が成功し、match.group() が一致するテキスト('word:cat' など)です。false の場合(より具体的に指定すると None)、検索は成功せず、一致するテキストはありません。

パターン文字列の先頭にある「r」は、Python の「raw」文字列で、バックスラッシュを変更せずに渡します。これは正規表現に非常に便利です(Java ではこの機能が不適切です)。「r」を含むパターン文字列を記述することを習慣づけることをおすすめします。

基本的なパターン

正規表現は、固定文字だけでなくパターンも指定できる点で優れています。1 つの文字を照合する最も基本的なパターンは次のとおりです。

  • a、X、9、< -- 通常の文字は完全に一致します。特別な意味を持つために自身と一致しないメタ文字は、: です。^ $ * + ? { [ ] \ | ( )(詳細は下記)
  • 。(ピリオド)-- 改行「\n」以外の任意の 1 文字に一致します。
  • \w --(小文字の w)は「単語」文字に一致します。文字、数字、アンダーバー [a-zA-Z0-9_]。「word」はこれの記憶表記ですが、1 つの単語の char にのみ一致し、単語全体には一致しません。\W(大文字の W)は、単語以外の文字に一致します。
  • \b -- 単語と単語以外の境界
  • \s --(小文字の s)は 1 つの空白文字(スペース、改行、リターン、タブ、フォーム [ \n\r\t\f])に一致します。\S(大文字の S)は、空白以外の文字に一致します。
  • \t、\n、\r -- タブ、改行、戻り値
  • \d -- 10 進数字 [0 ~ 9](一部の古い正規表現ユーティリティは \d をサポートしていませんが、\w と \s はすべてサポートされています)
  • ^ = start, $ = end -- 文字列の先頭または末尾に一致
  • \ -- 文字の「特殊性」を禁止します。たとえば、ピリオドを一致させるには \. を、スラッシュを一致させるには \\ を使用します。文字に「@」などの特別な意味があるかどうか不明な場合は、文字の前にスラッシュ(\@)を付けてみてください。\c などの有効なエスケープ シーケンスでない場合、Python プログラムはエラーで停止します。

基本的な例

ジョーク: 目が 3 つあるブタを何と呼ぶの?ピイグ!

文字列内のパターンを正規表現検索する基本的なルールは次のとおりです。

  • 検索は文字列の先頭から最後まで進み、最初に見つかった一致で停止します。
  • すべてのパターンが一致する必要がありますが、文字列のすべてに一致しません。
  • match = re.search(pat, str) が成功した場合、match は None ではなく、特に match.group() は一致するテキスト
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

繰り返し

+ と * を使用してパターンの繰り返しを指定すると、より面白くなります

  • + -- パターンの左側で 1 回以上出現。例: 'i+' = 1 つ以上の i
  • * -- パターンの左側で 0 回以上出現
  • ? -- パターンの左辺に 0 個または 1 個出現する箇所に一致

左端と最大

まず、パターンの左端に一致するものが検索され、次にその文字列をできるだけ多く使用しようとします。つまり、+ と * は可能な限り多く使われていきます(+ と * は「欲張り」と言われます)。

繰り返しの例

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

メールの例

文字列「xyz alice-b@google.com purple monkey」からメールアドレスを検索するとします。ここでは、正規表現の機能の例をいくつか紹介します。たとえば、r'\w+@\w+' というパターンを使用します。

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

この場合、\w はアドレスの「-」や「.」と一致しないため、メールアドレス全体が検索されません。この問題は、後述の正規表現機能を使用して修正します。

角括弧

角かっこは一連の文字を示すために使用できます。たとえば、[abc] は 'a'、'b'、'c' と一致します。コード \w や \s なども角括弧で囲みますが、ドット(.)はリテラルのドットのみを意味します。メールの問題では、角かっこを使用すると、@ の周りに r'[\w.-]+@[\w.-]+' のパターンで @ を囲む文字群に「.」と「-」を簡単に追加できます。これにより、メールアドレス全体を取得できます。

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(角かっこの機能の追加)ダッシュを使用して範囲を示すこともできます。この場合、[a-z] はすべて小文字に一致します。範囲を指定せずにダッシュを使用するには、ダッシュを最後に付けます([abc-] など)。[^ab] は角かっこの先頭にあるアップハット(^)を反転します。したがって、[^ab] は「a」と「b」以外の文字を意味します。

グループ抽出

正規表現の「グループ」機能を使用すると、一致するテキストの一部を取り出すことができます。メールの問題で、ユーザー名とホストを別々に抽出するとします。そのためには、r'([\w.-]+)@([\w.-]+)' のようにパターン内のユーザー名とホストをかっこ( )で囲みます。この場合、かっこは一致対象を変更せず、一致テキスト内に論理「グループ」を設定します。検索が成功した場合、match.group(1) は 1 番目の左かっこに対応する一致テキストで、match.group(2) は 2 番目の左かっこに対応するテキストです。プレーンな match.group() は引き続き、通常どおり一致テキスト全体です。

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

正規表現を使用する一般的なワークフローは、探している部分のパターンを記述し、括弧のグループを追加することで必要な部分を抽出することです。

探す

findall() はおそらく、re モジュールで最も強力な関数の一つでしょう。上記では、re.search() を使用してパターンの最初の一致を見つけました。findall() はすべての一致を検出し、文字列のリストとして返します。各文字列は 1 つの一致を表します。
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

findall ファイル付き

ファイルの場合は、ファイルの行を反復するループを記述するのが習慣になっているかもしれません。その場合は、各行で findall() を呼び出すことができます。代わりに、findall() に反復処理を任せることで、はるかに効率的です。ファイルのテキスト全体を findall() にフィードし、1 つのステップですべてのマッチのリストを返すようにするだけです(f.read() はファイルのテキスト全体を 1 つの文字列として返します)。

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

検索、グループ

かっこ( )グループ メカニズムは findall() と組み合わせることができます。パターンに 2 つ以上のかっこグループが含まれている場合、findall() は文字列リストを返す代わりに、*タプル* リストを返します。各タプルはパターンの 1 つの一致を表し、タプルの内部には group(1), group(2) .. データがあります。たとえば、2 つのかっこのグループがメール パターンに追加された場合、findall() は、長さ 2 ごとにユーザー名とホストを含むタプルのリストを返します(例: ('alice', 'google.com')。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

タプルのリストを取得したら、それをループして、タプルごとに計算を行うことができます。パターンにかっこがない場合、findall() は前の例と同様に、検出された文字列のリストを返します。パターンにかっこのセットが 1 つ含まれている場合、findall() は 1 つのグループに対応する文字列のリストを返します。(わかりにくいオプション機能: パターンにかっこ( )のグループがあるものの、抽出したくない)場合があります。その場合、丸かっこは先頭に ?: を付けて記述します(例: (?:))。その左かっこはグループ結果としてカウントされません。)

RE ワークフローとデバッグ

正規表現パターンは、わずか数文字に多くの意味が詰め込まれていますが、非常に高密度であるため、パターンのデバッグに多くの時間を費やすことができます。ランタイムを設定して、パターンを実行し、一致するものを簡単に出力できるようにします。たとえば、小さなテストテキストで実行し、findall() の結果を出力します。パターンが一致しない場合は、パターンを弱めて、パターンの一部を削除して、一致するものが多すぎます。一致するものがない場合は、明確に確認するものがないため、何も進行できません。一致しすぎたら、徐々に絞り込んで、目的とする範囲にたどり着きます。

オプション

re 関数は、パターン一致の動作を変更するオプションを受け取ります。オプション フラグは、search() や findall() などに追加引数として追加されます(re.search(pat, str, re.IGNORECASE など))。

  • IGNORECASE -- マッチングの際に大文字と小文字の違いを無視するため、「a」は「a」と「A」の両方に一致します。
  • DOTALL -- ドット(.)を改行と一致させます。通常は、改行以外と一致します。「.*」はすべてに一致しますが、デフォルトでは行末を超えることはありません。なお、\s(空白)には改行が含まれるため、改行を含む可能性のある空白の羅列と一致させたい場合は、\s* を使用してください。
  • 複数行 -- 多数の行で構成される文字列の中で、^ と $ を各行の先頭と末尾に一致させることができます。通常、「^/$」は文字列全体の先頭と末尾に一致します。

貪欲または非食欲(省略可)

このセクションでは省略可能なセクションでは、演習では不要な、より高度な正規表現の手法について説明します。

<b>foo</b> と <i>so on</i> というタグを含むテキストがあるとします。

各タグをパターン「(<.*>)」に一致させようとしているとします。最初に何に一致しますか?

結果は少し驚きでしたが、.* の欲張りな側面により、'<b>foo</b> と <i>so on</i> の全体が 1 つの大きな一致として一致します。問題は、.* が最初の > で止まらず(つまり「欲張り」である)ようにできるだけ長くなってしまうことです。

正規表現の拡張機能として、?末尾に .*? や .+? などの文字列を追加して、非欲張りな文字列に変更します。今ではできる限り早くやめるようになりました。したがって、パターン '(<.*?>)' は、最初の一致として <b>'、2 番目の一致として '</b>' のみを取得します。これも <..> の各ペアを順番に取得します。このスタイルは通常、.*? の直後に具体的なマーカー(この場合は >)を続けて使用します。それに .*? 実行が強制的に拡張されます。

*? 拡張機能は Perl で開発されており、Perl の拡張機能を含む正規表現は Perl 互換正規表現(pcre)と呼ばれています。Python は pcre をサポートしています。多くのコマンドライン ユーティリティなどには、pcre パターンを受け入れるフラグがあります。

「X で停止する以外のすべての文字」という考え方をコーディングするために広く使用されている古い手法では、角かっこスタイルが使用されます。上記ではパターンを記述できますが、すべての文字を取得するには .* の代わりに [^>]* を使用します。これは > 以外のすべての文字をスキップします(先頭の ^ は角かっこのセットを「反転」し、かっこ内にないすべての文字に一致します)。

置換(省略可)

re.sub(pat, replacement, str) 関数は、指定された文字列に含まれるパターンのインスタンスをすべて検索し、置換します。置換文字列には、元の一致テキストの group(1)、group(2) などのテキストを参照する '\1'、'\2' などを含めることができます。

次の例では、すべてのメールアドレスを検索し、ユーザー(\1)を保持し、yo-yo-dyne.com をホストとして変更しています。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

演習

正規表現を練習するには、赤ちゃんの名前のエクササイズをご覧ください。