YutaKaのPython教室

Python の文法やライブラリ、API、環境構築について画像・動画・ソースコード付きで徹底解説!

Python正規表現パターンを図解&サンプルで本当にわかりやすく解説

正規表現を使うと、「文字列の中から自分で設定したパターンの文字列を探すこと」ができます。

正規表現が便利なことはわかっていても、よくわからない記号がたくさんあったり、うまくマッチしなかったり、なかなか手を出せないものです。

この記事では、Pythonの正規表現用モジュールreに焦点をあてて、正規表現のパターン作成をわかりやすく解説します!

  • 基本的な正規表現パターンの作成
  • 正規表現の特殊文字の意味(特定の文字種や繰り返し)
  • パターン内のグループ化(特定部分の抽出)、A or Bの設定
  • 先読み/後読み・肯定/否定

正規表現パターンの作成方法を覚えて、正規表現を使いこなせるようになりましょう!

正規表現の使い方イメージ

まずは、正規表現による文字の抽出例を見て、正規表現のイメージをつかみましょう。

次のような、日付を含んだ例文を考えてみます。

  • 私は4月1日は焼き肉を食べて、4月2日は寿司を食べて、4月13日はタラコを食べた。

この文字列から、×月×日というパターンの日付だけ抽出してみます。

import re

src = "私は4月1日は焼き肉を食べて、4月2日は寿司を食べて、4月13日はタラコを食べた。"
re.findall(r"\d月\d{1,2}日", src) #\dは数字を表す特殊文字

# ['4月1日', '4月2日', '4月13日']

ここでは、×月×日を"\d月\d{1,2}日"という正規表現のパターンにして文章の中から探しています。

図にすると次のようなイメージでにパターンを探しています。

テキストのパターンを見抜けば、様々なデータの抽出が可能です。

上の例では、「×月×日は○○○を食べ」というパターンで日付と食べ物が記述されているようです。

食べ物も抽出してみましょう。

re.findall(r"(\d月\d{1,2}日)は(\w+?)を食べ", src)
# [('4月1日', '焼き肉'), ('4月2日', '寿司'), ('4月13日', 'タラコ')]

日付と食べ物が抽出できました。

正規表現を覚えれば、簡単にデータの抽出、削除、置換ができるようになります。

この記事では、pythonの正規表現ライブラリreに焦点をあてて正規表現パターンの作成方法を解説していきます!

正規表現用ライブラリ|re

pythonでは、標準ライブラリreを使用して正規表現を取り扱うことが出来ます。

標準ライブラリなので、外部モジュールのインストールといった環境構築の必要はなく、import文だけで使用できるようになります。

import re

以下で正規表現の基本的パターンの生成方法について確認していきましょう。

正規表現パターンの作り方(3つのポイント)

正規表現では、特定の文字のパターンを作成して、パターンにマッチする文字部分を検索します。

パターンを作成するときは、次の3つの概念が重要になります。

  1. 特定の文字の繰り返しを指定
  2. 数値や文字、空白など条件による文字条件の指定
  3. グループ化によるパターン内の特定部分の抽出

例えば、正規表現で「数字の後に円」が来るパターンを表現すると次のように書けます。

  • "(\d+)円"

これは、次のような条件の組み合わせです。

  • 「数字(文字条件"\d")を一回以上(繰り返し条件"+")」
  • その後に「円」が続く

さらに、数字部分"\d""()"で囲んでグループ化しておくと、後から数字だけ抽出できます。

以下で、文字条件、繰り返し条件、グループ化を詳しく見ていきましょう。

文字の繰り返し

繰り返し回数を指定すると、直前の文字を何回繰り返すかを指定できます。

繰り返し回数の指定方法は、次の2つの方法があります。

  • 数字による繰り返し回数の指定"{n,m}"
  • 特殊文字による繰り返し回数の指定"*+?"

2つの指定方法について、順番に見ていきましょう。

繰り返し回数指定{n,m}

数値での指定は{}に繰り返し回数を入力します。

記法 意味
{n} n回繰り返す
{n,m} n回~m回繰り返す
{n,} n回以上繰り返す
{,m} 0~m回まで繰り返す

こうすると、次のように似たようなパターンを一つの正規表現パターンで記述できます。

  • "gre{2,5}n""e""2~5"回まで繰り返すパターン
サンプル文字列 マッチするかどうか コメント
"grn" × "e""0"回なので
"green" "e""2"回なので
"greeeeen" "e""5"回なので
"greeeeeeeeeen" × "e""10"回なので

実際に正規表現用ライブラリreで実行例を見てみましょう。

re.match("gre{2,5}n", "grn")
# None
re.match("gre{2,5}n", "green")
# <re.Match object; span=(0, 5), match='green'>
re.match("gre{2,5}n", "greeeeen")
# <re.Match object; span=(0, 8), match='greeeeen'>
re.match("gre{2,5}n", "greeeeeeeeeen")
# <re.Match object; span=(0, 8), match='greeeeen'>
# None

手入力で繰り返し回数を指定する以外にも、特殊文字でよく使う繰り返し回数を指定できます。

既定の繰り返し回数*+?

{n,m}の形式では、手入力で繰り返し回数を指定することができました。

ありがたいことに、よく使う繰り返し回数は、特殊文字で指定することができます。

繰り返しに関する特殊文字は下表の通りです。

特殊文字 意味 ざっくりした使い道
? 0~1回以上繰り返す 直前の文字があってもなくてもいいとき
* 0回以上繰り返す 直前の文字が何回あってもなくてもいいとき
+ 1回以上回繰り返す 直前の文字が少なくとも1回は必要なとき

それぞれ次のような使い方ができます。

特殊文字 使い方例 マッチ例
? "https?" "http", "https"("s"はあってもなくてもOK)
* "Yes!*" "Yes", "Yes!", "Yes!!!""!"は何回あってもなくてもOK)
"+" "\d+日" "1日", "10日", "99日"(少なくとも数字\dが1回は必要)

実際に正規表現用ライブラリreで実行例を見てみましょう。

# "?":0~1回以上繰り返す
re.match("https?", "http")
# <re.Match object; span=(0, 4), match='http'>
re.match("https?", "https")
# <re.Match object; span=(0, 5), match='https'>
 
# "*":0回以上繰り返す
re.match("Yes!*", "Yes")
# <re.Match object; span=(0, 3), match='Yes'>
re.match("Yes!*", "Yes!!!")
# <re.Match object; span=(0, 6), match='Yes!!!'>

# "+":1回以上繰り返す
re.match("\d+日", "1日")
# <re.Match object; span=(0, 2), match='1日'>
re.match("\d+日", "99日")
# <re.Match object; span=(0, 3), match='99日'>
re.match("\d+日", "日")
# <re.Match object; span=(0, 3), match='99日'>
# None  ←数字が1回以上必要

"?", "*", "+"は、よくわからない変な記号ではなくて、繰り返し回数を一文字で指定できる便利な記号だったんですね!

文字の集合の表現

正規表現パターン作成の重要なもう一つの要素が文字集合です。

例えば、次の例の「 」内の文字のように特定の文字の集合を指定できます。

  • 「月、火、水、木、金」+ 曜日
  • 「数字」 + 日
  • 「名前」 + さん

文字集合でパターン生成すると、様々なパターンの検索ができるようになります。

文字集合を指定[]

文字集合を自分で指定する場合は、"[]"内に文字を並べます。

特殊文字 含まれる文字の集合
"[]" "[ ]"内で文字集合の要素を指定
"[^]" "[^ ]"内の要素以外にマッチ

例えば、次のように使用します。

  • "[ABCDE]""A", "B", "C", "D", "E"にマッチ
  • "[12345]""1", "2", "3", "4", "5"にマッチ
  • "[月火水木金]""月", "火", "水", "木", "金"にマッチ

連続する文字の場合は、次のように-で省略表記することもできます。

  • "[A-E]""A", "B", "C", "D", "E"にマッチ
  • "[1-5]""1", "2", "3", "4", "5"にマッチ

実際に正規表現用ライブラリreで実行例を見てみましょう。

re.match("[月火水木金]曜日", "月曜日")
# <re.Match object; span=(0, 3), match='月曜日'>
re.match("[月火水木金]曜日", "日曜日")
# None ←日曜日は含まれていない

re.match("[1-5]時間", "1時間")
# <re.Match object; span=(0, 3), match='1時間'>
re.match("[1-5]時間", "9時間")
# None ←9は含まれていない

"^"を付けて要素を指定すると、指定した要素以外にマッチするようになります。

特定の文字を除外したいときに便利ですね。

ただし、指定していない文字全てにマッチする点には注意が必要です。

re.match(r"[^土日]曜日","火曜日" )
# <re.Match object; span=(0, 1), match='火曜日'>
re.match(r"[^土日]曜日","炎曜日" )
# <re.Match object; span=(0, 3), match='炎曜日'>

文字集合を表す特殊文字.\w\d\s

正規表現で頻繁に使用される文字集合の特殊文字は次の通りです。

特殊文字 含まれる文字の集合 コメント
"." 任意の文字(ワイルドカード) 改行は含まない。改行を含む場合は、"re.DOTALL"指定
"\w" 単語文字(英数字・日本語などのこと) 全角文字等も含む。"re.ASCII"でASCIIに限定可能
"\W" 単語文字以外 同上
"\d" 数字 全角数字等も含む。"re.ASCII"でASCIIに限定可能
"\D" 数字以外 同上
"\s" 空白文字(スペース、タブ、改行など) "[ \t\n\r\f\v]"
"\S" 空白文字以外  

もちろん繰り返しを表す"{}", "+", "*", "?"と組み合わ可能なので、様々なパターンが自由自在に作れます。

使用例を見てみましょう。

特殊文字 使用例 コメント
"." "<p>.*</p>" 特定の文字に挟まれた任意の文字抽出(ここではpタグの例)
"\w" "\w曜日" 曜日の抽出
"\d" "\d+月\d+日" 日にちの抽出

実際に正規表現用ライブラリreで実行例を見てみましょう。

re.match("<p>.*</p>", "<p>こんにちは</p>")
# <re.Match object; span=(0, 12), match='<p>こんにちは</p>'>

re.match("\w曜日","月曜日" )
# <re.Match object; span=(0, 3), match='月曜日'>

re.match("\d+月\d+日","10月20日" )
# <re.Match object; span=(0, 6), match='10月20日'>

任意文字.に改行を含める方法

注意点としては、任意の文字列の特殊文字"."は、改行は含まないという例外があります。

例えば、次のテキストで「」内文字列を".+"で抽出しようとしても、改行があるのでうまくいきません。

text="""
田中「こんにちは。
私は元気です。
あなたはどう?」
"""

re.search("「.+」", text)
# None

任意の文字列"."に、改行を含ませるには"re.DOTALL"フラグを有効にします。

re.search("「.+」", text, re.DOTALL)
# <re.Match object; span=(3, 27), match='「こんにちは。\n私は元気です。\nあなたはどう?」'>

貪欲マッチと非貪欲マッチ+?*?

繰り返し回数指定と文字集合を組み合わせるときは、貪欲、非貪欲という概念が重要になります。

通常、正規表現は結果が最長になるようにマッチ(貪欲マッチ)しようとします。

例として、次のテキストで「」に囲まれた文字列を抽出してみます。

text = """
田中「こんにちは」吉田「さようなら」
"""
re.search("「.+」", text)
# <re.Match object; span=(3, 19), match='「こんにちは」吉田「さようなら」'>

「こんにちは」ではなく、「こんにちは」吉田「さようなら」が抽出されています。

結果が最長になるようにマッチ(貪欲マッチ)したため、一回目の"「"と、二回めの"」"にマッチしてしまいました。

最小の長さでマッチ(非貪欲マッチ)させるには、繰り返しの特殊文字"*", "+"の後に"?"を追加します。

text = """
田中「こんにちは」吉田「さようなら」
"""
re.search("「.+?」", text)
# <re.Match object; span=(3, 10), match='「こんにちは」'>

貪欲マッチと非貪欲マッチを整理すると、下表のようになります。

マッチ対象 貪欲マッチ(最長) 非貪欲マッチ(最短)
任意文字列"0"回以上繰り返し ".*" ".*?"
任意文字列"1"回以上繰り返し ".+" ".+?"

グループ化

正規表現パターン作成の重要な要素の一つがグループ化です。

グループ化を行うと、次のようなことができます。

  • 各グループの内容を個別に抽出
  • 置換時にグループ内の内容を再利用
  • AまたはBの指定

まずはグループ化の方法を確認していきましょう。

グループ作成方法

グループを作成するには、パターン内でグループにしたい部分を"()"で囲みます。

m = re.match("商品名:(\w+), 価格:(\d+)円", "商品名:コーヒー, 価格:100円")
# <re.Match object; span=(0, 17), match='商品名:コーヒー, 価格:100円'>
# 商品名と価格を()で囲んで、それぞれグループ化済み

続いてグループ化したマッチ結果の利用方法を見ていきましょう。

グループの内容を個別抽出

"Matchオブジェクト"のメソッドを使用して、各グループの内容を抽出することができます。

グループ抽出メソッド メソッド内容
.groups() 各グループを要素として持つ"タプル"
.group() 引数で指定したグループ内容を出力

まずは、.groups()メソッドで各グループの内容を確認してみます。

m.groups()
'商品名:コーヒー, 価格:100円'

個別に出力する場合は、".group()"メソッドを使用します。

グループには、"("が現れる順番に1から連番が振られています。

引数を省略するか、0を入力すると、マッチした内容全体に出力されます。

# 引数0または省略で、全体を出力
m.group()

# 引数1で一つ目のグループ内容
m.group(1)
# 'コーヒー'

# 引数2で一つ目のグループ内容
m.group(2)
# '100'

グループに名前設定

次のようにグループに名前をつけることも可能です。

  • "(?P<name>...)"

実際に正規表現用ライブラリreで実行例を見てみましょう。

m = re.match("商品名:(?P<prod_name>\w+), 価格:(?P<prod_price>\d+)円", "商品名:コーヒー, 価格:100円")
# <re.Match object; span=(0, 17), match='商品名:コーヒー, 価格:100円'>

名前を指定しておけば、.group()メソッドの引数で名前を使用することも可能です。

# 引数1で一つ目のグループ内容
m.group("prod_name")
# 'コーヒー'

# 引数2で一つ目のグループ内容
m.group("prod_price")
# '100'

マッチ内容を利用して置換

正規表現を使用した文字列置換時には、パターン内のグループ要素を置換時に再利用することも可能です。

置換後のテキスト内で次のように指定します。

指定方法 指定例
"\グループ番号" "\1", "\2", ...
"\g<グループ番号>" "\g<1>", "\g<2>", ...
"\g<グループ名>" "\g<グループ名1>", "\g<グループ名2>",...

実際に正規表現用ライブラリreで実行例を見てみましょう。

re.sub("商品名:(\w+), 価格:(\d+)円", #置換対象のパターン
       r"Name: \1, price: \g<2>", # 置換後の内容
       "商品名:コーヒー, 価格:100円") # 置換されるテキスト
# 'Name: コーヒー, price: 100'
re.sub("商品名:(?P<prod_name>\w+), 価格:(?P<prod_price>\d+)円", #置換対象のパターン
       r"Name: \g<prod_name>, price: \g<prod_price>", # 置換後の内容
       "商品名:コーヒー, 価格:100円") # 置換されるテキスト
# 'Name: コーヒー, price: 100'

単語Aまたは単語B(ORの組み込み)

「単語Aまたは単語B」のOR条件を指定する場合は、"|"で単語を繋ぎます。

  • "単語A|単語B" ← 単語Aまたは単語Bにマッチ

ただし、実用上はORの前後にも何か文字があることがほとんどです。

その場合は、次のようにOR部分をグループ化します。

  • "これは(単語A|単語B)です" ←「これは単語Aです」または「これは単語Bです」にマッチ

実際に正規表現用ライブラリreで実行例を見てみましょう。

re.search("(python|ruby)経験あり", "python経験あり")
# <re.Match object; span=(0, 10), match='python経験あり'>

re.search("(python|ruby)経験あり", "ruby経験あり")
# <re.Match object; span=(0, 8), match='ruby経験あり'>

re.search("(python|ruby)経験あり", "調理師経験あり")
# None ←python or ruby にマッチしない

文頭/文末の指定^$

特殊文字"^", "$"を使用すると文頭/文末のみにマッチするという条件を追加することができます。

特殊文字 追加条件 書き方
^ 文頭でマッチ ^パターン
$ 文末でマッチ パターン$

ここで注意したいのは、"^", "$"は行ごとに文頭/文末の評価をするのではなく、文字列全体の先頭か末尾かの評価しかできません。

各行ごとに、文頭/文末のチェックを行うには、"re.MULTILINE"フラグを指定します。

次のサンプルテキストで実行例を見てみましょう。

text="""1月:寒い
2月:超寒い
3月:かなり寒い(2月よりはまし)
4月:暖かい
5月:まあまま暖かい(たまに8月並みのこともある)
6月:かなり暖かい
"""

まずは"^"について、"^"の有無、"re.MULTILINE"の有無による挙動の違いを確認してみましょう。

# ^がない場合
re.findall("\d月",text)
# ['1月', '2月', '3月', '2月', '4月', '5月', '8月', '6月']
# 文頭・文中関係なしに全ての月を抽出

# ^で先頭指定
re.findall("^\d月",text)
# ['1月']
# テキストの先頭の一個のみ抽出

# ^で文頭指定(re.MULTILINE)
re.findall("^\d月",text, re.MULTILINE)
# ['1月', '2月', '3月', '4月', '5月', '6月']
# re.MULTILINEフラグによって、各行の文頭の月に一致

次に"$"について、"$"の有無、"re.MULTILINE"の有無による挙動の違いを確認してみましょう。

# $がない場合
re.findall(":.*暖かい",text)
# [':暖かい', ':まあまま暖かい', ':かなり暖かい']
# 文頭・文中関係なしに全ての暖かいを抽出

# $で末尾指定
re.findall(":.*暖かい$",text)
# [':かなり暖かい']
# テキストの末尾の一個のみ抽出

# $で文末指定(re.MULTILINE)
re.findall(":.*暖かい$",text, re.MULTILINE)
# [':暖かい', ':かなり暖かい']
# re.MULTILINEフラグによって、各行の文末に一致するか判定
# 文末に余計なコメント(たまに8月並みのこともある)がある行はマッチしない

上記の例では、":"は抽出しないほうが自然ですね。

この場合は、次に紹介する後読み肯定を利用します。

先読み/後読み・肯定/否定(?…)

先読み/後読み・肯定/否定を指定すると、マッチ条件に「パターンの前後に特定の文字があるかないかを追加」できます。

先読み/後読み・肯定/否定で追加される条件は次の表の通りです。

先読み/後読み 肯定 否定
先読み:パターンの右側に… 特定の文字がある 特定の文字がない
後読み:パターンの左側に… 特定の文字がある 特定の文字がない

ポイントとしては、先読み/後読みの内容は、マッチ結果としては出力されません。

そのため、次のようなときに便利です。

  • マッチの条件としては、設定したいが…
  • 結果として出力したいわけではない

具体的には、下表のように"(?…)"という形式でパターンに組み込みます。

先読み/後読み 肯定 否定
先読み "パターン(?=肯定文字)" "パターン(?!否定文字)"
後読み "(?<=肯定内容)パターン" "(?<!否定内容)パターン"

まずは次のようなテキストで"「」"に囲まれた文字列を普通に抽出してみます。

text="""
新商品「ハイパードリンク」
新商品「スーパードリンク」(オススメ)
従来品「ノーマルドリンク」(廃盤)
"""

re.findall("「.+?」", text)
# ['「ハイパードリンク」', '「スーパードリンク」', '「ノーマルドリンク」']

先読み肯定で、"オススメ"が続くという条件を付与してみます。

re.findall("「.+?」(?=(オススメ))", text)
# ['「スーパードリンク」']

次に先読み否定で、"廃盤"が続かないという条件を付与してみます。

re.findall("「.+?」(?!(廃盤))", text)
# ['「ハイパードリンク」', '「スーパードリンク」']

次に後読み肯定で、"新商品"に続くという条件を付与してみます。

re.findall("(?<=新商品)「.+?」", text)
# ['「ハイパードリンク」', '「スーパードリンク」']

最後に後読み肯定で、"新商品"に続かないという条件を付与してみます。

re.findall("(?<!新商品)「.+?」", text)
# ['「ノーマルドリンク」']

先読み/後読みは、条件を付与するときに便利な機能ですね。

正規表現内にコメント記載 #

正規表現は作り込んでいくと、何が書いてあるのかよくわからなくなってきます。

そんな時は、正規表現にコメントを記入しましょう。

次のように、"re.VERBOSE"(or "re.X")フラグを設定すると、正規表現内にコメントが記載できます。

  • "re.compile(パターン, re.VERBOSE)"
  • "re.search(パターン, re.VERBOSE)"

"re.VERVOSE"を有効にすると、空白・改行が無視され、"#"から行末までがコメントとして認識されます。

p = re.compile("""\d{4}年      # 4ケタの数字で、年を探す
                  \d{1,2}月    # 1~2ケタの数字で、月を探す
                  \d{1,2}日    # 1~2ケタの数字で、日を探す
""", re.VERBOSE)

p.search("2012年12月25日")
# <re.Match object; span=(0, 11), match='2012年12月25日'>

コメントがあると、正規表現パターンの意味がわかりやすくなりますね。

正規表現の応用例

今回は正規表現のパターン作成方法について解説しました。

しかし、正規表現は使わないことには、なかなか身につきません…。

次の記事で使用例を紹介しているので、ぜひ読んで参考にしてみてください!

≫Amazonの購入履歴をスクレイピングなしで抽出
簡易的にAmazonの注文履歴を整理したい場合のPythonスクリプトです。「Amazonの注文履歴から金額と商品名と日付を抽出したいな」、「とはいえ、わざわざSeleniumを使ってスクレイピングするほどでもないな」、「でも、手作業で集計するのはめんどくさいな・・・」。こういったときに、さらっとAmazonの注文履歴から金額、商品名、日付を抽出するPythonスクリプトを紹介します。
www.yutaka-note.com/entry/ama2df
 
≫aタグのリンクを新規タブで開くようにするPythonスクリプト
htmlのaタグのリンクを外部リンクで開くようにaタグを更新するpythonスクリプトを紹介します。テキスト形式でhtmlを用意して、pythonスクリプトを実行すると自動でaタグの属性を更新してくれます。
www.yutaka-note.com/entry/a_tag_jido