正規表現を使うと、「文字列の中から自分で設定したパターンの文字列を探すこと」ができます。
正規表現が便利なことはわかっていても、よくわからない記号がたくさんあったり、うまくマッチしなかったり、なかなか手を出せないものです。
この記事では、Pythonの正規表現用モジュールre
に焦点をあてて、正規表現のパターン作成をわかりやすく解説します!
- 基本的な正規表現パターンの作成
- 正規表現の特殊文字の意味(特定の文字種や繰り返し)
- パターン内のグループ化(特定部分の抽出)、A or Bの設定
- 先読み/後読み・肯定/否定
正規表現パターンの作成方法を覚えて、正規表現を使いこなせるようになりましょう!
- 正規表現の使い方イメージ
- 正規表現パターンの作り方(3つのポイント)
- 文字の繰り返し
- 文字の集合の表現
- グループ化
- 文頭/文末の指定^$
- 先読み/後読み・肯定/否定(?…)
- 正規表現内にコメント記載 #
- 正規表現の応用例
正規表現の使い方イメージ
まずは、正規表現による文字の抽出例を見て、正規表現のイメージをつかみましょう。
次のような、日付を含んだ例文を考えてみます。
- 私は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つの概念が重要になります。
- 特定の文字の繰り返しを指定
- 数値や文字、空白など条件による文字条件の指定
- グループ化によるパターン内の特定部分の抽出
例えば、正規表現で「数字の後に円」が来るパターンを表現すると次のように書けます。
"(\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日'>
コメントがあると、正規表現パターンの意味がわかりやすくなりますね。
正規表現の応用例
今回は正規表現のパターン作成方法について解説しました。
しかし、正規表現は使わないことには、なかなか身につきません…。
次の記事で使用例を紹介しているので、ぜひ読んで参考にしてみてください!