プログラミング

正規表現の基本構文とパターン検索(Python)

文字列の中から 「数字だけ」 「特定の形をした部分だけ」 を取り出したり、「入力が決まった形式か」を判定したり、こうした文字の並びのパターンを扱う道具が正規表現です。本記事では、Pythonで正規表現を使うための基本構文を、ひとつずつ例とともに整理しています。

使うのは標準ライブラリのreモジュールだけで、追加インストールは不要です。

動作確認

Python3.12で実施しています。

目次

はじめに

正規表現とは

正規表現(regular expression、略してregex)は、文字列パターンを記号で表す書き方です。たとえば\d+は「1個以上の数字」を意味します。「探したいもののパターンを記号で書き、その形に当てはまる箇所を取り出す」のが基本的な発想です。Pythonでは標準ライブラリのreモジュールで使えます。

本記事では、各構文について「記号の意味」と「実際に動かした例」をセットで示します。

例を実行する道具としてはre.findallを一貫して使います。re.findall(パターン, 文字列)という形で使います。文字列からパターンに合う箇所をすべて見つけてリストで返す関数です。

import re

print(re.findall(r"\d+", "A1 B22"))
# ['1', '22'] ← 数字のかたまりを全部見つけてリストにする

findallは引数flagsを使うことでフラグを設定し、機能を変えることができます。

raw文字列(r"...")を使う

パターンを書くときは文字列の前にrを付けたraw文字列(r"\d+"のような書き方)を使うのが定番です。通常の文字列だと\がエスケープシーケンス(\nは改行、\tはタブなど)として先に解釈されて、正規表現にわたる前に意味が変わってしまいます。r"..."にすれば\がそのまま正規表現にわたるので、余計なトラブルを避けられます。

構文の一覧

この記事で扱う構文を、先に一覧で示します。細かい意味や使いどころは、表のあとの各節で例とともに記述しています。例の列は、「パターン("入力文字列")→re.findall」の結果の形で読みます。

分類記号意味
位置^文字列(フラグ変更で行)の先頭^a("abc")→ ['a']
$文字列(フラグ変更で行)の末尾c$("abc")→ ['c']
\b単語の境界\bcat\b("cat category")→ ['cat']
1文字.任意の1文字(改行を除く)a.c("a-c")→ ['a-c']
\d / \D数字 / 数字以外の1文字\d("a1")→ ['1']
\w / \W英数字とアンダースコア / それ以外の1文字\w("a!")→ ['a']
\s / \S空白 / 空白以外の1文字\s("a b")→ [' ']
文字クラス[…]かっこ内のどれか1文字[ab]("cabd")→ ['a', 'b']
[a-z]範囲指定[0-9]("a1b2")→ ['1', '2']
[^…]かっこ内"以外"の1文字[^0-9]("a1")→ ['a']
繰り返し*直前を0回以上ab*("a abb")→ ['a', 'abb']
+直前を1回以上ab+("a abb")→ ['abb']
?直前を0回または1回ab?("a ab")→ ['a', 'ab']
{n}/ {n,m}ちょうどn回 / n〜m回\d{2,3}("1 2345")→ ['234']
*? / +?*/+などに?を付けると最短マッチa.*?b("axbyb")→ ['axb']
グループ・選択(…)取り出す部分を囲む(キャプチャ)(\d+)円("80円")→ ['80']
|AまたはB猫|犬("猫と犬")→ ['猫', '犬']
エスケープ\. \( など\を付けると、メタ文字を「文字そのもの」として扱う\.("a.b")→ ['.']

注意点

  • \b(単語の境界)は日本語の単語の区切りには使えません\b\w\Wの境目を探す仕組みですが、日本語は語の間に空白がなく、ひらがな・漢字なども\w扱いのため、語の途中に境界ができないからです(空白・記号・英数字と接する位置でのみ働きます)。
  • \d\w\sはPython3では既定でUnicode基準で、全角数字・日本語・全角スペースにもマッチします。半角だけに限るにはre.ASCIIを使います。
  • *+{n,m}デフォルトで最長一致(貪欲)です。取りすぎるときは*?+?{n,m}?(最短)にします。
  • ^$デフォルトで文字列全体の先頭・末尾です。各行に効かせるにはre.MULTILINEが必要です。
  • (...)の有無・数でre.findallの戻り値が変わります(グループが1個ならその中身、2個以上ならタプルのリスト)。

位置を表す(アンカー)

扱う記号

^ $ \b

「行の先頭」「単語の境目」など、文字そのものではなく「位置」を表す記号があります。これをアンカーと呼びます。

先頭と末尾

^は文字列全体(フラグ変更で行)の先頭、$は末尾を表します。

print(re.findall(r"^.", "abc"))   # ['a']   先頭の1文字
print(re.findall(r".$", "abc"))   # ['c']   末尾の1文字

複数行のテキストで「各行の先頭・末尾」に効かせたいときはre.MULTILINEというフラグを渡します。

text = "one\ntwo"
print(re.findall(r"^.", text))                # ['o']        文字列の先頭だけ
print(re.findall(r"^.", text, re.MULTILINE))  # ['o', 't']   各行の先頭

このように、^$の対象は、フラグで行単位にも文字列単位にも切り替わります。

単語境界

\bは単語の境界(語を構成する文字と、それ以外との境目)を表します。「単語そのもの」を狙うときに使います。

print(re.findall(r"\bcat\b", "the cat category"))   # ['cat']

\bcat\bは前後が単語の境界になっているcatだけにマッチするので、categoryの中のcatは拾いません。なお、日本語の単語の区切りには使えません。日本語の場合は空白・記号・英数字と接する位置でのみ働きます

1文字にマッチする

扱う記号

. \d \D \w \W \s \S

次は「任意の1文字」「数字1文字」のように、1文字に当てはまる記号です。

.は任意の1文字にマッチします(既定では改行を除く)。

print(re.findall(r"a.c", "abc a c a-c"))   # ['abc', 'a c', 'a-c']

a.cは「a、任意の1文字、c」なので、間の1文字が何であっても拾えます。

数字・空白などには、それぞれ専用の記号と、その否定(大文字版)があります。

print(re.findall(r"\d+", "A1 B22"))   # ['1', '22']    \d:数字
print(re.findall(r"\D+", "A1 B22"))   # ['A', ' B']    \D:数字以外
print(re.findall(r"\w+", "id_42 あ!"))   # ['id_42', 'あ']   \w:英数字とアンダースコア(日本語なども含む)
print(re.findall(r"\W+", "id_42 あ!"))   # [' ', '!']        \W:それ以外
print(re.findall(r"\S+", "a b\tc"))   # ['a', 'b', 'c']   \S:空白以外(\s は空白)

\sはスペース・タブ・改行などの空白文字に、その否定\Sは空白以外にマッチします。上の例では、空白(スペースとタブ)で区切られたかたまりを\S+で取り出しています。

なお、Python3では\d\w\sデフォルトでUnicode基準になります。そのため\dは半角数字だけでなく全角数字()にもマッチし、\wは日本語の文字もマッチ対象にします。半角だけに限定する方法は、フラグre.ASCIIを使います。

文字クラス([...])

扱う記号

[...] [a-z] [^...]

「このうちのどれか1文字」を自分で列挙したいときは、角かっこで囲む文字クラスを使います。

print(re.findall(r"[aeiou]", "regular"))   # ['e', 'u', 'a']   かっこ内のどれか1文字

[0-9][a-z]のように、ハイフンで範囲を指定できます。[0-9]半角に限れば\dとほぼ同じ用途です(\dは全角も含みます)。

print(re.findall(r"[0-9]+", "tel 03-1234"))   # ['03', '1234']

先頭に^を置くと「かっこ内"以外"の1文字」という否定の意味になります。

print(re.findall(r"[^0-9]+", "03-1234"))   # ['-']   数字以外のかたまり

文字クラスの中では、.)などの記号は特別な意味を失い、ただの文字として扱われます。たとえば[.()]は「ピリオド・開きかっこ・閉じかっこのどれか」という意味で、エスケープは不要です。

繰り返し(量指定子)

扱う記号

* + ? {n} {n,m} *?/+?

直前の1個が「何回繰り返すか」を表す記号を量指定子と言います。*0回以上+1回以上です。

print(re.findall(r"ab*", "a ab abb"))   # ['a', 'ab', 'abb']   * は0回以上("a" も拾う)
print(re.findall(r"ab+", "a ab abb"))   # ['ab', 'abb']        + は1回以上("a" は拾わない)

両者の違いは「直前(ここではb)が0個でも許すか」です。ab*bが0個のa単独も拾い、ab+bが最低1個ないとマッチしません。

?は0回または1回(あってもなくてもよい)を表します。

print(re.findall(r"colou?r", "color colour"))   # ['color', 'colour']

u?を付けたことで、colorcolourの両方にマッチします。

回数を数で指定するには{n}(ちょうどn回)、{n,m}(n回以上m回以下)を使います。

print(re.findall(r"\d{2,4}", "1 22 333 55555"))   # ['22', '333', '5555']

\d{2,4}は2〜4桁の数字にマッチするので、1桁の1は拾わず、5桁の55555は先頭4桁の5555までを拾います。このように、{n,m}できるだけ長い方にマッチします。

貪欲マッチと最短マッチ

*+{n,m}などの繰り返しは、既定で「できるだけ長く」マッチします。これを貪欲マッチと言います。直後に?を付けて*?+?{n,m}?のようにすると、「できるだけ短く」に変わります(最短マッチ)。

print(re.findall(r"<.+>", "<a><b>"))    # ['<a><b>']        貪欲:最初の < から最後の > まで
print(re.findall(r"<.+?>", "<a><b>"))   # ['<a>', '<b>']    最短:< と > の最小ペアで区切る

<.+>は貪欲なので、<a>で止まらず最後の>まで一気に取ってしまいます。<.+?>にすると、最も近い>で区切るので<a><b>に分かれます。「取り出す範囲が想定より広い」ときは、まず最短マッチにすることを考えます。

回数を範囲で指定する{n,m}も同じです。{n,m}は上限のm回まで、{n,m}?は下限のn回で止まります。

print(re.findall(r"a{2,4}", "aaaaa"))    # ['aaaa']        貪欲:上限の4回まで取る
print(re.findall(r"a{2,4}?", "aaaaa"))   # ['aa', 'aa']    最短:下限の2回で止める

グループと選択

扱う記号

(...) |

丸かっこ(...)は、パターンの一部をひとまとめにしたり、マッチした一部を取り出したりするのに使います。これをキャプチャグループと言います。re.findallでは、グループで囲んだ部分だけが結果として返ります。

print(re.findall(r"\d+円", "120円と80円"))     # ['120円', '80円']   グループなし → マッチ全体
print(re.findall(r"(\d+)円", "120円と80円"))   # ['120', '80']      (...) で囲った部分だけ

(\d+)円のように「という目印を手がかりにしつつ、数字部分だけを取り出す」といった使い方ができます。

グループを2個以上にすると、戻り値の形が変わります。各マッチが「(グループ1, グループ2)」のタプルになり、そのリストが返ります。

print(re.findall(r"(\d+)年(\d+)月", "2020年4月と2021年12月"))   # [('2020', '4'), ('2021', '12')]
print(re.findall(r"(\d+)年(\d+)月", "2020年4月と2021年"))   # [('2020', '4')]

グループが複数ある場合、すべてのグループのパターンに一致した箇所だけが、タプルで返されます。

縦棒|は「AまたはB」という選択を表します。

print(re.findall(r"赤|青|黄", "赤と青の旗"))   # ['赤', '青']

赤|青|黄は、赤・青・黄のいずれかにマッチします。

エスケープ(記号を文字として扱う)

扱う記号

\

.+(などは正規表現で特別な意味を持つ記号(メタ文字)です。これらを「文字そのもの」として扱いたいときは、前に\を付けてエスケープします。

print(re.findall(r"\d+\.\d+", "v1.5 と 2.0"))   # ['1.5', '2.0']

\.はピリオドそのものを表します(.のままだと「任意の1文字」になってしまいます)。\d+\.\d+で「数字・ピリオド・数字」、つまりバージョン番号のような形にマッチします。

まとめ

正規表現の基本構文として、以下の構文をre.findallで例を示しながら確認しました。

  • 位置(^ $ \b
  • 1文字(.\d\D\w\W\s\S
  • 文字クラス([...] [^...]
  • 繰り返し(* + ? {n,m}、貪欲と最短)
  • グループ(...)と選択|
  • エスケープ\.

ここで紹介した基本構文を組み合わせると、ちょっとした抽出や判定の多くは表現することができます。

なお、次の記事「正規表現のグループ : キャプチャ・名前付き・後方参照(Python)」で正規表現のグループを取り扱います。

-プログラミング
-