LoginSignup
54
52

More than 1 year has passed since last update.

ゼロから覚えるPython正規表現の基本とTips

Last updated at Posted at 2020-02-03

Pythonの正規表現についてです。今まで必要なときにググって調べて実装していましたが、そろそろ理解を深めようと思いまとめてみました。「必要なときにググって調べて実装」なんて偉そうなことを言っていますが、初心者レベルです。初心者がゼロから覚えたほうがいい内容、およびたまに使う人が学習し直す内容を意識してい書いています。
当記事では言語処理100本ノック 2015「第3章: 正規表現」で学習したことを整理しています。

Python実行が面倒なときは、以下ツールでチェックしています(便利)。

参考リンク

リンク 備考
正規表現 HOWTO Python公式の正規表現 How To
re --- 正規表現操作 Python公式のreパッケージ説明

基本

Pythonではパッケージreを使って正規表現の実装します。以後のPython文内ではimport reを省略します。

import re

2種類の使い方

1. 関数を直接使用

re.matchre.subなどの関数を使います。

# 第1引数が正規表現パターン(検索語句)、第2引数が検索対象
result = re.match('Hel', 'Hellow python')

print(result)
# <_sre.SRE_Match object; span=(0, 3), match='Hel'>

print(result.group())
# Hel

2. コンパイルして使用

正規表現パターンをコンパイルした後にmatchsubなどの関数を使用します。

# あらかじめ正規表現パターンをコンパイル
regex = re.compile('Hel')

result = regex.match('Hellow python')

print(result)
# <_sre.SRE_Match object; span=(0, 3), match='Hel'>

print(result.group())
# Hel

2種類の使い分け

複数の正規表現パターンを何度も使う場合はコンパイル方式を使います公式に以下の記載があります。

re.compile() を使い、結果の正規表現オブジェクトを保存して再利用するほうが、一つのプログラムでその表現を何回も使うときに効率的です。
re.compile() やモジュールレベルのマッチング関数に渡された最新のパターンはコンパイル済みのものがキャッシュされるので、一度に正規表現を少ししか使わないプログラムでは正規表現をコンパイルする必要はありません。

同じ正規表現パターンを何度も使う場合はコンパイルしても速さのメリットはないようです。どれだけキャッシュされるのかは調べていません。

正規表現パターン(検索語句)の定義

raw文字列でエスケープシーケンス無効

raw文字列(raw string)は正規表現固有のトピックではありませんが、使うことでエスケープシーケンスを無効にできます

以下の例の前者だと\tはタブ、\nが改行となるが、後者だとそのまま\t,\n文字列として扱われる

print('a\tb\nA\tB')
print(r'a\tb\nA\tB')
ターミナル出力結果
a	b
A	B

a\tb\nA\tB

正規表現パターン内でバックスラッシュなどに対してエスケープシーケンスを書きたくないのでraw文字列を使います

result = re.match(r'\d', '329')

記事「Python の raw 文字列を用いて正規表現を書く」「Pythonでエスケープシーケンスを無視(無効化)するraw文字列」に詳しい解説があります。

トリプルクォートとre.VERBOSEで改行・コメント・空白無視

'''トリプルクォート("""でも可能)で囲むことにより、正規表現パターン中に改行を使うことができます(改行がなくても問題なし)。
re.VERBOSEを渡すことで、空白やコメントを正規表現パターンから除外できます。
リプルクォートとre.VERBOSEで非常に読みやすくなります
以下のような正規表現パターンを記述すると見やすいですね。

a = re.compile(r'''\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits''', re.VERBOSE)

トリプルクォートに関しては、記事「Pythonで文字列生成(引用符、strコンストラクタ)」に詳しくかかれています。

ちなみにcompileのパラメータflagsで複数のコンパイルフラグを使いたい場合は、単純に+(加算)してやればOKです。

a = re.compile(r'''\d''', re.VERBOSE+re.MULTILINE)

特殊文字

文字 説明 備考 マッチする マッチしない
\d 数字 [0-9]と同じ
\D 数字以外 [^0-9]と同じ
\s 空白文字 [\t\n\r\f\v]と同じ
\S 空白文字以外 [^\t\n\r\f\v]と同じ
\w 英数文字と下線 [a-zA-Z0-9_]と同じ
\W 英数文字以外 [\a-zA-Z0-9_]と同じ
\A 文字列の先頭 ^と類似
\Z 文字列の末尾 $と類似
\b 単語の境界(スペース)
. 任意の一文字 - 1.3 123, 133 1223
^ 文字列の先頭 - ^123 1234 0123
$ 文字列の末尾 - 123$ 0123 1234
* 0回以上の繰り返し - 12* 1, 12, 122 11, 22
+ 1回以上の繰り返し - 12+ 12, 122 1, 11, 22
? 0回または1回 - 12? 1, 12 122
{m} m回の繰り返し - 1{3} 111 11, 1111
{m,n} m〜n回の繰り返し - 1{2, 3} 11, 111 1, 1111
[] 集合 [^5]とすると5以外 [1-3] 1, 2, 3 4, 5
| 和集合(or) - 1|2 1, 2 3
() グループ化 - (12)+ 12, 1212 1, 123

マッチ関数

以下の関数をよく使います。

関数 目的
match 文字列の先頭で正規表現とマッチするか判定
search 正規表現がどこにマッチするか検索
findall マッチする部分文字列を全て探しリストとして返します
sub 文字列置換

matchsearch

文字列の先頭でのみのマッチするのがre.matchで文字列中の位置にかかわらずマッチするのがre.search。詳しくは公式の「search() vs. match()」を参照。両者とも最初のパターンのみを返します(2回目以降にマッチしたものを返さない)。

>>> re.match("c", "abcdef")    # 先頭が"c"でないのでマッチしない
>>> re.search("c", "abcdef")   # マッチする
<re.Match object; span=(2, 3), match='c'>

結果はgroupに入っています。group(0)に結果がすべて入っていて、グループ化した検索結果は連番で1から入っています。

>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m.group(0)       # The entire match
'Isaac Newton'
>>> m.group(1)       # The first parenthesized subgroup.
'Isaac'
>>> m.group(2)       # The second parenthesized subgroup.
'Newton'
>>> m.group(1, 2)    # Multiple arguments give us a tuple.
('Isaac', 'Newton')

findall

パターンにマッチした全ての文字列をリスト形式で返すのがfindall

>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']

()を使ってキャプチャ対象を指定できますが、複数指定した場合は以下のようになります。グループごとにタプルで返ってきます。

>>> print(re.findall(r'''(1st)(2nd)''', '1st2nd1st2nd'))
[('1st', '2nd'), ('1st', '2nd')]

sub

文字置換をします。引数の順に1. 正規表現パターン、2. 置換後の文字列、3. 置換対象文字列です。

>>> re.sub(r'置換前', '置換後', '置換前 対象外 置換前')
'置換後 対象外 置換後'

※ちなみに4つ目の引数はcount, 5つ目はflags(コンパイルフラグ)です。4つ目に気づかずに必死でコンパイルフラグを渡した気になっていて、うまく行かずに30分ほど無駄にしたことがありました・・・

コンパイルフラグ

コンパイルフラグでよく使うものは以下のあたり。関数のパラメータflagsに渡します。

フラグ 意味
DOTALL .を改行を含む任意の文字に設定
IGNORECASE 大文字小文字を区別しない
MULTILINE ^$で複数行文字列に対するマッチングを行います。
VERBOSE 正規表現内のコメントと空白無視

既に説明したVERBOSEとわかりにくいやつと以外を少し詳しく解説します。

DOTALL

re.DOTALLはワイルドカードである.(DOT)に対して改行を含めるオプションです。

string = r'''\
行頭 1st line
行頭 2nd line'''

print(re.findall(r'1st.*2nd', string, re.DOTALL))
# ['1st line\n行頭 2nd']

print(re.findall(r'1st.*2nd', string))
# No Match

詳細は記事「Python: 正規表現で複数行マッチングの置換を行う」参照。

MULTILINE

複数行に対してそれぞれ検索したい場合に使います。以下の例だとre.MULTILINEを使うと2行目(「行頭 2nd line」)も対象となります。
match関数の場合は、re.MULTILINEを使っても意味ありません

string = r'''\
行頭 1st line
行頭 2nd line'''

print(re.findall(r'^行頭.*', string, re.MULTILINE))
# ['行頭 1st line', '行頭 2nd line']

print(re.findall(r'^行頭.*', string))
# ['行頭 1st line']

詳細は記事「Python: 正規表現で複数行マッチングの置換を行う」参照。

Tips

キャプチャ対象外

(?:...)をつけると検索結果文字列に含めずキャプチャ対象外となります。公式の正規表現のシンタックスでは以下のように説明があります。

普通の丸括弧の、キャプチャしない版です。丸括弧で囲まれた正規表現にマッチしますが、このグループがマッチした部分文字列は、マッチを実行したあとで回収することも、そのパターン中で以降参照することも できません 。

以下の例では、4の部分を正規表現パターンとはしていますが、結果には出力していません。

>>> re.findall(r'(.012)(?:4)', 'A0123 B0124 C0123')
['B012']

貪欲・非貪欲マッチ

検索結果対象文字列の長さをコントロールすることができます。最大限の長さでマッチさせるのが貪欲マッチ(greedy match)で最小限の長さでマッチさせるのが非貪欲マッチ(Non-greedy match)です。
デフォルトは貪欲マッチで、非貪欲マッチにするためには連続系特殊文字(*, ?, +)に?をくっつけます。以下が両者の例文です。

# 貪欲マッチ
>>> print(re.findall(r'.0.*2',  'A0123 B0123'))
['A0123 B012']

# 非貪欲マッチ(*の後に?)
>>> print(re.findall(r'.0.*?2', 'A0123 B0123'))
['A012', 'B012']

詳細は記事「貪欲マッチと非貪欲マッチ」を参照ください。

後方参照

\numberを使うことで前のグループの中身にマッチさせることができます。公式シンタックスでは下記の記述。

同じ番号のグループの中身にマッチします。グループは 1 から始まる番号をつけられます。例えば、 (.+) \1 は、 'the the' あるいは '55 55' にマッチしますが、 'thethe' にはマッチしません(グループの後のスペースに注意して下さい)。この特殊シーケンスは最初の 99 グループのうちの一つとのマッチにのみ使えます。 number の最初の桁が 0 であるか、 number が 3 桁の 8 進数であれば、それはグループのマッチとしてではなく、 8 進値 number を持つ文字として解釈されます。文字クラスの '[' と ']' の間では全ての数値エスケープが文字として扱われます。

具体的にはこんな感じで、\1部分は前の(ab)でマッチした部分と同じ意味でabcabはマッチしますが、abdddは4文字目と5文字目がabではないのでマッチしません。
※Pythonのリストなどは0からカウントしますが、正規表現では1からカウントです。

>>> print(re.findall(r'''(ab).\1''', 'abcab abddd'))
['ab']

先読み・後読みアサーション

マッチ対象には含めないけど、検索条件に文字列を含める・含めないという使い方に対して以下の4つがあります。

  • 肯定の先読みアサーション(Positive Lookahead Assertions)
  • 否定の先読みアサーション(Negative Lookahead Assertions)
  • 肯定の後読みアサーション(Positive Lookbehind Assertions)
  • 否定の後読みアサーション(Negative Lookbehind Assertions)

マトリックスにすると以下の形。

肯定 否定
先読み (?=...)
...部分が次に続けばマッチ
(?!...)
...部分が次に続かなければマッチ
後読み (?<=...)
...部分が現在位置より前でマッチがあればマッチ
(?<!...)
...部分が現在位置より前でマッチがなければマッチ

細かい説明より、具体例の方がわかりやすいです。

>>> string = 'A01234 B91235 C01234'

# 肯定の先読みアサーション(Positive Lookahead Assertions)
# '123'の次に'5'が続く文字列('(?=5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?=5).', string))
['B91235']

# 否定の先読みアサーション(Negative Lookahead Assertions)
# '123'の次に'5'が続かない文字列('(?!5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?!5).', string))
['A01234', 'C01234']

# 肯定の後読みアサーション(Positive Lookbehind Assertions)
# '0'が'123'の前にマッチする文字列('(?<=0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<=0)123', string))
['A0123', 'C0123']

# 否定の後読みアサーション(Negative Lookbehind Assertions)
# '0'が'123'の前にマッチしない文字列('(?<!0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<!0)123', string))
['B9123']
54
52
0

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
54
52