Pythonができる文字列の事。中巻:正規表現の章

この記事の実行環境

  • バージョン: Python3.8
  • IDE: spacemacs
  • ライブラリ: re
  • 環境構築: pyenv、pyenv-virtualenv
  • OS: macOS

正規表現とは?

正規表現は検索するパターンを指定できるもの。単純な単語あるいはその複数で検索するだけではない。パターンには文字や数字、記号が指定でき、始まりと終わり、含むか含まないか、完全一致するか部分一致するか、などどいったことを指定できる。かなり便利。

正規表現には3種類ある。単純正規表現、基本正規表現、拡張正規表現である。実際には拡張正規表現をさらに拡張したPealの正規表現がある。Pythonの標準モジュールである「re.py」はPerlの正規表現と同様のものを提供する。

正規表現だけで記事を3本くらい書けそうだが、この記事ではPythonの正規表現の実装である「re.py」について取り扱う。

正規表現によるパターンマッチング

動かして学ぶために、書籍「退屈な事はPythonにやらせよう」に倣ってやっていこう。

正規表現を用いないテキストパターン検索

"""
3桁-3桁-4桁の電話番号を想定。
受け取る文字列の中に電話番号を見つける。
"""
def is_phone_number(text):
  """\
  受け取る文字列が、DDD-DDD-DDDDの形かどうかを調べるメソッド
  Retrun:
      True or Flase
  """
  if len(text) != 12: # 受け取った文字列が12文字かどうかを判断
    return False
  for i in range(0, 3): # 文字列の最初の3文字が数字だけか調べる
    if not text[i].isdecimal():
      retrun False
  if text[3] != '-': # 4文字目がハイフンかどうか調べる
    return False
  for i in range(4, 7): # 5文字目から7文字目までが数字だけかどうか調べる
    if not text[i].isdecimal():
      return False
  if text[7] != '-': # 8文字目がハイフンかどうか調べる
    return False
  for i in range(8, 12): # 9文字目から12文字目までが数字だけかどうか調べる
    if not text[i].isdecimal():
      return False
  return True

"""\
messageの先頭から12文字を、1文字ずらしながら
is_phone_number()に渡して評価させる。
"""
message = "明日415-555-1011に電話してください。オフィスは415-555-9999です。"
for i in range(len(message)):
  chunk = message[i:i+12]
  if is_phone_number(chunk):
    print(' 電話番号が見つかりました:' + chunk)
print("完了")

正規表現なし、実行結果

正規表現なし、実行結果

正規表現を用いてテキストパターンを検索する

次はいよいよ正規表現のモジュールを使ってみる。re.compile()に正規表現のパターンを渡す事で、Regexパターンオブジェクトが返ってくる。そしてRegexオブジェクトに対してsearch()によって検索対象の文字列を渡す。その結果、今回の場合は指定した形式の電話番号が変えてくるのだ。

search()は見つかればMatchオブジェクトを返し、見つからなければNoneを返す。実際に見つかったテキストを返すのはMatchオブジェクトのgroup()メソッドとなっている。compile()メソッドに渡すのは\d\d\dとなるところは\d{3}と、短縮して書ける。

"""
3桁-3桁-4桁の電話番号を想定。
受け取る文字列の中に電話番号を見つける。
"""
import re

phone_num_regex = re.compile(r"\d{3}-\d{3}-\d{4}")
mo = phone_num_regex.search("私の電話番号は415-555-4242です。")

print("Telephone Number:" + mo.group())

正規表現有り、実行結果

当たり前のことではあるが、用意されたモジュールを使うわけだから、ソースコードはこれだけ短くできる。

もっと正規表現する

正規表現はもっと強力な機能をもっている。それを以下の順番で試して行こう。

  1. 丸括弧()を用いてグルーピングする。
  2. 縦線|を用いて複数のグループとマッチする。
  3. 疑問符?を用いた任意のマッチング。
  4. アスタリスク*を用いいた0回以上のマッチ。
  5. プラス+を用いた1回以上のマッチ。
  6. 波括弧{}を用いて繰り返し回数を指定する。
  7. 貪欲マッチ、非貪欲マッチ
  8. findall()メソッドを使う
  9. 文字集合と独自の集合
  10. キャレットとドル記号
  11. ワイルドカード
  12. ドットとアスタリスクと改行
  13. 大文字、小文字を無視したマッチ
  14. sub()メソッドで置換する

もっと正規表現したソースコード1

import re
# 1.丸括弧()を用いてグルーピングする。
# 電話番号を市外局番とそれ以外に分けたい
# gourp()に引数を渡すとマッチした中の指定したグループだけを取得できる。
phone_num_regex = re.compile(r"(\d{3})-(\d{3}-\d{4})")
mo = phone_num_regex.search("私の電話番号は415-555-4242です。")
mo.group(0)
mo.group(1)
mo.group(2)
# groups()で全てのグループをタプル型で取得できる。
area_code, main_number = mo.groups()


# 2.縦線|を用いて複数のグループとマッチする。
# 両方ある場合は最初に出現した方を取得する。
hero_regex = re.compile(r"Batman|Tina Fey")
mo1. = hero_regex.search("Batman and Tina Fey.")
mo1.gourp()
mo2 = hero_regex.search("Tina Fey and Batman")
mo2.group()
# パターンを複数指定したい場合は丸括弧を使う
# 例えばBatで始まるいくつかの単語をパターンにしたい
bat_regex = re.compile(r"Bat(man|mobile|copter|bat)")
mo3 = bat_regex.search("Batmobile lost a wheel")
mo3.group(0)
mo3.group(1)


# 3.疑問符?を用いた任意のマッチング。
# 任意のマッチングなのでマッチしてもしなくても良い場合
bat_regex = re.compile(r"Bat(wo)?man")
mo = bat_regex.search("The Aventures of Batman")
mo.group()
mo1 = bat_regex.search("The Adventures of Batwoman")
mo1.group()
# 市外局番の有無に関係なく検索したい場合
phone_regex = re.compile(r"(\d{3}-)?\d{3}-\d{4}")
mo2 = phone_regex.search("私の電話番号は415-555-4242")
mo2.group()
mo3 = phone_regex.search("私の電話番号は555-4242")
mo3.group()


# 4.アスタリスク*を用いいた0回以上のマッチ。
# ?とほぼ同じ気がする。
bat_regex2 = re.compile(r"Bat(wo)*man")
mo4 = bat_regex2.search("The Adventures of Batman")
mo4.group()
mo5 = bat_regex2.search("The Adventures of Batwoman")
mo5.group()
mo6 = bat_regex2.search("The Adventures of Batwowowowoman")
mo6.group()
もっと正規表現した実行結果1

もっと正規表現したソースコード2

import re
# 5.プラス+を用いた1回以上のマッチ。
bat_regex3 = re.compile(r"Bat(wo)+man")
mo7 = bat_regex3.search("The Adventures of Batman")
mo7.group()
mo8 = bat_regex3.search("The Adventures of Batwoman")
mo8.group()
mo9 = bat_regex3.search("The Adventures of Batwowowowoman")
mo9.group()


# 6.波括弧{}を用いて繰り返し回数を指定する。
ha_regex = re.compile(r"(Ha){3,5}")
mo10 = ha_regex.search("HaHaHaHaHa")
mo10.group()
# Ha1回だけだとマッチしないので「None」が返される
mo11 = ha_regex.search("Ha")
mo11.group()


# 7.貪欲マッチ、非貪欲マッチ
# 貪欲=最長マッチ
greedy_Ha_regex = re.compile(r"(Ha){3,5}")
mo = greedy_Ha_regex.search("HaHaHaHaHa")
mo.group()
# 非貪欲=最短マッチ
nonreedy_Ha_regex = re.compile(r"(Ha){3,5}?")
mo1 = nongreedy_Ha_regex.search("HaHaHaHaHa")
mo1.group()
# ?は任意グループの指定と、非貪欲マッチの指定の2つの働きがある


# 8.findall()メソッドを使う
# search()とは違って一致した全ての文字列を返す
# 比較すると。
phone_num_regex = re.compile(r"\d{3}-\d{3}-\d{4}")
mo2 = phone_num_regex.search("cell: 415-555-9999 work: 212-555-0000")
mo2.group()
# findall()は文字列のリストを返す
mo3 = phone_num_regex.findall("cell: 415-555-9999 work: 212-555-0000")
mo3.group()
# パターンにグループを使うとタプルのリストを返す
phone_num_regex = re.compile(r"(\d{3})-(\d{3})-(\d{4})")
mo4 = phone_num_regex.search("cell: 415-555-9999 work: 212-555-0000")
mo4.group()
もっと正規表現した実行結果2

最初、「re.compile(r”(Ha){3, 5}”)」と書いてしまってエラーとなった。5の前の半角スペースは不要。正規表現のパターンの中に入れるとエラーとなるので注意しる。

もっと正規表現したソースコード3

import re
# 9.文字集合と独自の集合
# \dは数字を意味する
# (1|2|3|4|5|6|7|8|9|0)の短縮系、その他の短縮型など後述する
xmas_regex = re.compile(r"\d+\s\w+")
xmas_regex.findall("12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids,"
                   "7 swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge")
# 独自に集合を定義する
vowel_regex = re.compile(r"[aeiouAEIOU]")
vowel_regex.findall("RoboCop eats baby food. BABY FOOD.")
# キャレットで「それ以外」となる
vowel_regex1 = re.compile(r"[^aeiouAEIOU]")
vowel_regex1.findall("RoboCop eats baby food. BABY FOOD.")


# 10.キャレットとドル記号
# キャレットには「先頭に一致するものがあるかどうか」という働きもある
begins_with_hello = re.compile(r"^Hello")
begins_with_hello.search("Hello world!")
begins_with_hello.search("He said Hello.")

# ドルは終わりに一致するものがあるかどうかをみてくれるようになる。
ends_with_number = re.compile(r"\d$")
ends_with_number.search("Your number is 42")
ends_with_number.search(Your are 42 years old.)
# キャレットとドルで囲むと「全体の中に〜が一致するか」となる。
whole_string_is_num = re.compile(r"^\d+$")
whole_string_is_num.search("123456789")
hole_string_is_num.search("12345xyz6789")
hole_string_is_num.search("1234 56789")
もっと正規表現した実行結果3

もっと正規表現したソースコード4

import re
# 11. ワイルドカード
# 11-1 ドットがワイルドカード。1文字にしか当たらない。
at_regex1 = re.compile(r".at")
at_regex1.findall("The cat in the hat sat on the flat mat.")


# 11-2ドットとアスタリスクと改行
# 何文字でもOKになる。(改行を含まない)
at_regex2 = re.compile(r".*at")
at_regex2.findall("The cat in the\n hat sat on the \nflat mat.")
# 改行を含目たい場合は第2引数に「re.DOTALL」を指定する。
at_regex3 = re.compile(r".*at", re.DOTALL)
at_regex3.findall("The cat in the\n hat sat on the \nflat mat.")


# 12.大文字、小文字を無視したマッチ
# 大文字小文字を区別する。	区別しない場合は「re.IGNORECACE」または「re.I」
regex = re.compile(r"robocop", re.I)
regex.search("RoboCop").group()
regex.search("robocoP").group()
regex.search("ROBOCOp").group()
# 上記は全てマッチする


# 13.sub()メソッドで置換する
# 第1引数で置換文字、第2引数は置換対象の文字、2が1になる。
names_regex = re.compile(r"Agent \w+")
names_regex.sub("CENSORED", "Agent Alice gave the secret document to Agent Bob.")
# マッチした文字を置換の一部として使いたい場合、グループを指定する
agent_names_regex = re.compile(r"Agent (\w)\w*")
agent_names_regex.sub(r"\1****", "Agent Alice told Agent Carol that"
                      " Agent Eve knew Agent Bob was a double agent.")  # \1がグループの番号
もっと正規表現した実行結果4

アスタリスクと改行については、指定がない限り、改行は改行として扱われるので改行で区切られた文字列がリストに入れられて返ってくる。

正規表現の書き方一覧

ここに正規表現に出てくる記号に意味を簡易的に上げておく。

記号機能
.任意の1文字にマッチする
*直前の文字が0回以上繰り返す場合でマッチする。最長一致。
+直前の文字が1回以上繰り返す場合にマッチする。最長一致。
?直前の文字が0個か1個の場合にマッチする。最長一致。
+?直前の文字が0回以上繰り返す場合でマッチする。最短一致。
*?直前の文字が1回以上繰り返す場合にマッチする。最短一致。
??直前の文字が0個か1個の場合にマッチする。最短一致。
|OR条件。
\エスケープする。
[…]かっこ内のどれか1文字にマッチする。
[^…]かっこ内のどれか1文字以外ににマッチする。
(…)文字をグループにまとめる。
{n}直前の文字の桁数を指定。
{n,}直前の文字の最小の桁数を指定。
{n,m}直前の文字の最小と最大の桁数を指定。最長一致。
{n,m}?直前の文字の最小と最大の桁数を指定。最短一致。
メタ文字
\n改行、LF。
\r改行、CR。
\tタブ。
\d全ての数字。
\D数字以外。
\s垂直タブを除く空白文字。
\S全ての空白文字以外。
\wアルファベット、アンダーバー、数字。
\Wアルファベット、アンダーバー、数字以外。
位置の指定
^直後の文字が行の先頭にあればマッチ。
$直前の文字が行の行末にあればマッチ。
\<単語の先頭にマッチ。
\>単語の末尾にマッチ。
\b単語の先頭または末尾にマッチ。
\B単語の先頭または末尾以外にマッチ。
\Aファイルの先頭にマッチ。
\zファイルの末尾にマッチ。
\G直前の一致文字列の末尾にマッチ。
置換指定
\0一致した文字列の全体を置換する。
\1 ~ \9一致した文字列の1~9番目に対応する文字列に置換する。
\l次の1文字を小文字に変換する。
\L \E挟まれた文字列を小文字に変換する。
\u次の1文字を大文字に変換する。
\U \E挟まれた文字列を大文字に変換する。

コメントする