note.nkmk.me

Pythonの正規表現モジュールreの使い方(match、search、subなど)

Date: 2017-09-05 / Modified: 2019-06-09 / tags: Python, 文字列操作, 正規表現

Pythonで正規表現の処理を行うには標準ライブラリのreモジュールを使う。正規表現パターンによる文字列の抽出や置換、分割などができる。

ここではまずreモジュールの関数やメソッドについて説明する。

  • 正規表現パターンをコンパイル: compile()
  • マッチオブジェクト
  • 文字列の先頭がマッチするかチェック、抽出: match()
  • 先頭に限らずマッチするかチェック、抽出: search()
  • 文字列全体がマッチするかチェック: fullmatch()
  • マッチする部分すべてをリストで取得: findall()
  • マッチする部分すべてをイテレータで取得: finditer()
  • マッチする部分を置換: sub(), subn()
  • 正規表現パターンで文字列を分割: split()

そのあとで、reモジュールで使える正規表現のメタ文字(特殊文字)・特殊シーケンスについて説明する。基本的には標準的な正規表現のシンタックスだが、フラグの設定(特にre.ASCII)は要注意。

  • Pythonでの正規表現のメタ文字・特殊シーケンスと注意点
  • フラグの設定
    • ASCII文字に限定: re.ASCII
    • 大文字小文字を区別しない: re.IGNORECASE
    • 各行の先頭・末尾にマッチ: re.MULTILINE
    • 複数のフラグを指定
  • 貪欲マッチと非貪欲マッチ
スポンサーリンク

正規表現パターンをコンパイル: compile()

reモジュールで正規表現の処理を実行する方法は2つある。

関数で実行

1つ目は関数。re.match(), re.sub()のように正規表現パターンを用いた抽出や置換などの処理を行う関数が用意されている。

関数の詳細については後述するが、いずれも第一引数に正規表現パターンの文字列を指定し、その後に処理する文字列などを指定するようになっている。例えば、置換を行うre.sub()では第二引数に置換文字列、第三引数に処理対象の文字列を指定する。

import re

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

m = re.match(r'([a-z]+)@([a-z]+)\.com', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

result = re.sub(r'([a-z]+)@([a-z]+)\.com', 'new-address', s)
print(result)
# new-address, new-address, ccc@zzz.net

ちなみに、この例の正規表現パターンの[a-z]aからzまでのいずれかの文字(=アルファベットの小文字)、+は直前のパターン(ここでは[a-z])を1回以上繰り返す、という意味。[a-z]+は小文字のアルファベットが1文字以上繰り返される文字列にマッチする。

.はメタ文字(特別な意味を持つ文字)なので\でエスケープする必要がある。

正規表現パターンの文字列はバックスラッシュ\を多用する場合が多いので、例のようにraw文字列を使うと便利。

正規表現パターンオブジェクトのメソッドで実行

reモジュールで正規表現の処理を行う方法の2つ目は正規表現パターンオブジェクトのメソッド。

re.compile()を使うと、正規表現パターン文字列をコンパイルして正規表現パターンオブジェクトを作成できる。

p = re.compile(r'([a-z]+)@([a-z]+)\.com')

print(p)
# re.compile('([a-z]+)@([a-z]+)\\.com')

print(type(p))
# <class 're.Pattern'>

re.match(), re.sub()などの関数と同様の処理が、正規表現オブジェクトのメソッドmatch(), sub()として実行できる。

m = p.match(s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

result = p.sub('new-address', s)
print(result)
# new-address, new-address, ccc@zzz.net

以降で説明するre.xxx()の関数はすべて正規表現オブジェクトのメソッドとしても提供されている。

同じパターンを使う処理を繰り返し行う場合は、re.compile()で正規表現オブジェクトを生成して使い回すほうが効率的。

以降のサンプルコードでは便宜上コンパイルせずに関数を使っているが、同じパターンを繰り返し使う場合は、前もってコンパイルして正規表現オブジェクトのメソッドとして実行することをおすすめする。

マッチオブジェクト

match()search()などはマッチオブジェクトを返す。

s = 'aaa@xxx.com'

m = re.match(r'[a-z]+@[a-z]+\.[a-z]+', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

print(type(m))
# <class 're.Match'>

マッチした文字列や位置はマッチオブジェクトの以下のメソッドを使って取得する。

  • マッチした位置を取得: start(), end(), span()
  • マッチした文字列を取得: group()
  • 各グループの文字列を取得: groups()

マッチした部分の位置はstart(), end(), span()、文字列はgroup()

print(m.start())
# 0

print(m.end())
# 11

print(m.span())
# (0, 11)

print(m.group())
# aaa@xxx.com

正規表現パターンの文字列中の部分を括弧()で囲むと、その部分がグループとして処理される。このとき、groups()で各グループにマッチした部分の文字列がタプルとして取得できる。

m = re.match(r'([a-z]+)@([a-z]+)\.([a-z]+)', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

print(m.groups())
# ('aaa', 'xxx', 'com')

グループに名前を設定した場合の振る舞いや、if文での使い方など、マッチオブジェクトの詳細は以下の記事を参照。

文字列の先頭がマッチするかチェック、抽出: match()

match()は文字列の先頭がパターンにマッチするとマッチオブジェクトを返す。

上述のように、マッチオブジェクトを使ってマッチした部分文字列を抽出したり、単純にマッチしたかどうかをチェックしたりできる。

match()が調べるのはあくまでも先頭のみ。先頭にマッチする文字列がない場合はNoneを返す。

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

m = re.match(r'[a-z]+@[a-z]+\.com', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

m = re.match(r'[a-z]+@[a-z]+\.net', s)
print(m)
# None

search()は文字列すべてが検索対象で、先頭にない文字列にもマッチする。match()と同じく、マッチする場合はマッチオブジェクトを返す。

マッチする部分が複数ある場合は、最初のマッチ部分のみが返される。

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

m = re.search(r'[a-z]+@[a-z]+\.net', s)
print(m)
# <re.Match object; span=(26, 37), match='ccc@zzz.net'>

m = re.search(r'[a-z]+@[a-z]+\.com', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

マッチする部分をすべて取得したい場合は後述のfindall()またはfinditer()を使う。

文字列全体がマッチするかチェック: fullmatch()

文字列全体が正規表現パターンにマッチしているかどうかの確認にはfullmatch()を使う。例えば、ある文字列がメールアドレスとして有効かどうかなどをチェックする場合に便利。

文字列全体がマッチしているとマッチオブジェクトが返される。

s = 'aaa@xxx.com'

m = re.fullmatch(r'[a-z]+@[a-z]+\.com', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

マッチしていない部分がある(一部しかマッチしていない、または、全くマッチしていない)とNoneが返される。

s = '!!!aaa@xxx.com!!!'

m = re.fullmatch(r'[a-z]+@[a-z]+\.com', s)
print(m)
# None

fullmatch()はPython3.4で追加された。それより前のバージョンで同様の処理を行いたい場合は、match()と末尾にマッチするメタ文字$を利用する。先頭から末尾まで文字列全体がマッチしていないとNoneを返す。

s = '!!!aaa@xxx.com!!!'

m = re.match(r'[a-z]+@[a-z]+\.com$', s)
print(m)
# None

マッチする部分すべてをリストで取得: findall()

findall()はマッチするすべての部分文字列をリストにして返す。リストの要素はマッチオブジェクトではなく文字列なので注意。

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

result = re.findall(r'[a-z]+@[a-z]+\.[a-z]+', s)
print(result)
# ['aaa@xxx.com', 'bbb@yyy.com', 'ccc@zzz.net']

マッチした部分が何個あるかは、リストの要素数を返す組み込み関数len()を使って確認できる。

print(len(result))
# 3

正規表現パターンで括弧()を使ってグルーピングすると、各グループの文字列を要素とするタプル(マッチオブジェクトのgroups()に相当)のリストが返される。

result = re.findall(r'([a-z]+)@([a-z]+)\.([a-z]+)', s)
print(result)
# [('aaa', 'xxx', 'com'), ('bbb', 'yyy', 'com'), ('ccc', 'zzz', 'net')]

グループの括弧()は入れ子状に設定できるので、マッチ全体も合わせて取得したい場合は全体を括弧()で囲めばよい。

result = re.findall(r'(([a-z]+)@([a-z]+)\.([a-z]+))', s)
print(result)
# [('aaa@xxx.com', 'aaa', 'xxx', 'com'), ('bbb@yyy.com', 'bbb', 'yyy', 'com'), ('ccc@zzz.net', 'ccc', 'zzz', 'net')]

マッチしない場合は空のタプルを返す。

result = re.findall('[0-9]+', s)
print(result)
# []

マッチする部分すべてをイテレータで取得: finditer()

finditer()はマッチするすべての部分をイテレータで返す。その要素はfindall()のように文字列ではなくマッチオブジェクトなので、マッチした部分の位置(インデックス)なども取得できる。

イテレータはそれ自体をprint()で出力しても中身は得られない。組み込み関数next()やfor文を使うと中身が一つずつ取り出せる。

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

result = re.finditer(r'[a-z]+@[a-z]+\.[a-z]+', s)
print(result)
# <callable_iterator object at 0x10b0efa90>

print(type(result))
# <class 'callable_iterator'>

for m in result:
    print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>
# <re.Match object; span=(13, 24), match='bbb@yyy.com'>
# <re.Match object; span=(26, 37), match='ccc@zzz.net'>

list()でリストに変換することも可能。

l = list(re.finditer(r'[a-z]+@[a-z]+\.[a-z]+', s))
print(l)
# [<re.Match object; span=(0, 11), match='aaa@xxx.com'>, <re.Match object; span=(13, 24), match='bbb@yyy.com'>, <re.Match object; span=(26, 37), match='ccc@zzz.net'>]

print(l[0])
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

print(type(l[0]))
# <class 're.Match'>

print(l[0].span())
# (0, 11)

マッチするすべての部分の位置を取得したいといった場合は、list()よりもリスト内包表記のほうが便利。

print([m.span() for m in re.finditer(r'[a-z]+@[a-z]+\.[a-z]+', s)])
# [(0, 11), (13, 24), (26, 37)]

イテレータは順番に要素を取り出していく。最後まで到達した後でさらに要素を取り出そうとすると何も残っていない状態になるので注意。

result = re.finditer(r'[a-z]+@[a-z]+\.[a-z]+', s)

for m in result:
    print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>
# <re.Match object; span=(13, 24), match='bbb@yyy.com'>
# <re.Match object; span=(26, 37), match='ccc@zzz.net'>

print(list(result))
# []

マッチする部分を置換: sub(), subn()

sub()を使うと、マッチした部分を他の文字列に置換することができる。置換処理された文字列が返される。

s = 'aaa@xxx.com, bbb@yyy.com, ccc@zzz.net'

result = re.sub(r'[a-z]+@[a-z]+\.com', 'new-address', s)
print(result)
# new-address, new-address, ccc@zzz.net

print(type(result))
# <class 'str'>

括弧()でグルーピングした場合、置換後の文字列の中でマッチした文字列を使用することができる。

デフォルトでは\1, \2, \3...が、それぞれ1つ目の()、2つ目の()、3つ目の()...にマッチした部分に対応している。raw文字列ではない通常の文字列だと'\\1'のように\をエスケープする必要があるので注意。

result = re.sub(r'([a-z]+)@([a-z]+)\.com', r'\1@\2.net', s)
print(result)
# aaa@xxx.net, bbb@yyy.net, ccc@zzz.net

正規表現パターンの()の先頭に?P<xxx>を記述してグループに名前をつけると、\1のような番号ではなく\g<xxx>のように名前を使って指定できる。

result = re.sub(r'(?P<local>[a-z]+)@(?P<SLD>[a-z]+)\.com', r'\g<local>@\g<SLD>.net', s)
print(result)
# aaa@xxx.net, bbb@yyy.net, ccc@zzz.net

引数countで最大置換回数(個数)を指定できる。左側からcount個の部分のみ置換される。

result = re.sub(r'[a-z]+@[a-z]+\.com', 'new-address', s, count=1)
print(result)
# new-address, bbb@yyy.com, ccc@zzz.net

subn()は置換処理された文字列(sub()の返り値と同じ)と置換された部分の個数(パターンにマッチした個数)とのタプルを返す。

result = re.subn(r'[a-z]+@[a-z]+\.com', 'new-address', s)
print(result)
# ('new-address, new-address, ccc@zzz.net', 2)

引数の指定方法などはsub()と同じ。()でグルーピングした部分を使ったり、引数countを指定したりできる。

result = re.subn(r'(?P<local>[a-z]+)@(?P<SLD>[a-z]+)\.com', r'\g<local>@\g<SLD>.net', s)
print(result)
# ('aaa@xxx.net, bbb@yyy.net, ccc@zzz.net', 2)

result = re.subn(r'[a-z]+@[a-z]+\.com', 'new-address', s, count=1)
print(result)
# ('new-address, bbb@yyy.com, ccc@zzz.net', 1)

文字列の置換についての詳細は以下の記事を参照。

正規表現パターンで文字列を分割: split()

split()はパターンにマッチした部分で文字列を分割し、リストにして返す。

先頭・末尾にマッチする場合、結果のリストの最初と最後に空文字列''が含まれるので注意。

s = '111aaa222bbb333'

result = re.split('[a-z]+', s)
print(result)
# ['111', '222', '333']

result = re.split('[0-9]+', s)
print(result)
# ['', 'aaa', 'bbb', '']
source: re_split.py

引数maxsplitで最大分割回数(個数)を指定できる。左側からcount個の部分でのみ分割される。

result = re.split('[a-z]+', s, 1)
print(result)
# ['111', '222bbb333']
source: re_split.py

文字列の分割についての詳細は以下の記事を参照。

Pythonでの正規表現のメタ文字・特殊シーケンスと注意点

Python3のreモジュールで使える正規表現のメタ文字(特殊文字)・特殊シーケンスの主なものは以下の通り。

メタ文字 内容
. 改行以外の任意の1文字(DOTALLフラグで改行も含む)
^ 文字列の先頭(MULTILINEフラグで各行の先頭にもマッチ)
$ 文字列の末尾(MULTILINEフラグで各行の末尾にもマッチ)
* 直前のパターンを0回以上繰り返し
+ 直前のパターンを1回以上繰り返し
? 直前のパターンを0回または1回繰り返し
{m} 直前のパターンをm回繰り返し
{m, n} 直前のパターンをmn回繰り返し
[] 文字の集合 - []内のいずれか1文字にマッチ
| OR(または) - A|BABいずれかのパターンにマッチ
特殊シーケンス 内容
\d Unicode10進数字(ASCIIフラグでASCIIの数字に限定)
\D \dの反対(\d以外)
\s Unicode空白文字(ASCIIフラグでASCIIの空白文字に限定)
\S \sの反対(\s以外)
\w Unicode単語文字と_ASCIIフラグでASCIIの英字と_に限定)
\W \wの反対(\w以外)

この表に挙げたものが全てではない。完全なリストは公式ドキュメントを参照。

また、Python2では意味が異なるものもあるので注意。

基本的なものを使ったシンプルな活用例を以下の記事で紹介している。

フラグの設定

上の表でも示した通り、メタ文字・特殊シーケンスの中にはフラグによってモードが変わるものがある。

ここでは主なフラグのみを取り上げる。そのほかは公式ドキュメントを参照。

ASCII文字に限定: re.ASCII

Python3の文字列に対しては、\wはデフォルトで全角の日本語や英数字などにもマッチする。標準的な正規表現とは異なり\w[a-zA-Z0-9_]は等価ではない。

m = re.match(r'\w+', 'あいう漢字ABC123')
print(m)
# <re.Match object; span=(0, 11), match='あいう漢字ABC123'>

m = re.match('[a-zA-Z0-9_]+', 'あいう漢字ABC123')
print(m)
# None
source: re_flag.py

各関数で引数flagsre.ASCIIを指定するか、正規表現パターンの文字列の先頭にインラインフラグ(?a)をつけると、ASCII文字にのみマッチするようになる(全角の日本語や英数字などにはマッチしない)。この場合は\w[a-zA-Z0-9_]と等価。

m = re.match(r'\w+', 'あいう漢字ABC123', flags=re.ASCII)
print(m)
# None

m = re.match(r'(?a)\w+', 'あいう漢字ABC123')
print(m)
# None
source: re_flag.py

re.compile()でコンパイルする場合も同様。引数flagsかインラインフラグ(?a)を使う。

p = re.compile(r'\w+', flags=re.ASCII)
print(p)
# re.compile('\\w+', re.ASCII)

print(p.match('あいう漢字ABC123'))
# None

p = re.compile(r'(?a)\w+')
print(p)
# re.compile('(?a)\\w+', re.ASCII)

print(p.match('あいう漢字ABC123'))
# None
source: re_flag.py

またre.ASCIIは短縮形re.Aとしても提供されている。どちらを使ってもよい。

print(re.ASCII is re.A)
# True
source: re_flag.py

\wの反対を表す\Wre.ASCII(?a)の影響を受ける。

m = re.match(r'\W+', 'あいう漢字ABC123')
print(m)
# None

m = re.match(r'\W+', 'あいう漢字ABC123', flags=re.ASCII)
print(m)
# <re.Match object; span=(0, 11), match='あいう漢字ABC123'>
source: re_flag.py

\wと同様に、数字にマッチする\d、空白にマッチする\sも、デフォルトでは半角にも全角にもマッチする。re.ASCII(?a)を指定すると半角のみに限定される。

m = re.match(r'\d+', '123')
print(m)
# <re.Match object; span=(0, 3), match='123'>

m = re.match(r'\d+', '123')
print(m)
# <re.Match object; span=(0, 3), match='123'>

m = re.match(r'\d+', '123', flags=re.ASCII)
print(m)
# <re.Match object; span=(0, 3), match='123'>

m = re.match(r'\d+', '123', flags=re.ASCII)
print(m)
# None

m = re.match(r'\s+', ' ')  # 全角スペース
print(m)
# <re.Match object; span=(0, 1), match='\u3000'>

m = re.match(r'\s+', ' ', flags=re.ASCII)
print(m)
# None
source: re_flag.py

それらの反対、\D, \Sre.ASCII(?a)の影響を受ける。

大文字小文字を区別しない: re.IGNORECASE

デフォルトでは大文字小文字が区別される。両方にマッチさせるには大文字と小文字の両方をパターンに入れる必要がある。

re.IGNORECASE]を指定すると大文字小文字を区別せずにマッチする。標準的な正規表現のiフラグに相当。

m = re.match('[a-zA-Z]+', 'abcABC')
print(m)
# <re.Match object; span=(0, 6), match='abcABC'>

m = re.match('[a-z]+', 'abcABC', flags=re.IGNORECASE)
print(m)
# <re.Match object; span=(0, 6), match='abcABC'>

m = re.match('[A-Z]+', 'abcABC', flags=re.IGNORECASE)
print(m)
# <re.Match object; span=(0, 6), match='abcABC'>
source: re_flag.py

インラインフラグ(?i)、または、短縮形のre.IでもOK。

各行の先頭・末尾にマッチ: re.MULTILINE

正規表現のメタ文字^は文字列の先頭にマッチする。

デフォルトでは文字列全体の先頭のみにマッチするが、re.MULTILINEを指定すると各行の先頭にもマッチするようになる。標準的な正規表現のmフラグに相当。

s = '''aaa-xxx
bbb-yyy
ccc-zzz'''

print(s)
# aaa-xxx
# bbb-yyy
# ccc-zzz

result = re.findall('[a-z]+', s)
print(result)
# ['aaa', 'xxx', 'bbb', 'yyy', 'ccc', 'zzz']

result = re.findall('^[a-z]+', s)
print(result)
# ['aaa']

result = re.findall('^[a-z]+', s, flags=re.MULTILINE)
print(result)
# ['aaa', 'bbb', 'ccc']
source: re_flag.py

末尾にマッチする$も同様。デフォルトでは文字列全体の末尾のみにマッチ、re.MULTILINEを指定すると各行の末尾にもマッチするようになる。

result = re.findall('[a-z]+$', s)
print(result)
# ['zzz']

result = re.findall('[a-z]+$', s, flags=re.MULTILINE)
print(result)
# ['xxx', 'yyy', 'zzz']
source: re_flag.py

インラインフラグ(?m)、または、短縮形のre.MでもOK。

複数のフラグを指定

複数のフラグを同時に有効にしたい場合は|を使う。インラインフラグの場合は(?am)のように各文字を続けて記述する。

s = '''aaa-xxx
あああ-んんん
bbb-zzz'''

print(s)
# aaa-xxx
# あああ-んんん
# bbb-zzz

result = re.findall(r'^\w+', s, flags=re.M)
print(result)
# ['aaa', 'あああ', 'bbb']

result = re.findall(r'^\w+', s, flags=re.M | re.A)
print(result)
# ['aaa', 'bbb']

result = re.findall(r'(?am)^\w+', s)
print(result)
# ['aaa', 'bbb']
source: re_flag.py

貪欲マッチと非貪欲マッチ

これは正規表現の一般的な問題でPythonだけの問題ではないが、ハマりがちなので書いておく。

デフォルトでは*, +, ?は貪欲(greedy)マッチで、できる限り長い文字列にマッチする。

s = 'aaa@xxx.com, bbb@yyy.com'

m = re.match(r'.+com', s)
print(m)
# <re.Match object; span=(0, 24), match='aaa@xxx.com, bbb@yyy.com'>

print(m.group())
# aaa@xxx.com, bbb@yyy.com
source: re_greedy.py

?を後ろにつける(*?, +?, ??)と、非貪欲(non-greedy)、最小(minimal)のマッチとなり、できる限り短い文字列にマッチする。

m = re.match(r'.+?com', s)
print(m)
# <re.Match object; span=(0, 11), match='aaa@xxx.com'>

print(m.group())
# aaa@xxx.com
source: re_greedy.py

デフォルトの貪欲マッチだと思わぬ文字列にマッチする場合があるので要注意。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事