【初心者向け】Python 正規表現の使い方


【初心者向け】Python 正規表現(reモジュール)の使い方を徹底解説!テキスト処理の強力なツールをマスターしよう

はじめに:正規表現とは何か、なぜPythonで使うのか?

あなたは普段、パソコンで文章を読んだり書いたり、インターネットで情報を検索したりしていますよね。その裏側では、たくさんの「テキスト」が処理されています。特定のキーワードを探したり、決められた形式の文字列を抽出したり、あるいは不要な部分を削除したり…これらのテキスト処理は、プログラミングにおいて非常に重要なタスクです。

Pythonはテキスト処理が得意な言語ですが、さらに強力な武器として「正規表現(Regular Expression)」があります。正規表現を使うと、複雑なパターンを持つ文字列を驚くほどシンプルに、そして効率的に見つけたり、置き換えたり、分割したりすることができます。

例えば:
* メールアドレスの形式が正しいかチェックしたい。
* Webページから特定の日付形式(例: YYYY-MM-DD)の情報を全て抜き出したい。
* ログファイルの中から、エラーメッセージが含まれる行だけを見つけたい。
* 文章中の特定の単語を別の単語にまとめて置き換えたい。
* CSVファイルのような、特定の区切り文字で区切られたデータを分割したい。

このような作業を、もし正規表現を使わずに、文字列のメソッド(find, replace, splitなど)やループ、条件分岐だけで行おうとすると、コードは非常に複雑になり、メンテナンスも難しくなります。しかし、正規表現をマスターすれば、これらのタスクを簡潔かつ強力に記述できます。

Pythonでは、標準ライブラリであるreモジュールを使って正規表現を扱います。このモジュールには、正規表現を使った検索、置換、分割など、様々な機能が用意されています。

この記事では、Pythonにおける正規表現の基本的な考え方から、reモジュールの使い方、そして少し応用的なテクニックまでを、初心者の方でも理解できるように、たくさんの例を交えながら丁寧に解説していきます。約5000語というボリュームで、正規表現の基礎からしっかりと身につけられる内容を目指します。

さあ、Pythonと正規表現の世界に飛び込み、あなたのテキスト処理能力を格段に向上させましょう!

1. 正規表現の基本:パターンマッチングの考え方

正規表現は、文字列のパターンを記述するための特別な記法です。そして、そのパターンを使って、対象となる文字列の中から、そのパターンに一致する部分(マッチ)を探し出すのが「パターンマッチング」です。

例えるなら、正規表現は「探し物」の具体的な特徴を指定するリスト、対象文字列は「探し物をする場所」です。正規表現エンジン(Pythonのreモジュールなど)は、そのリストに従って場所をくまなく探し、一致する部分を見つけ出します。

1.1. リテラル文字:普通の文字にマッチさせる

正規表現の最も基本的な要素は「リテラル文字」です。これは、単にその文字自身にマッチすることを意味します。

例えば、正規表現パターン "apple" は、文字列 "I like apple pie." の中の "apple" という部分にマッチします。

“`python
import re

text = “I like apple pie.”
pattern = “apple”

re.search()関数を使って、文字列の中にパターンがあるか探す

マッチが見つかればMatchオブジェクト、見つからなければNoneを返す

match = re.search(pattern, text)

if match:
print(f”パターン ‘{pattern}’ が文字列 ‘{text}’ の中に見つかりました。”)
print(f”マッチした部分: {match.group()}”) # マッチした文字列を取得
print(f”開始位置: {match.start()}”) # マッチの開始インデックス
print(f”終了位置: {match.end()}”) # マッチの終了インデックス (終了位置+1)
print(f”範囲: {match.span()}”) # マッチの開始/終了インデックスのタプル
else:
print(f”パターン ‘{pattern}’ は文字列 ‘{text}’ の中に見つかりませんでした。”)

別の例

text2 = “banana is yellow.”
pattern2 = “apple”
match2 = re.search(pattern2, text2)

if match2:
print(f”パターン ‘{pattern2}’ が文字列 ‘{text2}’ の中に見つかりました。”)
else:
print(f”パターン ‘{pattern2}’ は文字列 ‘{text2}’ の中に見つかりませんでした。”)
“`

この例では、re.search()関数を使っています。これは、文字列のどこかにパターンにマッチする部分があるかを検索する関数です。マッチが見つかると、そのマッチに関する情報を持つMatchオブジェクトを返します。Matchオブジェクトのgroup()メソッドで、実際にマッチした文字列を取り出すことができます。

1.2. エスケープ文字:特殊な意味を持つ文字にマッチさせる

正規表現には、後述する.*+などのように、特別な意味を持つ「メタ文字」と呼ばれる文字があります。もし、これらのメタ文字自身にマッチさせたい場合は、その直前にバックスラッシュ \ を付けてエスケープする必要があります。

例えば、文字列中のドット.を探したいのに、パターンをそのまま.とすると、これは「任意の一文字」という意味になってしまいます。文字列中のリテラルなドット.にマッチさせるには、パターンを"\."とします。

“`python
import re

特別な意味を持つドット’.’を含む文字列

text = “File name is data.txt”

パターンに”.”を含めると「任意の一文字」になってしまう

pattern_wrong = “data.txt” # この’.’は任意の一文字にマッチ

正しくリテラルなドット’.’にマッチさせるにはエスケープする

pattern_correct = “data.txt” # この’.’はリテラルなドットにマッチ

match_wrong = re.search(pattern_wrong, text)
match_correct = re.search(pattern_correct, text)

print(f”‘{pattern_wrong}’ で検索: {match_wrong.group() if match_wrong else ‘マッチなし’}”)
print(f”‘{pattern_correct}’ で検索: {match_correct.group() if match_correct else ‘マッチなし’}”)

この例では、’data.txt’も’dataXtxt’のような文字列にもパターン_wrongはマッチします。

pattern_correctは正確に’data.txt’にのみマッチします。

“`

正規表現で特別な意味を持つメタ文字には、以下のものがあります。

. ^ $ * + ? { } [ ] \ | ( )

これらの文字にリテラルにマッチさせたい場合は、直前に \ を付けます。

注意: Pythonの文字列リテラル自身もバックスラッシュをエスケープ文字として使います(例: \nは改行)。そのため、正規表現パターン文字列中でバックスラッシュを使いたい場合、Pythonの文字列として記述する際にさらにエスケープが必要になることがあります(例: 正規表現パターンで \ を意味するには、Python文字列では "\\" と書く)。しかし、これは非常に紛らわしいです。この問題を避けるために、正規表現パターンを記述する際には、必ず「生文字列(Raw String)」を使うことを強く推奨します。生文字列は文字列の前に r を付けます(例: r"data\.txt")。生文字列内では、バックスラッシュは特別な意味を持たず、文字通りバックスラッシュとして扱われます。正規表現パターンを生文字列で書くことで、Pythonの文字列エスケープを気にすることなく、正規表現本来のバックスラッシュの意味だけに集中できます。

“`python
import re

生文字列を使わない場合(推奨されない)

バックスラッシュを表現するために二重にエスケープが必要になることがある

pattern_complicated = “\section” # 正規表現としては “\section” と等価

生文字列を使う場合(推奨)

正規表現そのままの記述でOK

pattern_simple = r”\section” # 正規表現として “\section” と等価

text = “This is the \section command.”

match_complicated = re.search(pattern_complicated, text)
match_simple = re.search(pattern_simple, text)

print(f”二重エスケープパターンで検索: {match_complicated.group() if match_complicated else ‘マッチなし’}”)
print(f”生文字列パターンで検索: {match_simple.group() if match_simple else ‘マッチなし’}”)
“`

今後、正規表現パターンを示すコード例では、特に理由がない限り生文字列(r"...")を使用します。

2. メタ文字(特殊文字)の詳細

ここからが正規表現の本番です。特定の文字そのものにマッチさせるだけでなく、「任意の文字」「繰り返し」「選択」など、パターンを柔軟に記述するために使用されるのが「メタ文字」です。

2.1. . (ドット):任意の一文字

ドット.は、改行文字(\n)を除く任意の一文字にマッチします。

“`python
import re

text = “cat hat bat sat”
pattern = r”.at” # 任意の一文字 + ‘at’

re.findall()関数: マッチする全ての部分をリストで取得

matches = re.findall(pattern, text)
print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘cat’, ‘hat’, ‘bat’, ‘sat’]

text2 = “flat\nmat”
pattern2 = r”.at” # ‘.’ は通常改行にマッチしない

matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ (改行あり) で検索: {matches2}”) # 出力: [‘flat’, ‘mat’] – ‘mat’は改行の後に来るため別々のマッチと認識される

re.DOTALLフラグを使うと’.’が改行にもマッチするようになる(後述)

“`

2.2. * (アスタリスク):直前の要素の0回以上の繰り返し

アスタリスク*は、その直前にある正規表現要素(文字、文字クラス、グループなど)が0回以上繰り返される場合にマッチします。

“`python
import re

text = “color colour cooor”
pattern = r”colou*r” # ‘u’が0回以上繰り返される

matches = re.findall(pattern, text)
print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘color’, ‘colour’, ‘cooor’]

‘color’では’u’が0回、’colour’では’u’が1回、’cooor’では’o’に*がついていないので’o’が1回、’u’が2回と解釈される

パターンは ‘c’ + ‘o’ + ‘l’ + ‘o’ + ‘u’の0回以上繰り返し + ‘r’

なので ‘cooor’ は ‘o’ にはマッチしません。正しくは ‘colouur’ となるべきです。

修正例: ‘o’ の繰り返しにマッチさせたい場合

text = “coool”
pattern = r”co+l” # ‘o’ が1回以上の繰り返し
match = re.search(pattern, text)
print(f”‘{pattern}’ で検索: {match.group() if match else ‘マッチなし’}”) # 出力: ‘coool’

‘*’ の例に戻る

text = “caat cat cccat”
pattern = r”ca*t” # ‘a’ が0回以上の繰り返し

matches = re.findall(pattern, text)
print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘caat’, ‘cat’, ‘ct’] – ‘ct’は’a’が0回の場合

text2 = “apple appple apppple”
pattern2 = r”ap*le” # ‘p’ が0回以上の繰り返し

matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘aple’, ‘apple’, ‘appple’, ‘apppple’]

‘aple’ (‘p’が0回) にもマッチする点に注意

“`

2.3. + (プラス):直前の要素の1回以上の繰り返し

プラス+は、その直前にある要素が1回以上繰り返される場合にマッチします。*と似ていますが、0回の場合はマッチしない点が異なります。

“`python
import re

text = “cooor color coor”
pattern = r”co+r” # ‘o’ が1回以上の繰り返し

matches = re.findall(pattern, text)
print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘cooor’, ‘coor’] – ‘color’は’o’の後に’l’が来るのでマッチしない

text2 = “aple apple appple”
pattern2 = r”ap+le” # ‘p’ が1回以上の繰り返し

matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘apple’, ‘appple’] – ‘aple’ (‘p’が0回) にはマッチしない
“`

2.4. ? (疑問符):直前の要素の0回または1回の繰り返し(オプション)

疑問符?は、その直前にある要素が0回または1回出現する場合にマッチします。これは、特定の文字やパターンが「あってもなくてもよい」という状況で便利です。

“`python
import re

text = “color colour”
pattern = r”colou?r” # ‘u’ が0回または1回

matches = re.findall(pattern, text)
print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘color’, ‘colour’]

text2 = “Nov December”
pattern2 = r”Nov(ember)?” # ‘ember’ が0回または1回 (‘()’はグループ化 – 後述)

matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘Nov’, ‘November’] – findall()はグループ指定があるとグループの中身だけを返すことに注意

グループ全体のマッチを見つけるには search() を使うか、グループ化しないか、非キャプチャグループ(?:…)を使う

例: 非キャプチャグループを使う

pattern2_non_capturing = r”Nov(?:ember)?”
matches2_nc = re.findall(pattern2_non_capturing, text2)
print(f”‘{pattern2_non_capturing}’ で検索: {matches2_nc}”) # 出力: [‘Nov’, ‘November’]
“`

2.5. {} (波かっこ):直前の要素の出現回数を指定

波かっこ{}を使うと、その直前の要素が出現する特定の回数を指定できます。

  • {n}: ちょうどn回
  • {n,}: n回以上
  • {n,m}: n回以上m回以下

“`python
import re

text = “aa a aaa aaaa aaaaa”

pattern1 = r”a{3}” # ‘a’ がちょうど3回
matches1 = re.findall(pattern1, text)
print(f”‘{pattern1}’ で検索: {matches1}”) # 出力: [‘aaa’, ‘aaa’] (aaaaaの中から最初のaaa、残りのaaからはマッチしない)

pattern2 = r”a{2,}” # ‘a’ が2回以上
matches2 = re.findall(pattern2, text)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘aa’, ‘aaa’, ‘aaaa’, ‘aaaaa’]

pattern3 = r”a{2,4}” # ‘a’ が2回以上4回以下
matches3 = re.findall(pattern3, text)
print(f”‘{pattern3}’ で検索: {matches3}”) # 出力: [‘aa’, ‘aaa’, ‘aaaa’, ‘aaaa’] (aaaaaからは最初のaaaaがマッチ)

text2 = “123 1234 12345 123456″
pattern4 = r”\d{4}” # 数字がちょうど4回 (\dは数字にマッチ – 後述)
matches4 = re.findall(pattern4, text2)
print(f”‘{pattern4}’ で検索: {matches4}”) # 出力: [‘1234’, ‘2345’, ‘3456’]
“`

2.6. ^ (キャレット):行の先頭

キャレット^は、対象文字列の先頭にマッチします。re.MULTILINEフラグを使うと、文字列中の各行の先頭にもマッチするようになります(後述)。

“`python
import re

text = “First line\nSecond line\nThird line”

pattern1 = r”^First” # 文字列の先頭に’First’があるか
match1 = re.search(pattern1, text)
print(f”‘{pattern1}’ で検索: {match1.group() if match1 else ‘マッチなし’}”) # 出力: ‘First’

pattern2 = r”^Second” # 文字列の先頭に’Second’があるか
match2 = re.search(pattern2, text)
print(f”‘{pattern2}’ で検索: {match2.group() if match2 else ‘マッチなし’}”) # 出力: マッチなし

re.MULTILINE フラグを使う

pattern3 = r”^Second” # 行の先頭に’Second’があるか
match3 = re.search(pattern3, text, re.MULTILINE)
print(f”‘{pattern3}’ (MULTILINE) で検索: {match3.group() if match3 else ‘マッチなし’}”) # 出力: ‘Second’

pattern4 = r”^Third” # 行の先頭に’Third’があるか
matches4 = re.findall(pattern4, text, re.MULTILINE)
print(f”‘{pattern4}’ (MULTILINE) で検索: {matches4}”) # 出力: [‘Third’]
“`

2.7. $ (ドル):行の末尾

ドル$は、対象文字列の末尾にマッチします。re.MULTILINEフラグを使うと、文字列中の各行の末尾にもマッチするようになります(後述)。改行文字\nの直前(および文字列の末尾)にもマッチします。

“`python
import re

text = “First line\nSecond line\nThird line”

pattern1 = r”line$” # 文字列の末尾が’line’で終わるか
match1 = re.search(pattern1, text)
print(f”‘{pattern1}’ で検索: {match1.group() if match1 else ‘マッチなし’}”) # 出力: ‘line’ (‘Third line’の’line’にマッチ)

pattern2 = r”line$” # 行の末尾が’line’で終わるか
matches2 = re.findall(pattern2, text, re.MULTILINE)
print(f”‘{pattern2}’ (MULTILINE) で検索: {matches2}”) # 出力: [‘line’, ‘line’, ‘line’]

text2 = “Ends with line.\nAnother line”
pattern3 = r”.$” # 文字列の末尾が’.’で終わるか
match3 = re.search(pattern3, text2)
print(f”‘{pattern3}’ で検索: {match3.group() if match3 else ‘マッチなし’}”) # 出力: ‘.’

pattern4 = r”line\n” # ‘line’の直後に改行がある箇所
match4 = re.search(pattern4, text2)
print(f”‘{pattern4}’ で検索: {match4.group() if match4 else ‘マッチなし’}”) # 出力: ‘line\n’

pattern5 = r”line$” # MULTILINEフラグなしでも、最後の行の’line’の直後(文字列の末尾)にマッチ
match5 = re.search(pattern5, text2)
print(f”‘{pattern5}’ (末尾) で検索: {match5.group() if match5 else ‘マッチなし’}”) # 出力: ‘line’
“`

2.8. [] (角かっこ):文字クラス – いずれか一文字

角かっこ[]は「文字クラス」を定義します。角かっこ内に記述された文字のいずれか一文字にマッチします。

“`python
import re

text = “apple banana cherry”

pattern1 = r”[abc]ana” # ‘a’または’b’または’c’ の後に ‘ana’
matches1 = re.findall(pattern1, text)
print(f”‘{pattern1}’ で検索: {matches1}”) # 出力: [‘banana’]

pattern2 = r”c[ha]t” # ‘c’ の後に ‘h’または’a’ の後に ‘t’
text2 = “cat cht cut”
matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘cat’, ‘cht’]
“`

文字クラス内の特殊な使い方:

  • 範囲指定: ハイフン-を使うと、文字の範囲を指定できます。

    • [a-z]: 全ての小文字アルファベット
    • [A-Z]: 全ての大文字アルファベット
    • [0-9]: 全ての数字
    • [a-zA-Z]: 全てのアルファベット(大文字・小文字問わず)
    • [a-zA-Z0-9]: 全てのアルファベットと数字
    • [0-9a-fA-F]: 16進数の文字

    “`python
    import re

    text = “ID: abc123, Code: XYZ789″
    pattern = r”[a-zA-Z]{3}[0-9]{3}” # アルファベット3文字の後に数字3文字

    matches = re.findall(pattern, text)
    print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘abc123’, ‘XYZ789’]
    “`

  • 否定 ([^...]): 角かっこの先頭にキャレット^を置くと、その文字クラスで指定された文字以外の任意の一文字にマッチします。

    “`python
    import re

    text = “Hello, world! 123.”
    pattern = r”[^0-9 ]” # 数字と空白以外の文字

    matches = re.findall(pattern, text)
    print(f”‘{pattern}’ で検索: {matches}”) # 出力: [‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘,’, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘!’, ‘.’]

    pattern2 = r”[^aeiouAEIOU]” # 母音以外の文字
    matches2 = re.findall(pattern2, text)
    print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘H’, ‘l’, ‘l’, ‘,’, ‘ ‘, ‘w’, ‘r’, ‘l’, ‘d’, ‘!’, ‘ ‘, ‘1’, ‘2’, ‘3’, ‘.’]
    ``
    **注意:** 文字クラス内では、多くのメタ文字が特別な意味を失います。例えば、
    *,+,?,.などは文字クラス内ではリテラル文字として扱われます(ただし、,^`は特殊な意味を持つことがあります)。

2.9. () (丸かっこ):グループ化とキャプチャ

丸かっこ()は、複数の要素をまとめてグループ化するために使われます。グループ化されたパターンは、量指定子(*, +, ?, {}) をまとめて適用したり、後述するOR条件|と組み合わせて使ったりする際に便利です。

また、グループ化されたパターンにマッチした部分は「キャプチャ」され、後で取り出すことができます。これを「キャプチャグループ」と呼びます。

“`python
import re

text = “ababab cdc”

pattern1 = r”(ab)+” # ‘ab’ という並びが1回以上の繰り返し
matches1 = re.findall(pattern1, text)
print(f”‘{pattern1}’ で検索: {matches1}”) # 出力: [‘ab’] – findall()はキャプチャグループがある場合、グループの中身だけを返す

グループ全体のマッチを取得するにはsearch()を使用

match1_search = re.search(pattern1, text)
print(f”‘{pattern1}’ で検索 (search): {match1_search.group() if match1_search else ‘マッチなし’}”) # 出力: ‘ababab’

text2 = “Nov Dec Jan Feb Mar”
pattern2 = r”(Jan|Feb|Mar)” # ‘Jan’ または ‘Feb’ または ‘Mar’

matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘Jan’, ‘Feb’, ‘Mar’]

複数のキャプチャグループ

text3 = “Name: Alice, Age: 30″
pattern3 = r”Name: (\w+), Age: (\d+)” # (\w+):単語構成文字の繰り返し, (\d+):数字の繰り返し

match3 = re.search(pattern3, text3)

if match3:
print(f”パターン ‘{pattern3}’ が見つかりました。”)
print(f”全体のマッチ: {match3.group(0)}”) # group(0) または group() は全体のマッチ
print(f”最初のグループ (名前): {match3.group(1)}”) # 最初のグループ
print(f”二番目のグループ (年齢): {match3.group(2)}”) # 二番目のグループ
print(f”全てのグループ: {match3.groups()}”) # 全てのグループをタプルで取得
else:
print(“パターンは見つかりませんでした。”)

出力例:

パターン ‘Name: (\w+), Age: (\d+)’ が見つかりました。

全体のマッチ: Name: Alice, Age: 30

最初のグループ (名前): Alice

二番目のグループ (年齢): 30

全てのグループ: (‘Alice’, ’30’)

“`

findall()関数は、引数なしで呼ばれた場合は文字列全体のマッチのリストを返しますが、パターンの中にキャプチャグループが一つでも含まれている場合は、各マッチに対して、各キャプチャグループにマッチした部分の文字列のリストを返します。キャプチャグループが複数ある場合は、それらのタプルのリストを返します。

“`python
import re

text = “Data1: ValueA, Data2: ValueB, Data3: ValueC”
pattern_single_group = r”Data\d: (\w+)” # 値の部分をキャプチャ

matches_single = re.findall(pattern_single_group, text)
print(f”単一グループ ‘{pattern_single_group}’ で検索: {matches_single}”) # 出力: [‘ValueA’, ‘ValueB’, ‘ValueC’]

pattern_multiple_groups = r”(Data\d): (\w+)” # キーと値の両方をキャプチャ

matches_multiple = re.findall(pattern_multiple_groups, text)
print(f”複数グループ ‘{pattern_multiple_groups}’ で検索: {matches_multiple}”) # 出力: [(‘Data1’, ‘ValueA’), (‘Data2’, ‘ValueB’), (‘Data3’, ‘ValueC’)]

pattern_no_group = r”Value\w” # グループなし
matches_no_group = re.findall(pattern_no_group, text)
print(f”グループなし ‘{pattern_no_group}’ で検索: {matches_no_group}”) # 出力: [‘ValueA’, ‘ValueB’, ‘ValueC’]

パターン全体のマッチリストを取得したい場合は、非キャプチャグループを使用するか finditer() を使う

pattern_non_capturing = r”(?:Data\d): (?:\w+)” # 非キャプチャグループを使用 (後述)
matches_non_capturing = re.findall(pattern_non_capturing, text)
print(f”非キャプチャグループ ‘{pattern_non_capturing}’ で検索: {matches_non_capturing}”) # 出力: [‘Data1: ValueA’, ‘Data2: ValueB’, ‘Data3: ValueC’]
“`

非キャプチャグループ (?:...):

グループ化はしたいけれど、その部分をキャプチャして後で取り出す必要がない場合は、(?:...) という形式の「非キャプチャグループ」を使うことができます。非キャプチャグループはパフォーマンスがわずかに向上する可能性があり、特にfindall()を使った場合にキャプチャグループの中身ではなくパターン全体のマッチを取得したい場合に役立ちます。

“`python
import re

text = “apple,banana,cherry”
pattern_capturing = r”(?:apple|banana)” # apple または banana にマッチ(非キャプチャ)
matches_capturing = re.findall(pattern_capturing, text)
print(f”非キャプチャグループ ‘{pattern_capturing}’ で検索: {matches_capturing}”) # 出力: [‘apple’, ‘banana’]

比較のため、キャプチャグループを使った場合

pattern_capturing_compare = r”(apple|banana)”
matches_capturing_compare = re.findall(pattern_capturing_compare, text)
print(f”キャプチャグループ ‘{pattern_capturing_compare}’ で検索: {matches_capturing_compare}”) # 出力: [‘apple’, ‘banana’]

この例では findall の結果は同じに見えますが、内部的な Match オブジェクトの取得方法などが異なります。

重要なのは、(?:…) がグループ番号に影響を与えないことです。

“`

2.10. | (縦棒):OR条件

縦棒|は、複数のパターンの中からいずれか一つにマッチすることを意味します。OR条件を適用する範囲は、通常はパターン全体ですが、()でグループ化することで特定の範囲に適用できます。

“`python
import re

text = “apple banana cherry date”

pattern1 = r”apple|banana” # ‘apple’ または ‘banana’ にマッチ
matches1 = re.findall(pattern1, text)
print(f”‘{pattern1}’ で検索: {matches1}”) # 出力: [‘apple’, ‘banana’]

pattern2 = r”bana(na|nas)” # ‘bana’ の後に ‘na’ または ‘nas’
text2 = “banana bananas banan”
matches2 = re.findall(pattern2, text2)
print(f”‘{pattern2}’ で検索: {matches2}”) # 出力: [‘na’, ‘nas’] (グループの中身だけを返すことに注意)

グループ全体を見つけるには findall を使わず search() を使うか、非キャプチャグループを使う

pattern2_non_capturing = r”bana(?:na|nas)”
matches2_nc = re.findall(pattern2_non_capturing, text2)
print(f”‘{pattern2_non_capturing}’ で検索: {matches2_nc}”) # 出力: [‘banana’, ‘bananas’]
“`

3. よく使われる特殊シーケンス

正規表現には、特定の種類の文字を表すためにバックスラッシュ\と組み合わせた特別なシーケンスがいくつかあります。これらは文字クラス[]の省略形として非常によく使われます。

  • \d: 数字にマッチします。[0-9]と等価です。
  • \D: 数字以外にマッチします。[^0-9]と等価です。
  • \w: 単語構成文字にマッチします。[a-zA-Z0-9_]と等価です(言語や環境によっては異なる場合があります)。
  • \W: 単語構成文字以外にマッチします。[^a-zA-Z0-9_]と等価です。
  • \s: 空白文字にマッチします(スペース, タブ \t, 改行 \n, キャリッジリターン \r, フォームフィード \f, 垂直タブ \v)。[ \t\n\r\f\v]と等価です。
  • \S: 空白文字以外にマッチします。[^ \t\n\r\f\v]と等価です。
  • \b: 単語の境界にマッチします。単語構成文字と非単語構成文字の間、または文字列の先頭/末尾と単語構成文字の間です。
  • \B: 単語の境界以外にマッチします。

“`python
import re

text = “The price is $100.50. Item_ID: 123_abc.”

\d, \D の例

pattern_digit = r”\d+” # 1回以上の数字
matches_digit = re.findall(pattern_digit, text)
print(f”‘{pattern_digit}’ で検索: {matches_digit}”) # 出力: [‘100′, ’50’, ‘123’]

pattern_non_digit = r”\D+” # 1回以上の数字以外
matches_non_digit = re.findall(pattern_non_digit, text)
print(f”‘{pattern_non_digit}’ で検索: {matches_non_digit}”)

出力例: [‘The price is $’, ‘.’, ‘. Item_ID: ‘, ‘_abc.’] (空白や句読点なども含まれる)

\w, \W の例

pattern_word_char = r”\w+” # 1回以上の単語構成文字
matches_word_char = re.findall(pattern_word_char, text)
print(f”‘{pattern_word_char}’ で検索: {matches_word_char}”) # 出力: [‘The’, ‘price’, ‘is’, ‘100’, ’50’, ‘Item_ID’, ‘123_abc’]

pattern_non_word_char = r”\W+” # 1回以上の単語構成文字以外
matches_non_word_char = re.findall(pattern_non_word_char, text)
print(f”‘{pattern_non_word_char}’ で検索: {matches_non_word_char}”)

出力例: [‘ ‘, ‘ ‘, ‘ ‘, ‘$’, ‘.’, ‘ ‘, ‘: ‘, ‘.’] (空白、句読点、$などが含まれる)

\s, \S の例

text2 = “Line 1\nLine 2\tLine 3″
pattern_whitespace = r”\s+” # 1回以上の空白文字
matches_whitespace = re.findall(pattern_whitespace, text2)
print(f”‘{pattern_whitespace}’ で検索: {matches_whitespace}”) # 出力: [‘\n’, ‘\t’]

pattern_non_whitespace = r”\S+” # 1回以上の空白文字以外
matches_non_whitespace = re.findall(pattern_non_whitespace, text2)
print(f”‘{pattern_non_whitespace}’ で検索: {matches_non_whitespace}”) # 出力: [‘Line’, ‘1’, ‘Line’, ‘2’, ‘Line’, ‘3’]

\b, \B の例

text3 = “cat catalog concatenate the scat”
pattern_boundary = r”\bcat\b” # 単語の境界にある’cat’
matches_boundary = re.findall(pattern_boundary, text3)
print(f”‘{pattern_boundary}’ で検索: {matches_boundary}”) # 出力: [‘cat’] (‘catalog’, ‘concatenate’, ‘scat’ の中の ‘cat’ にはマッチしない)

pattern_non_boundary = r”\Bcat\B” # 単語の境界にない’cat’ (catの前後が単語構成文字)
matches_non_boundary = re.findall(pattern_non_boundary, text3)
print(f”‘{pattern_non_boundary}’ で検索: {matches_non_boundary}”) # 出力: [‘cat’] (‘concatenate’の中の’cat’にマッチ)

pattern_non_boundary_end = r”\Bcat\b” # catの直前が単語構成文字、直後が単語境界
matches_non_boundary_end = re.findall(pattern_non_boundary_end, text3)
print(f”‘{pattern_non_boundary_end}’ で検索: {matches_non_boundary_end}”) # 出力: [‘cat’] (‘scat’の中の’cat’にマッチ)

pattern_non_boundary_start = r”\bcat\B” # catの直前が単語境界、直後が単語構成文字
matches_non_boundary_start = re.findall(pattern_non_boundary_start, text3)
print(f”‘{pattern_non_boundary_start}’ で検索: {matches_non_boundary_start}”) # 出力: [‘cat’] (‘catalog’の中の’cat’にマッチ)
“`

これらの特殊シーケンスは非常に便利で、正規表現を簡潔に記述するために頻繁に使用されます。

4. reモジュールの主要関数詳解

これまでに登場したre.search()re.findall()以外にも、reモジュールには様々な関数があります。ここでは、特によく使う主要関数について詳しく見ていきましょう。

4.1. re.search(pattern, string, flags=0)

re.search()関数は、対象のstring全体からpatternにマッチする最初の箇所を探します。マッチが見つかればMatchオブジェクトを返し、見つからなければNoneを返します。

“`python
import re

text = “apple banana apple cherry”
pattern = r”apple”

match = re.search(pattern, text)

if match:
print(f”‘{pattern}’ が見つかりました: {match.group()}”)
print(f”開始位置: {match.start()}”)
print(f”終了位置: {match.end()}”)
print(f”範囲: {match.span()}”)
else:
print(“見つかりませんでした。”)

別の例 – 最初に見つかったものだけを返す

text2 = “The quick brown fox jumps over the lazy fox.”
pattern2 = r”fox”

match2 = re.search(pattern2, text2)
if match2:
print(f”最初に見つかった ‘{pattern2}’: {match2.group()} (位置: {match2.span()})”)

Matchオブジェクトが持つメソッド(再掲)

group(N): N番目のキャプチャグループにマッチした文字列を取得 (0は全体)

groups(): 全てのキャプチャグループにマッチした文字列をタプルで取得

start(N): N番目のキャプチャグループの開始インデックス

end(N): N番目のキャプチャグループの終了インデックス+1

span(N): N番目のキャプチャグループの (開始インデックス, 終了インデックス+1) のタプル

“`

re.search()は「パターンが文字列のどこかに存在するか」を調べたい場合に適しています。

4.2. re.match(pattern, string, flags=0)

re.match()関数は、re.search()と似ていますが、検索が対象のstring先頭から行われます。パターンが文字列の先頭にマッチする場合のみMatchオブジェクトを返し、そうでない場合はマッチする箇所が文字列の途中にあってもNoneを返します。

“`python
import re

text = “apple banana”
pattern = r”apple”

match_match = re.match(pattern, text) # 文字列の先頭にマッチ
match_search = re.search(pattern, text) # 文字列全体から検索

print(f”re.match(‘{pattern}’, ‘{text}’): {match_match.group() if match_match else ‘マッチなし’}”) # 出力: apple
print(f”re.search(‘{pattern}’, ‘{text}’): {match_search.group() if match_search else ‘マッチなし’}”) # 出力: apple

text2 = “banana apple”
pattern2 = r”apple”

match_match2 = re.match(pattern2, text2) # 文字列の先頭にマッチしない
match_search2 = re.search(pattern2, text2) # 文字列全体から検索

print(f”re.match(‘{pattern2}’, ‘{text2}’): {match_match2.group() if match_match2 else ‘マッチなし’}”) # 出力: マッチなし
print(f”re.search(‘{pattern2}’, ‘{text2}’): {match_search2.group() if match_search2 else ‘マッチなし’}”) # 出力: apple
“`

re.match()は「文字列が特定のパターンで始まるか」を調べたい場合に適しています。文字列の途中のマッチを調べたい場合は必ずre.search()を使用してください。

4.3. re.findall(pattern, string, flags=0)

re.findall()関数は、対象のstring全体からpatternにマッチする全ての非重複箇所を探し、それらを文字列のリストとして返します。これは非常に頻繁に使われる便利な関数です。

前述のように、パターンにキャプチャグループが含まれているかどうかで返されるリストの内容が変わる点に注意が必要です。

  • パターンにキャプチャグループがない場合: マッチした文字列全体がリストの要素となります。
  • パターンに単一のキャプチャグループがある場合: そのグループにマッチした文字列がリストの要素となります。
  • パターンに複数のキャプチャグループがある場合: 各マッチに対して、各グループにマッチした文字列のタプルがリストの要素となります。

“`python
import re

text = “Prices are $10, $20, and $30.”

キャプチャグループなし

pattern_no_group = r”\$\d+” # ‘$’ の後に1回以上の数字
matches_no_group = re.findall(pattern_no_group, text)
print(f”‘{pattern_no_group}’ で検索 (グループなし): {matches_no_group}”) # 出力: [‘$10’, ‘$20’, ‘$30’]

単一キャプチャグループ

pattern_single_group = r”\$(\d+)” # ‘$’ の後に (数字の繰り返し) をキャプチャ
matches_single_group = re.findall(pattern_single_group, text)
print(f”‘{pattern_single_group}’ で検索 (単一グループ): {matches_single_group}”) # 出力: [’10’, ’20’, ’30’]

複数キャプチャグループ

text2 = “Item: A, Qty: 100\nItem: B, Qty: 200″
pattern_multiple_groups = r”Item: (\w+), Qty: (\d+)” # (アイテム名), (数量) をキャプチャ
matches_multiple_groups = re.findall(pattern_multiple_groups, text2)
print(f”‘{pattern_multiple_groups}’ で検索 (複数グループ): {matches_multiple_groups}”) # 出力: [(‘A’, ‘100’), (‘B’, ‘200’)]
“`

re.findall()は、対象文字列の中から特定の形式のデータをまとめて抽出したい場合に非常に役立ちます。

4.4. re.finditer(pattern, string, flags=0)

re.finditer()関数は、re.findall()と同様に全ての非重複マッチを検索しますが、結果をリストではなくイテレータとして返します。イテレータは、Matchオブジェクトを一つずつ返します。

この関数は、マッチした部分が多数ある場合に、全てのマッチを一度にメモリに読み込むre.findall()よりもメモリ効率が良いという利点があります。また、Matchオブジェクトとして結果が得られるため、各マッチの開始/終了位置や複数のキャプチャグループの値などを簡単に取得できます。

“`python
import re

text = “apple banana apple cherry”
pattern = r”apple”

findall と比較

matches_findall = re.findall(pattern, text)
print(f”findall の結果: {matches_findall}”) # 出力: [‘apple’, ‘apple’]

finditer の結果

matches_finditer = re.finditer(pattern, text)
print(“finditer の結果:”)
for match in matches_finditer:
print(f” マッチ: {match.group()}, 位置: {match.span()}”)

出力例:

マッチ: apple, 位置: (0, 5)

マッチ: apple, 位置: (12, 17)

複数グループの場合

text2 = “ID: 001, Name: Alice\nID: 002, Name: Bob”
pattern2 = r”ID: (\d+), Name: (\w+)”

matches2_finditer = re.finditer(pattern2, text2)
print(f”‘{pattern2}’ で検索 (finditer):”)
for match in matches2_finditer:
print(f” 全体マッチ: {match.group()}”)
print(f” ID: {match.group(1)}, Name: {match.group(2)}”)
print(f” グループ全体: {match.groups()}”)
print(f” 位置: {match.span()}”)

出力例:

全体マッチ: ID: 001, Name: Alice

ID: 001, Name: Alice

グループ全体: (‘001’, ‘Alice’)

位置: (0, 20)

全体マッチ: ID: 002, Name: Bob

ID: 002, Name: Bob

グループ全体: (‘002’, ‘Bob’)

位置: (21, 40)

“`

大量のテキストや多数のマッチを扱う場合は、re.finditer()の使用を検討すると良いでしょう。

4.5. re.sub(pattern, repl, string, count=0, flags=0)

re.sub()関数は、対象のstring中でpatternにマッチする全ての箇所を、別の文字列repl置換します。置換後の文字列を返します。

  • pattern: 検索する正規表現パターン。
  • repl: 置換する文字列。キャプチャグループを参照するために\1, \2\g<1>, \g<name>などを使用できます。関数を指定することも可能です。
  • string: 置換対象の文字列。
  • count: 最大置換回数を指定します。デフォルトは0で、全てを置換します。
  • flags: オプションフラグを指定します(後述)。

“`python
import re

text = “Date: 2023-10-27, Time: 14:30:00”

日付形式をスラッシュ区切りに置換

pattern: 年(\d{4}) – 月(\d{2}) – 日(\d{2}) をキャプチャ

repl: \1/\2/\3 でキャプチャしたグループを参照して置換

pattern = r”(\d{4})-(\d{2})-(\d{2})”
repl = r”\1/\2/\3″ # or r”\g<1>/\g<2>/\g<3>”

new_text = re.sub(pattern, repl, text)
print(f”置換前: {text}”)
print(f”置換後: {new_text}”) # 出力: Date: 2023/10/27, Time: 14:30:00

不要な空白を削除

text2 = “This has too much space.”
pattern2 = r”\s+” # 1回以上の空白
repl2 = ” ” # 1つのスペースに置換

new_text2 = re.sub(pattern2, repl2, text2)
print(f”置換前: {text2}”)
print(f”置換後: {new_text2}”) # 出力: This has too much space.

特定回数だけ置換

text3 = “apple banana apple cherry apple date”
pattern3 = r”apple”
repl3 = “orange”

new_text3_once = re.sub(pattern3, repl3, text3, count=1)
print(f”1回だけ置換: {new_text3_once}”) # 出力: orange banana apple cherry apple date

new_text3_twice = re.sub(pattern3, repl3, text3, count=2)
print(f”2回だけ置換: {new_text3_twice}”) # 出力: orange banana orange cherry apple date
“`

repl引数に関数を指定する:

re.sub()repl引数には、文字列だけでなく関数を指定することもできます。この関数は、マッチが見つかるたびに呼び出され、その戻り値が置換文字列として使用されます。関数は引数としてMatchオブジェクトを受け取ります。これにより、マッチした内容に応じて動的に置換文字列を生成できます。

“`python
import re

text = “Value1: 10, Value2: 25, Value3: 5”

マッチした数字を2倍にする関数

def double_match(match):
value_str = match.group(1) # 最初のキャプチャグループ(数字)を取得
value = int(value_str)
return str(value * 2)

pattern = r”Value\d: (\d+)” # 数字をキャプチャ

new_text = re.sub(pattern, double_match, text)
print(f”置換前: {text}”)
print(f”置換後: {new_text}”) # 出力: Value1: 20, Value2: 50, Value3: 10
“`

関数を使った置換は、複雑な条件に基づいて文字列を生成したい場合に非常に強力です。

4.6. re.split(pattern, string, maxsplit=0, flags=0)

re.split()関数は、patternにマッチする箇所を区切り文字として、対象のstringを分割します。結果は文字列のリストとして返されます。

“`python
import re

text = “apple,banana;cherry:date”

‘,’ ‘;’ ‘:’ のいずれかで分割

pattern = r”[,;:]”

parts = re.split(pattern, text)
print(f”‘{pattern}’ で分割: {parts}”) # 出力: [‘apple’, ‘banana’, ‘cherry’, ‘date’]

text2 = “word1 word2\tword3\nword4″
pattern2 = r”\s+” # 1回以上の空白文字で分割

parts2 = re.split(pattern2, text2)
print(f”‘{pattern2}’ で分割: {parts2}”) # 出力: [‘word1’, ‘word2’, ‘word3’, ‘word4’]

maxsplit 引数

text3 = “1-2-3-4-5″
pattern3 = r”-“

parts3_all = re.split(pattern3, text3)
print(f”全て分割: {parts3_all}”) # 出力: [‘1’, ‘2’, ‘3’, ‘4’, ‘5’]

parts3_one = re.split(pattern3, text3, maxsplit=1)
print(f”1回だけ分割: {parts3_one}”) # 出力: [‘1’, ‘2-3-4-5’]

parts3_two = re.split(pattern3, text3, maxsplit=2)
print(f”2回だけ分割: {parts3_two}”) # 出力: [‘1’, ‘2’, ‘3-4-5’]
“`

キャプチャグループを含むパターンで分割した場合:

re.split()に渡すパターンにキャプチャグループが含まれている場合、そのキャプチャグループにマッチした区切り文字自体も結果のリストに含まれます。

“`python
import re

text = “apple,banana;cherry”
pattern_no_group = r”[,;]”
pattern_with_group = r”([,;])” # 区切り文字をキャプチャグループにする

parts_no_group = re.split(pattern_no_group, text)
print(f”区切り文字含まず分割: {parts_no_group}”) # 出力: [‘apple’, ‘banana’, ‘cherry’]

parts_with_group = re.split(pattern_with_group, text)
print(f”区切り文字含めて分割: {parts_with_group}”) # 出力: [‘apple’, ‘,’, ‘banana’, ‘;’, ‘cherry’]
“`
これは、分割されたデータとその区切り文字の両方を保持したい場合に便利です。

5. パターンのコンパイル (re.compile)

同じ正規表現パターンを何度も繰り返し使用する場合、パターンを事前に「コンパイル」しておくことで、処理速度が向上することがあります。re.compile()関数は正規表現パターンを正規表現オブジェクトにコンパイルします。

“`python
import re

コンパイルしない場合

text = “This is the first string with numbers 123.”
text2 = “Another string with numbers 456 and 789.”
pattern_raw = r”\d+”

matches1 = re.findall(pattern_raw, text)
matches2 = re.findall(pattern_raw, text2)
print(f”コンパイルしない場合1: {matches1}”)
print(f”コンパイルしない場合2: {matches2}”)

コンパイルする場合

pattern_compiled = re.compile(r”\d+”)

matches3 = pattern_compiled.findall(text)
matches4 = pattern_compiled.findall(text2)
print(f”コンパイルする場合1: {matches3}”)
print(f”コンパイルする場合2: {matches4}”)
“`

コンパイルされた正規表現オブジェクト(上記の例ではpattern_compiled)は、search(), match(), findall(), finditer(), sub(), split()などのメソッドを持ちます。これらのメソッドの使い方は、reモジュールの同名の関数とほぼ同じですが、最初の引数であるpatternを指定する必要はありません。

“`python
import re

pattern = re.compile(r”(\w+) (\w+)”) # 単語2つをキャプチャするパターンをコンパイル

text = “Hello World”
match = pattern.search(text) # コンパイル済みオブジェクトのメソッドを呼び出す

if match:
print(f”Match: {match.group()}”)
print(f”Group 1: {match.group(1)}”)
print(f”Group 2: {match.group(2)}”)

text2 = “Python Programming”
matches = pattern.findall(text2)
print(f”Findall: {matches}”)
“`

頻繁に同じパターンを使う場合や、正規表現のフラグを複数回指定する必要がある場合に、re.compile()を使うとコードが整理され、実行効率も良くなります。

6. フラグ (Flags)

reモジュールの関数の多くやre.compile()には、flags引数があります。これにより、マッチングの挙動を細かく制御できます。複数のフラグを同時に指定する場合は、|(ビットOR演算子)で繋げて指定します。

よく使われるフラグは以下の通りです。

  • re.IGNORECASE または re.I: 大文字・小文字を区別しないマッチングを行います。
  • re.MULTILINE または re.M: ^$が文字列全体の先頭/末尾だけでなく、改行文字の直後(行の先頭)/直前(行の末尾)にもマッチするようになります。
  • re.DOTALL または re.S: ドット.が通常マッチしない改行文字\nにもマッチするようになります。
  • re.VERBOSE または re.X: 正規表現パターン中に空白やコメント(#以降)を含めることができるようになり、複雑なパターンを読みやすく記述できます。

“`python
import re

re.IGNORECASE (re.I)

text_case = “Apple apple APPLE”
pattern_case = r”apple”

matches_case_sensitive = re.findall(pattern_case, text_case)
print(f”大文字小文字区別あり: {matches_case_sensitive}”) # 出力: [‘apple’]

matches_case_insensitive = re.findall(pattern_case, text_case, re.IGNORECASE)
print(f”大文字小文字区別なし (re.I): {matches_case_insensitive}”) # 出力: [‘Apple’, ‘apple’, ‘APPLE’]

re.MULTILINE (re.M)

text_multiline = “Line 1\nLine 2\nLine 3″
pattern_multiline = r”^Line” # 行の先頭にマッチ

matches_multiline_default = re.findall(pattern_multiline, text_multiline)
print(f”複数行モードなし: {matches_multiline_default}”) # 出力: [‘Line’] (文字列の先頭のみ)

matches_multiline_m = re.findall(pattern_multiline, text_multiline, re.MULTILINE)
print(f”複数行モードあり (re.M): {matches_multiline_m}”) # 出力: [‘Line’, ‘Line’, ‘Line’] (各行の先頭にマッチ)

re.DOTALL (re.S)

text_dotall = “Line 1\nLine 2″
pattern_dotall = r”Line.Line” # ‘.’ は通常改行にマッチしない

match_dotall_default = re.search(pattern_dotall, text_dotall)
print(f”改行に’.’がマッチしない: {match_dotall_default}”) # 出力: None

match_dotall_s = re.search(pattern_dotall, text_dotall, re.DOTALL)
print(f”改行に’.’がマッチする (re.S): {match_dotall_s.group() if match_dotall_s else ‘マッチなし’}”) # 出力: ‘Line 1\nLine’

re.VERBOSE (re.X)

複雑なパターンを読みやすく書く

pattern_verbose = re.compile(r”””
^ # 行の先頭
(\d{3}) # 3桁の数字 (グループ1)
– # ハイフン
(\d{4}) # 4桁の数字 (グループ2)
$ # 行の末尾
“””, re.VERBOSE) # re.X と書いても同じ

text_verbose = “123-4567”
match_verbose = pattern_verbose.match(text_verbose)

if match_verbose:
print(f”VERBOSEモードでマッチ: {match_verbose.group()}”)
print(f”グループ1: {match_verbose.group(1)}”)
print(f”グループ2: {match_verbose.group(2)}”)

出力例:

VERBOSEモードでマッチ: 123-4567

グループ1: 123

グループ2: 4567

“`

re.VERBOSEフラグは、特に複雑な正規表現パターンを記述する際に、可読性を劇的に向上させることができます。空白やコメントは、正規表現の実際のパターンとしては無視されます。

複数のフラグを組み合わせる例:

“`python
import re

text = “EMAIL: [email protected]\nE-mail: [email protected]
pattern = re.compile(r”””
^E-?mail: \s* # 行頭から E-mail または Email に続き、コロンと任意個の空白
(\w+@\w+.\w+) # ユーザー名@ドメイン名.トップレベルドメイン をキャプチャ
“””, re.IGNORECASE | re.MULTILINE | re.VERBOSE) # 大文字小文字区別なし、複数行モード、VERBOSEモード

matches = pattern.findall(text)
print(f”複数フラグ使用で検索: {matches}”) # 出力: [‘[email protected]’, ‘[email protected]’]
“`

7. 実践的な使い方と注意点

7.1. 生文字列 (r"...") の活用

先ほども触れましたが、正規表現パターンを記述する際は、バックスラッシュのエスケープ地獄を避けるために、必ず生文字列(Raw String)を使用しましょう。パターン文字列の先頭にrを付けるだけで、文字列中のバックスラッシュがそのままバックスラッシュとして解釈されます。

“`python

推奨されない書き方

pattern = “\section” # Python文字列としてエスケープが必要

推奨される書き方

pattern = r”\section” # 正規表現パターンそのまま
“`

これは正規表現を扱う上での必須のテクニックと言えます。

7.2. Greedy (貪欲) と Non-Greedy (非貪欲) マッチ

量指定子(*, +, ?, {})は、デフォルトでは「貪欲 (Greedy)」です。これは、可能な限り長い文字列にマッチしようとする性質です。

“`python
import re

text = “This is bold and this too

貪欲なパターン: ‘>’ が最初に見つかるまでではなく、可能な限り最後の ‘>’ までマッチしようとする

pattern_greedy = r”.*

match_greedy = re.search(pattern_greedy, text)
print(f”貪欲マッチ: {match_greedy.group()}”)

出力: This is bold and this too

最初の から最後の まで全てにマッチしてしまう

“`

意図した長さのマッチを得るために、量指定子の直後に?を付けることで、「非貪欲 (Non-Greedy)」または「最小一致」モードに切り替えることができます。これにより、可能な限り短い文字列にマッチするようになります。

  • *?: ゼロ個以上の繰り返し(非貪欲)
  • +?: 1個以上の繰り返し(非貪欲)
  • ??: ゼロ個または1個の繰り返し(非貪欲)
  • {n,m}?: n回以上m回以下の繰り返し(非貪欲)

“`python
import re

text = “This is bold and this too

非貪欲なパターン: ‘>’ が最初に見つかったところでマッチを終了する

pattern_non_greedy = r”.*?” # * の直後に ? を付ける

matches_non_greedy = re.findall(pattern_non_greedy, text)
print(f”非貪欲マッチ: {matches_non_greedy}”)

出力: [‘This is bold‘, ‘this too‘]

それぞれの の組に正しくマッチする

“`

HTML/XMLのような構造化データからタグで囲まれた部分を抽出する際などは、この非貪欲マッチが非常に重要になります。ただし、HTML/XMLのパースには正規表現は限界があるため、専用のライブラリ(Beautiful Soupなど)を使う方がより堅牢で安全です。

7.3. 肯定先読み ((?=...))、否定先読み ((?!...))

先読み (Lookahead) は、現在の位置の後方に特定のパターンが続く場合にマッチしますが、そのパターン自体はマッチ結果に含まれません。これは、特定の条件を満たす位置を見つけたいが、その条件となる文字列自身は取得したくない場合に便利です。

  • 肯定先読み (?=...): ...で指定したパターンが続く場合にマッチ。
  • 否定先読み (?!...): ...で指定したパターンが続かない場合にマッチ。

“`python
import re

text = “apple,banana,cherry”

‘,’ の直前に ‘banana’ がある位置 (マッチ結果は位置のみ)

pattern_lookahead = r”banana(?=,)”

match_lookahead = re.search(pattern_lookahead, text)
if match_lookahead:
print(f”肯定先読みマッチ: {match_lookahead.group()}”) # 出力: banana
# マッチ結果は ‘banana’ ですが、’, ‘ が続いているという条件を満たした ‘banana’ です。
# ‘, ‘ 自体はマッチ結果に含まれません。

‘a’ の後に数字が続かない ‘a’ にマッチ

text2 = “a1 b2 c a3 d”
pattern_neg_lookahead = r”a(?!\d)”

matches_neg_lookahead = re.findall(pattern_neg_lookahead, text2)
print(f”否定先読みマッチ: {matches_neg_lookahead}”) # 出力: [‘a’] (‘a3’の’a’にはマッチしない)
“`

7.4. 肯定後読み ((?<=...))、否定後読み ((?<!...))

後読み (Lookbehind) は、現在の位置の前方に特定のパターンがある場合にマッチしますが、そのパターン自体はマッチ結果に含まれません。先読みと同様に、条件を満たす位置を見つけたいが、条件となる文字列自身は取得したくない場合に便利です。後読みの中のパターンは、固定長である必要がある場合があります(可変長の後読みは一部の正規表現エンジンでサポートされますが、Pythonのreモジュールは基本的に固定長のみをサポートします。ただし、一部のシンプルな可変長パターンは許容されることがあります)。

  • 肯定後読み (?<=...): ...で指定したパターンが直前にある場合にマッチ。
  • 否定後読み (?<!...): ...で指定したパターンが直前にない場合にマッチ。

“`python
import re

text = “Value: 100, Price: 50”

‘Value: ‘ の直後にある数字にマッチ (数字だけ取得)

pattern_lookbehind = r”(?<=Value: )\d+”

match_lookbehind = re.search(pattern_lookbehind, text)
if match_lookbehind:
print(f”肯定後読みマッチ: {match_lookbehind.group()}”) # 出力: 100

‘$’ の直後ではない数字にマッチ

text2 = “Total: $100. Free: 50″
pattern_neg_lookbehind = r”(?<!\$)\b\d+\b” # ‘$’の直後ではない単語境界にある数字

matches_neg_lookbehind = re.findall(pattern_neg_lookbehind, text2)
print(f”否定後読みマッチ: {matches_neg_lookbehind}”) # 出力: [‘100′, ’50’] (Total:の100とFree:の50にマッチ。$100の100にはマッチしない)
“`
先読み・後読みは、より洗練されたパターンマッチングを行うための高度なテクニックです。最初は難しく感じるかもしれませんが、使いこなせると非常に強力です。

7.5. 正規表現のデバッグ

複雑な正規表現パターンを作成する際は、意図した通りに機能するかをテスト・デバッグすることが重要です。以下の方法が役立ちます。

  • 小さな文字列でテストする: いきなり大きなテキストで試すのではなく、様々なパターン(マッチする場合、しない場合、境界条件など)を含む短いテスト文字列で試しましょう。
  • re.findall()re.finditer()を使う: マッチする全ての箇所を確認することで、パターンの意図しない挙動を発見しやすくなります。
  • re.VERBOSEフラグを使う: 複雑なパターンを分解してコメントを付けることで、パターン自体の理解が深まります。
  • オンラインツールを使う: Regex101 (regex101.com) や RegExr (regexr.com) など、オンラインの正規表現テスターは非常に強力です。パターンとテスト文字列を入力すると、マッチする部分をハイライト表示したり、各部分の意味を解説してくれたり、デバッグ情報を提供してくれます。これらのツールを活用すると、効率的にパターンを開発できます。

7.6. 複雑なパターンを書く際の注意点

  • 可読性: 複雑になりすぎる場合は、re.VERBOSEフラグを使ったり、パターンを小さな部分に分けて段階的に処理したりすることを検討しましょう。
  • 性能: 非常に長い文字列や大量の文字列に対して複雑な正規表現を実行すると、処理に時間がかかる場合があります。特に、多くのバックトラッキングが発生するパターン(例: (.*)* のような過剰な繰り返し)は「ReDOS (Regular Expression Denial of Service)」と呼ばれる脆弱性を引き起こす可能性もあります。貪欲マッチと非貪欲マッチの選択、グループ化の方法などを考慮し、不要なバックトラッキングを避けるようにパターンを設計することが望ましいです。
  • セキュリティ: ユーザーからの入力をそのまま正規表現パターンとして使用するのは非常に危険です。悪意のあるパターン(ReDOS攻撃を引き起こすものなど)を注入される可能性があるため、ユーザー入力を含むパターンを作成する場合は、必ず入力を適切にサニタイズするか、リテラルとして扱うように処理する必要があります。re.escape()関数は、パターン中の特殊文字をエスケープしてリテラルとして扱うためのものです。

7.7. HTML/XMLのパースには専用ライブラリを推奨

正規表現は強力ですが、HTMLやXMLのような構造化されたデータを正確にパースするには限界があります。例えば、ネストしたタグ(<b>...<b>...</b>...</b>)などを正規表現で正確に扱うのは非常に困難です。HTML/XMLのパースには、Beautiful Soupやlxmlのような専用ライブラリを使用することを強く推奨します。これらのライブラリは、ドキュメントの構造を理解し、より安全で効率的なパース機能を提供します。正規表現は、これらのライブラリで取得した要素の属性値やテキスト内容に対して、さらに詳細なパターンマッチングを行いたい場合に組み合わせて使うのが効果的です。

8. よくあるユースケースの例

正規表現は様々な場面で活用できます。いくつかの典型的なユースケースを紹介します。

  • 形式の検証:

    • メールアドレス、電話番号、郵便番号、日付形式などが正しい形式に従っているかチェックする。
    • パスワードが特定の要件(大文字、小文字、数字、記号を何文字以上含むなど)を満たしているか検証する。

    “`python
    import re

    email_pattern = r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$”
    phone_pattern = r”^\d{3}-\d{4}-\d{4}$” # 例: XXX-YYYY-ZZZZ

    print(f”‘[email protected]’ is valid email: {bool(re.match(email_pattern, ‘[email protected]’))}”)
    print(f”‘invalid-email’ is valid email: {bool(re.match(email_pattern, ‘invalid-email’))}”)
    print(f”‘090-1234-5678’ is valid phone: {bool(re.match(phone_pattern, ‘090-1234-5678’))}”)
    print(f”‘12345’ is valid phone: {bool(re.match(phone_pattern, ‘12345’))}”)
    “`
    注意: メールアドレスの正規表現は非常に複雑になる可能性があります。上記の例は簡略化されたものです。RFCに厳密に従うとパターンは膨大になります。目的によっては、正規表現ではなく専用の検証ライブラリを使う方が良い場合もあります。

  • 情報の抽出:

    • ログファイルからエラーメッセージや特定のイベントに関する情報を抽出する。
    • テキストからURL、日付、金額などの特定のパターンに合致する部分を全て抜き出す。

    “`python
    import re

    log_text = “””
    [INFO] 2023-10-27 10:00:01 – System started.
    [WARNING] 2023-10-27 10:05:23 – Low disk space.
    [ERROR] 2023-10-27 10:15:45 – Database connection failed.
    [INFO] 2023-10-27 10:20:00 – User login successful.
    “””

    ERROR行とそのメッセージを抽出

    error_pattern = re.compile(r”^[ERROR] (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) – (.*)$”, re.MULTILINE)

    for match in error_pattern.finditer(log_text):
    timestamp = match.group(1)
    message = match.group(2)
    print(f”[{timestamp}] {message}”)

    出力: [2023-10-27 10:15:45] Database connection failed.

    “`

  • データクリーニング:

    • 不要な空白、改行、HTMLタグなどを削除する。
    • データの表記ゆれ(例: “株”, “(株)”, “株式会社”)を統一する。

    “`python
    import re

    dirty_text = ” excessive space \n and \t tabs. ”
    cleaned_text = re.sub(r”\s+”, ” “, dirty_text).strip() # 連続する空白を一つに、前後の空白を削除
    print(f”クリーニング後: ‘{cleaned_text}'”) # 出力: ‘excessive space and tabs.’

    company_name = “ABC (株) デジタル”
    cleaned_name = re.sub(r” ?(?株)?”, “株式会社”, company_name) # ‘ (株)’ または ‘(株)’ または ‘株’ を ‘株式会社’ に置換
    print(f”表記ゆれ修正後: {cleaned_name}”) # 出力: ABC 株式会社 デジタル
    “`

  • 文字列の分割:

    • 複数の異なる区切り文字で文字列を分割する。

    “`python
    import re

    data_string = “Item1:100|Item2:200,Item3:300;Item4:400″
    parts = re.split(r”[:|,;]”, data_string) # ‘:’ または ‘,’ または ‘;’ で分割
    print(f”分割結果: {parts}”) # 出力: [‘Item1’, ‘100’, ‘Item2’, ‘200’, ‘Item3’, ‘300’, ‘Item4’, ‘400’]
    “`

これらの例は正規表現が解決できる問題の一部にすぎません。あなたの解決したいテキスト処理タスクに正規表現がどのように役立つかを考えてみてください。

9. まとめ:正規表現をマスターするために

この記事では、Pythonのreモジュールを使った正規表現の基本的な使い方から、メタ文字、特殊シーケンス、主要な関数、フラグ、そして実践的なテクニックまでを詳細に解説しました。

正規表現は最初は難しく感じるかもしれませんが、慣れてくるとテキスト処理において非常に強力なツールになります。その記法は最初は独特ですが、一度基本を理解すれば、様々なプログラミング言語やツール(テキストエディタ、シェルコマンドなど)で共通して使えるスキルです。

正規表現をマスターするためのポイント:

  1. 基本のメタ文字と特殊シーケンスを理解する: . * + ? [] () | ^ $ \d \w \s \b など、頻繁に使うものから覚えましょう。
  2. reモジュールの主要関数(search, match, findall, finditer, sub, split)の使い分けを理解する: それぞれの関数がどのような目的で使われるのか、どのような結果を返すのかを把握しましょう。
  3. たくさんの例を実際に書いて動かしてみる: 理論だけでなく、手を動かして様々なパターンと文字列で試すことが最も重要です。
  4. 生文字列 (r"...") を常に使う習慣をつける: バックスラッシュのエスケープ問題を避け、パターン記述をシンプルに保てます。
  5. オンラインツール(Regex101など)を積極的に活用する: パターンのテストやデバッグ、理解に役立ちます。
  6. 複雑なパターンは分割したり、VERBOSEフラグを使って読みやすくする: 可読性を意識することで、メンテナンスが容易になります。
  7. Greedy vs Non-Greedy の違いを理解し、必要に応じて ? を使う: 繰り返しパターンで意図しないマッチを防ぎます。
  8. 先読み・後読みは、必要な場合に高度なマッチングのために検討する: パターンをより洗練させることができます。

正規表現は、学習曲線が最初は急かもしれませんが、一度身につければプログラミングの様々な場面で役立つ普遍的なスキルです。この記事が、あなたのPythonでの正規表現学習の強固な土台となり、テキスト処理の幅を広げる一助となれば幸いです。

練習あるのみです!少しずつ複雑なパターンに挑戦し、正規表現の力を自分のものにしていきましょう。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール