はじめに

正規表現(regex)は、テキスト処理のための強力なツールです。これは検索パターンを定義する文字のシーケンスであり、驚くほどの柔軟性をもってテキストの検索、マッチング、管理を可能にします。Python では、正規表現の機能は re モジュールによって処理されます。

この実験(Lab)では、Python で正規表現を使用するための基礎を学びます。パターンのマッチングの基本から始め、グループ化、繰り返しのための量指定子(quantifiers)の使用、文字クラスの定義、そして複雑なマッチングシナリオを処理するための強力なメソッドやフラグの活用といった、より高度なトピックへと段階的に進みます。最終的には、正規表現を使用して高度な文字列操作を実行することに習熟するでしょう。

正規表現マッチングの紹介

このステップでは、Python で正規表現を使用するための基本的なワークフローを学習します。このプロセスは通常、次の 4 つの主要なステップで構成されます。

  1. re モジュールをインポートします。
  2. re.compile() を使用して正規表現オブジェクトを作成します。バックスラッシュがエスケープ文字として解釈されるのを防ぐため、raw 文字列 (r"...") の使用が推奨されます。
  3. 正規表現オブジェクトの search() メソッドを使用して、文字列内のパターンを検索します。パターンが見つかった場合、これは Match オブジェクトを返します。
  4. Match オブジェクトの group() メソッドを使用して、実際にマッチしたテキストを取得します。

まず、使用する一般的な正規表現の記号をいくつか見てみましょう。

記号 マッチするもの
? 直前のグループの 0 回または 1 回。
* 直前のグループの 0 回以上。
+ 直前のグループの 1 回以上。
{n} 直前のグループのちょうど n 回。
^spam 文字列が spam で始まる必要があることを意味します。
spam$ 文字列が spam で終わる必要があることを意味します。
. 改行文字を除く任意の 1 文字。
\d, \w, \s それぞれ、数字、単語、またはスペース文字。
\D, \W, \S それぞれ、数字、単語、またはスペース以外の任意の文字。
[abc] ブラケット内の任意の文字(a、b、など)。
[^abc] ブラケット内にない任意の文字。

それでは、これを実践してみましょう。文字列から電話番号を見つけるスクリプトを作成します。米国(U.S.)の電話番号のパターンは XXX-XXX-XXXX であり、ここで X は数字です。正規表現では、\d が数字を表します。

注: WebIDE(VS Code のようなもの)の使用に慣れていない場合は、この実験(lab)で先に学習してください。

左側のファイルエクスプローラーから main.py ファイルを開き、次のコードを追加します。

## Import re module for regular expression operations
import re

## re.compile(): create regex pattern object (use raw string r'' to avoid escaping)
## The pattern \d\d\d-\d\d\d-\d\d\d\d matches three digits, a hyphen, three digits, a hyphen, and four digits.
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')

## The search() method scans the string, looking for the first location where this regex produces a match.
mo = phone_num_regex.search('My number is 415-555-4242.')

## The group() method returns the string of the matched text.
print(f'Phone number found: {mo.group()}')

スクリプトを実行するには、ターミナルを開き、次のコマンドを実行します。

python3 main.py

マッチした電話番号がコンソールに出力されるはずです。

Phone number found: 415-555-4242

この基本的なワークフローは、Python におけるすべての正規表現操作の基礎となります。

グループ化と選択(Alternation)

このステップでは、グループを使用してマッチしたテキストの特定の部分を分離する方法と、パイプ(|)を使用して複数の可能なパターンのいずれかにマッチさせる方法を学びます。

丸括弧 () によるグループ化

丸括弧 () は、正規表現内でグループを作成するために使用されます。これは、電話番号から市外局番など、マッチの一部を抽出したい場合に役立ちます。個々のグループには group(n) を使用してアクセスでき、group(1) が最初のグループ、group(2) が 2 番目のグループとなります。group(0) または group() は、マッチした文字列全体を返します。

main.py を修正して、電話番号の市外局番をそれ以外の部分から分離してみましょう。

import re

## Parentheses create groups: group(1) for the first group, group(2) for the second.
## The first group (\d\d\d) captures the area code.
## The second group (\d\d\d-\d\d\d\d) captures the main number.
phone_num_regex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')
mo = phone_num_regex.search('My number is 415-555-4242.')

## To retrieve all groups at once, use the groups() method, which returns a tuple.
area_code, main_number = mo.groups()

print(f"Area Code: {area_code}")
print(f"Main Number: {main_number}")
print(f"Full Match: {mo.group(0)}")

スクリプトを実行します。

python3 main.py

出力では、番号が分離された部分が表示されます。

Area Code: 415
Main Number: 555-4242
Full Match: 415-555-4242

パイプ | による選択(Alternation)

パイプ文字 | は「OR」演算子のように機能します。これにより、複数の式のいずれかにマッチさせることができます。例えば、Batman|Tina Fey は "Batman" または "Tina Fey" のいずれかにマッチします。

パイプをグループ内で使用して、正規表現の一部として複数の可能なパターンのいずれかにマッチさせることもできます。ここでは、「Bat-」で始まるさまざまな用語にマッチさせてみましょう。main.py を次のコードに更新してください。

import re

## The pipe | matches one of several patterns within a group.
## Here, it matches 'man', 'mobile', 'copter', or 'bat'.
bat_regex = re.compile(r'Bat(man|mobile|copter|bat)')
mo = bat_regex.search('Batmobile lost a wheel')

print(f"Full match: {mo.group()}")
print(f"Group 1: {mo.group(1)}")

再度スクリプトを実行します。

python3 main.py

正規表現が "Batmobile" にマッチし、最初のグループが "mobile" をキャプチャしたことがわかります。

Full match: Batmobile
Group 1: mobile

量指定子(Quantifiers)による繰り返し

このステップでは、量化子(quantifiers)について探ります。量化子は、直前のグループが何回繰り返されるかを指定できる特殊文字です。

? によるオプションのマッチング

疑問符 ? は、直前のグループをオプション(任意)にし、ゼロ回または 1 回マッチすることを意味します。これは、出現する場合もしない場合もあるパターンに最適です。例えば、"Batman" と "Batwoman" の両方にマッチさせることができます。

main.py をこのコードに更新してください。

import re

## The (wo)? group is optional. It will match 'wo' if it exists, or match nothing if it doesn't.
bat_regex = re.compile(r'Bat(wo)?man')

mo1 = bat_regex.search('The Adventures of Batman')
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batwoman')
print(mo2.group())

スクリプトを実行します。

python3 main.py

出力は、両方の文字列が正常にマッチしたことを示しています。

Batman
Batwoman

* によるゼロ回以上のマッチング

アスタリスク *(またはスター)は、直前のグループがゼロ回以上繰り返される場合にマッチします。これは、パターンが何回繰り返されても(全く繰り返されなくても)よい場合に便利です。

import re

## The (wo)* group can appear zero or more times.
bat_regex = re.compile(r'Bat(wo)*man')

mo1 = bat_regex.search('The Adventures of Batman') ## Zero 'wo'
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batwowowowoman') ## Four 'wo's
print(mo2.group())

+ による 1 回以上のマッチング

プラス記号 + は、直前のグループが 1 回以上繰り返される場合にマッチします。スターと異なり、グループは少なくとも 1 回出現する必要があります。

import re

## The (wo)+ group must appear at least once.
bat_regex = re.compile(r'Bat(wo)+man')

mo1 = bat_regex.search('The Adventures of Batwoman') ## One 'wo'
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batman') ## Zero 'wo's
print(mo2 is None) ## This will print True because no match is found.

{} による特定回数の繰り返し

波括弧 {} を使用すると、正確な繰り返し回数、または範囲を指定できます。

  • {3} は、ちょうど 3 回マッチします。
  • {3,5} は、3 回から 5 回の間でマッチします。
  • {3,} は、3 回以上マッチします。
  • {,5} は、最大 5 回までマッチします。
import re

## (Ha){3} matches 'Ha' repeated exactly 3 times.
ha_regex = re.compile(r'(Ha){3}')
mo1 = ha_regex.search('HaHaHa')
print(mo1.group())

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

デフォルトでは、Python の正規表現は「貪欲(greedy)」であり、可能な限り長い文字列にマッチしようとします。これを「非貪欲(non-greedy)」にする(可能な限り短い文字列にマッチさせる)には、量化子の直後に疑問符 ? を追加します。

違いを見てみましょう。main.py をクリアし、次のコードを追加してください。

import re

## Greedy match: {3,5} matches the longest possible string (5 'Ha's).
greedy_ha_regex = re.compile(r'(Ha){3,5}')
mo1 = greedy_ha_regex.search('HaHaHaHaHa')
print(f"Greedy match: {mo1.group()}")

## Non-greedy match: {3,5}? matches the shortest possible string (3 'Ha's).
non_greedy_ha_regex = re.compile(r'(Ha){3,5}?')
mo2 = non_greedy_ha_regex.search('HaHaHaHaHa')
print(f"Non-greedy match: {mo2.group()}")

スクリプトを実行します。

python3 main.py

出力は違いを明確に示しています。

Greedy match: HaHaHaHaHa
Non-greedy match: HaHaHa

文字クラス、アンカー、およびワイルドカード

このステップでは、すべての一致を見つける方法、カスタム文字セットの作成、マッチを文字列の先頭または末尾に固定する方法、およびワイルドカードの使用方法など、より高度なテクニックを学びます。

findall() メソッド

search() が最初の一致のみを返すのに対し、findall() メソッドは文字列内のすべての一致しない部分(non-overlapping matches)のリストを返します。正規表現にグループが含まれている場合、findall() はタプルのリストを返し、各タプルには各グループの文字列が含まれます。

この例で main.py を更新します。

import re

## If the regex has no groups, findall() returns a list of strings.
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
matches = phone_num_regex.findall('Cell: 415-555-9999 Work: 212-555-0000')
print(matches)

スクリプトを実行して出力を確認します。

python3 main.py
['415-555-9999', '212-555-0000']

独自の文字クラス [] の作成

角括弧 [] を使用すると、その中にある任意の単一文字にマッチするカスタム文字クラスを定義できます。例えば、[aeiou] は任意の小文字の母音にマッチします。[a-z] のように範囲を指定して、任意の小文字にマッチさせることも、[0-9] のように任意の数字にマッチさせることもできます。

角括弧の最初の文字としてキャレット ^ を使用すると、そのクラスに含まれていない任意の文字にマッチする否定文字クラスが作成されます。

文字列内のすべての母音を見つけてみましょう。main.py を更新します。

import re

## [aeiouAEIOU] matches any vowel, lowercase or uppercase.
vowel_regex = re.compile(r'[aeiouAEIOU]')
vowels = vowel_regex.findall('Robocop eats baby food. BABY FOOD.')
print(vowels)

アンカー: キャレット ^ とドル記号 $

アンカーは、文字列内の特定の位置をアサート(主張)するために使用されます。

  • キャレット ^ は文字列の先頭にマッチします。
  • ドル記号 $ は文字列の末尾にマッチします。

これらを一緒に使用する(^...$)と、文字列全体がパターンにマッチしなければならないことを強制します。

import re

## ^Hello matches strings that start with 'Hello'.
begins_with_hello = re.compile(r'^Hello')
match1 = begins_with_hello.search('Hello world!')
print(match1 is not None) ## True

## \d+$ matches strings that end with one or more digits.
ends_with_number = re.compile(r'\d+$')
match2 = ends_with_number.search('Your number is 42')
print(match2.group()) ## '42'

## ^\d+$ matches strings that consist only of digits.
whole_string_is_num = re.compile(r'^\d+$')
match3 = whole_string_is_num.search('1234567890')
print(match3 is not None) ## True

ワイルドカード文字 .

ドット .(またはワイルドカード)文字は、改行を除く任意の単一文字にマッチします。これは「任意の文字」の便利な短縮形です。

末尾が "at" であるすべての 3 文字の単語を見つけてみましょう。main.py を更新します。

import re

## The . character matches any character (except newline).
## So, .at matches 'cat', 'hat', 'sat', etc.
at_regex = re.compile(r'.at')
matches = at_regex.findall('The cat in the hat sat on the flat mat.')
print(matches)

スクリプトを実行します。

python3 main.py

出力は、マッチしたすべての単語のリストになります。

['cat', 'hat', 'sat', 'lat', 'mat']

高度なメソッドとフラグ

この最終ステップでは、正規表現の動作を変更し、さらに多用途にするための強力なメソッドとフラグについて説明します。

ドットスター .* によるすべての一致

組み合わせ .* は一般的なパターンで、任意の文字(.)がゼロ回以上(*)繰り返されることにマッチします。これは、任意の文字列シーケンスにマッチするために使用されます。デフォルトでは貪欲(greedy)であり、可能な限り多くのテキストにマッチしようとします。

import re

## The pattern (.*) captures everything between 'First Name: ' and ' Last Name: '.
name_regex = re.compile(r'First Name: (.*) Last Name: (.*)')
mo = name_regex.search('First Name: Al Last Name: Sweigart')
print(f"First Name: {mo.group(1)}")
print(f"Last Name: {mo.group(2)}")

非貪欲にするには、.*? を使用します。これは可能な限り短い文字列にマッチします。

re.DOTALL による改行の一致

デフォルトでは、ドット . 文字は改行文字(\n)にマッチしません。これを変更するには、re.compile() の 2 番目の引数として re.DOTALL を渡すことができます。このフラグにより、. は改行を含むすべての文字にマッチできるようになります。

import re

## With re.DOTALL, '.*' will match across multiple lines.
newline_regex = re.compile('.*', re.DOTALL)
mo = newline_regex.search('Serve the public trust.\nProtect the innocent.\nUphold the law.')
print(mo.group())

re.IGNORECASE による大文字・小文字を区別しないマッチング

正規表現で大文字・小文字を無視したい場合(例:「cat」、「Cat」、「CAT」にマッチさせたい場合)、re.compile() の 2 番目の引数として re.IGNORECASE またはその短縮形 re.I を渡します。

import re

## re.I makes the pattern 'robocop' match 'Robocop', 'ROBOCOP', etc.
robocop = re.compile(r'robocop', re.I)
match = robocop.search('Robocop is part man, part machine, all cop.')
print(match.group())

sub() メソッドによる文字列の置換

sub() メソッドは、パターンのすべての一致を見つけ、指定された文字列に置き換えます。このメソッドは、置換文字列と検索対象の文字列の 2 つの引数を取ります。

エージェント名を「CENSORED」に置き換えてみましょう。main.py を更新します。

import re

## The pattern 'Agent \w+' matches 'Agent ' followed by one or more word characters.
names_regex = re.compile(r'Agent \w+')
result = names_regex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
print(result)

スクリプトを実行します。

python3 main.py

出力には置換された文字列が表示されます。

CENSORED gave the secret documents to CENSORED.

re.VERBOSE による複雑な正規表現の管理

正規表現パターンが複雑になると、読みづらくなることがあります。re.VERBOSE を re.compile() に渡すことで、正規表現を複数行にわたって記述し、異なる部分を説明するコメントを追加できます。文字列内の空白文字とコメント(#)は無視されます。

この複雑な正規表現: phone_regex = re.compile(r'((\d{3}|\(\d{3}\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4}(\s*(ext|x|ext.)\s*\d{2,5})?)')

re.VERBOSE を使用すると、次のように読みやすいバージョンになります。

import re

phone_regex = re.compile(r'''(
    (\d{3} | \(\d{3}\))?          ## area code (optional)
    (\s | - | \.)?                ## separator (optional)
    \d{3}                         ## first 3 digits
    (\s | - | \.)                 ## separator
    \d{4}                         ## last 4 digits
    (\s*(ext|x|ext.)\s*\d{2,5})?  ## extension (optional)
    )''', re.VERBOSE)

mo = phone_regex.search("My phone number is 415-555-4242 ext. 55")
if mo:
    print(f"Found: {mo.group()}")

これにより、コードの保守性が大幅に向上します。

まとめ

この実験(Lab)では、Python の re モジュールを使用して、正規表現の基礎を習得しました。re.compile() でパターンをコンパイルし、search() でマッチを見つけ、group() でテキストを取得するという完全なワークフローを学びました。

以下の主要な概念を探求しました。

  • マッチの特定の部分をキャプチャするための () を使用したグループ化(Grouping)。
  • 複数のパターンのいずれかにマッチさせるための | を使用した選択(Alternation)。
  • 反復を制御するための ?、*、+、{} などの量指定子(Quantifiers)。
  • マッチの長さを制御するための貪欲(Greedy)と非貪欲(Non-greedy)マッチング。
  • すべてのマッチを取得するための findall()、カスタムセットのための文字クラス []、文字列の境界にマッチを固定するためのアンカー ^ および $ などの高度なツール。
  • マルチラインマッチングのための re.DOTALL、大文字・小文字を区別しないマッチングのための re.IGNORECASE、およびクリーンでコメント付きの正規表現を記述するための re.VERBOSE などの強力なフラグ。
  • sub() メソッドによる文字列置換。

これらのスキルにより、簡単なデータ検証から複雑なデータ抽出および操作に至るまで、幅広いテキスト処理タスクに対応できるようになりました。