正規表現チートシート!正規表現を使いこなしてテキスト処理を効率化しよう

文章やその集合から特定のパターンを含む文字列を検索したり、置換したりしたいのであれば、かなりの確率で正規表現は有用です。

正規表現はある意味独自のルールを持つ小さな言語なので、未知の複雑さから避けてしまう人も多くいる分野です。

この記事では「正規表現とは何か」についてざっくりと知っていることを前提として、正規表現を学ぶメリット、習得法、そして参照用のチートシートを紹介したいと思います。

正規表現を使うメリット

正規表現のメリットは、例を考えてみるとわかりやすいので、ここでは「文章の中に含まれるインターネットURLを取り出したい」という例を考えてみましょう。

以下はインターネットURLを比較的単純な正規表現で表したものです。

^(https?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$

何やらやたらと記号を含んでいて暗号じみています。もちろんこのような単純な正規表現では全てのルールを記述できませんが、一般的なURLのパターンを持つ文字列についてマッチさせることができます。

次に正規表現を使わずに、インターネットURLのパターンを表すことを考えてみましょう。

httpまたはhttpsから始まって、その次は://で、その後のホスト名に使える文字は・・・

URL1つ表すにもルールの全てをコードに落とし込まなくてはならず、コードが複雑になり、それが正しいかの検証も大変です。

正規表現を使えば、より複雑なパターンを持つ文字列もマッチングできます。マッチングルールが明確な形で短い正規表現の中に凝縮されるので、使い方を覚えさえすれば可読性・保守性の高いコードとなります。

また正規表現は各種アプリケーションの入力検証、文書検索、Webスクレイピングなど、文字列を扱うありとあらゆる場所で頻繁に現れます。その需要の多さからも、正規表現を扱えることは重要なスキルセットの1つと言えるでしょう。

正規表現を学ぶメリット

  • 文字列をマッチングするあらゆる領域で使える
  • 正規表現を使うことで、マッチングルールが明確な形で短い文字列の中に凝縮される。その結果コードの可読性・保守性・再利用性が上がる
  • マッチング条件の検討漏れ(ロジックミス)を減らすことができる

正規表現を使いこなすには?

正規表現は奥の深い領域です。パフォーマンスの最適化のためには、正規表現エンジンの仕組みを理解して、「よい正規表現」を書かなくてはなりません。

そうは言っても、パフォーマンスの最適化が必要なユースケースは意外と少ないので「マッチングしたい文字列に対応した正規表現を素早くかけること」を目標にするのが精神衛生上も良いと思います。

この記事のターゲットは「正規表現のメリットを最速で享受したい人」なので、パフォーマンスは必要になった時点で検討するのが、正しい姿勢だと思います。まずはメタ文字の意味を覚えるところからはじめましょう!

メタ文字とは
メタ文字とは、文字本来の意味ではなく、正規表現の中で特別な意味を持つ文字のこと

以下が筆者のおすすめの学習フローです。

また、電子メールやURLなどの一般的な正規表現であれば、サンプルや検証パーツが既に存在します。正規表現を毎回1から書くよりも、過去の正規表現を活用することを考えましょう。

特に製品に正規表現を組み込むのであれば、自分で正規表現を書くよりも信頼性の高いパーツを使えるならそれが最善です。そこから自分の扱っている問題に特化したカスタマイズをすることで素早く実装ができます。

https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

正規表現の力をつける目的で、1から書いてみるのはむしろ超重要なので誤解なく!

正規表現チートシート

以降では学習フローを踏まえて、3部構成で以下のチートシートを用意しています。

正規表現チートシートリスト

  • 基本的なメタ文字
  • 電子メールやURLなど頻繁に利用される正規表現の一覧
  • 使用頻度の多い一部の発展的トピック

チートシートの内容は、追加・削除が必要な項目が見つかり次第、随時更新予定です。

基本的なメタ文字

メタ文字の分類方法は、「詳説 正規表現」を引用しています。

単一の文字にマッチするメタ文字

メタ文字マッチする文字列
.任意の1文字にマッチする
[abc]a,b,cの中の任意の1文字にマッチする
[a-z]アルファベット小文字(a-z)の中の任意の1文字にマッチする
[^a-z]アルファベット小文字(a-z)以外の任意の1文字にマッチする
\wアルファベット(小文字・大文字)、数字、ハイフンの中の任意の1文字にマッチする
[a-zA-Z0-9_]と同じ
\Wアルファベット(小文字・大文字)、数字、ハイフン以外の任意の1文字にマッチする
[^a-zA-Z0-9_]と同じ
\d数字の中の任意の1文字にマッチする
[0-9]と同じ
\D数字以外の任意の1文字にマッチする
\s空白文字(\n, \r, \tを含む)の中の任意の1文字にマッチする
\S「\s」以外の任意の1文字にマッチする
\metacharリテラルのメタ文字にマッチする
例えば、\.で.1文字にマッチする

テーブルに含まれるメタ文字についての注意点を、簡潔に挙げておく。

文字クラス […] 内のメタ文字

文字クラス内と文字クラス外では、それぞれ異なるメタ文字を持っている

文字クラス外でメタ文字であっても、文字クラス内ではただのリテラルとして扱われるものが大半であり、異なる意味を持つメタ文字として扱われることもある。

逆に「-」などは、文字クラス内でのみメタ文字としての意味を持つ。

https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

文字クラス内でよく使うメタ文字は、テーブルで挙げた否定の「^」や範囲の「-」ぐらいなので安心しよう

エスケープ(\)について

メタ文字をエスケープするとリテラルのメタ文字にマッチするのは、テーブルで示したとおり。注意すべき点としては、エスケープ自体もメタ文字なので、エスケープのリテラルにマッチさせたい場合は、\\とします。

逆に、メタ文字以外の文字の中にはエスケープ(\)するとメタ文字としての意味を持つものがある。テーブルで挙げた\w\d\sなどがその例です。

量指定子(”繰り返し”を提供するために付加されるメタ文字)

量指定子は、前の最短部分列の繰り返しを提供します。

最短部分列は平たく言うと「マッチングの最小単位」で通常は単一の文字のことを指しますが、直前が開カッコ「()」で囲まれる場合はカッコ内の文字列が最短部分列となります。

例としてds*hackはdhack、dshack、dsshack、・・・にマッチするが、(ds)*hackはhack、dshack、dsdshack、・・・にマッチする。

メタ文字マッチする文字列
?最短部分列の0回または1回の出現にマッチする
*最短部分列の0回以上の任意回数の繰り返しにマッチする
+最短部分列の1回以上の任意回数の繰り返しにマッチする
{n}最短部分列のn回の繰り返しにマッチする
{n,}最短部分列のn回以上の繰り返しにマッチする
{n, m}最短部分列のn回以上、m回以下の繰り返しにマッチする

位置にマッチするメタ文字

正規表現入門から一歩進むと、位置にマッチするという考え方が必要です。ここでは理解しやすさのために、具体例をテーブルに含めます。

メタ文字マッチする文字列
^文字列の先頭の位置にマッチする
$文字列の末尾の位置にマッチする
\b単語の境界にマッチする
^dshack文字列の先頭の「dshack」にマッチする
dshack$文字列の末尾の「dshack」にマッチする
\bdshack\b単語境界で囲まれた「dshack」にマッチする

単語境界は多くの場合、文字または数字の並びの先頭と末尾を指しています。

その他のメタ文字

メタ文字マッチする文字列
|区切っている正規表現のいずれかにマッチする
co(m|mm)i(t|tt)eecomitee, commitee, comittee, committeeのいずれかにマッチする
(どれが正しいスペルでしょうか?)
(dshack)*dshackの0回以上の任意回数の繰り返しにマッチする

正規表現において、開カッコ「()」には複数の意味があります。

ここで挙げたのは、選択「|」のための括弧と、量指定子の対象となる文字列のグループ化ですが、その他に後方参照のためのキャプチャなどの役割があります。

これらはやや発展的な内容(だけどしばしば便利)のため、発展的トピックの方で取り上げます。

頻繁に利用される正規表現リスト

頻繁にマッチングされるものについては、信頼性の高い正規表現のリストを持っておき、それを使うことが不具合を減らすことに繋がります。リストはPythonのr文字列を前提としていますが、手を加えることで他の言語でも使用可能です。

但し、盲信はよくありませんので、正規表現が何にマッチして、何にマッチしないのかアプリケーションに組み込む際には必ず把握しておくようにしましょう。

https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

パスワード以外の正規表現はアルファベットの大文字・小文字を区別しないマッチングモードを前提としています。

マッチング対象正規表現
Emailアドレスr’^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$’
URLスキーマr'(?:http|ftp)s?://’
ホスト名r'[a-z’ + ‘\u00a1-\uffff’ + r’0-9](?:[a-z’ +’\u00a1-\uffff’ + r’0-9-]{0,61}[a-z’ + ‘\u00a1-\uffff’ + r’0-9])?’
ドメイン名r'(?:\.(?!-)[a-z’ + ‘\u00a1-\uffff’ + r’0-9-]{1,63}(?<!-))*’
IPアドレス(IPv4)r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}’
インターネットURLリスト下のdjangoソースコード参照
twitterユーザー名r’^[a-z0-9_\-.]{3,15}$’
パスワードr’^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$’
(大文字小文字英数字組み合わせ、特殊文字禁止、長さは8-10)
固定電話r’^0\d(-\d{4}|\d-\d{3}|\d\d-\d\d|\d{3}-\d)-\d{4}$’
携帯電話r’^(0[789]0)-\d{4}-\d{4}$’
IP電話r’^050-\d{4}-\d{4}$’
フリーダイヤルr’^0120-\d{3}-\d{3}$’
日付r’^\d{4}-\d{1,2}-\d{1,2}$’
郵便番号r’^\d{3}-\d{4}$’

複数の構成要素が集まった文字列については、構成要素毎の正規表現をパーツ毎に分けて書くのが、コードの可読性を向上し、ミスを防ぐのに役立ちます。

下記はPythonのWebフレームワークdjangoのソースコードの一部で、URLについて、小さな正規表現の部品を組み合わせて複雑な正規表現を作成する良い例となっています。

ul = '\u00a1-\uffff' # r文字列で扱えないことに注意

scheme_re = r'(?:http|ftp)s?://'

# IP patterns
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
ipv6_re = r'\[[0-9a-f:\.]+\]'  # (simple regex, validated later)

# Host patterns
hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?&amp;amp;amp;lt;!-))*'
tld_re = (
    r'\.'                                # dot
    r'(?!-)'                             # can't start with a dash
    r'(?:[a-z' + ul + '-]{2,63}'         # domain label
    r'|xn--[a-z0-9]{1,59})'              # or punycode label
    r'(?&amp;amp;amp;lt;!-)'                            # can't end with a dash
    r'\.?'                               # may have a trailing dot
)
host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'

url_re = (
    scheme_re
    r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?'  # user:pass authentication
    r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
    r'(?::\d{2,5})?'  # port
    r'(?:[/?#][^\s]*)?'  # resource path
    r'\Z')

正規表現を書く際の注意点

全てに対応できる完璧な正規表現を書くのは至難です。正規表現の不具合の原因は大きく分けて下記の2つがあります。

正規表現の不具合原因

  • マッチングルールで検討できていないものがある
  • マッチングルールを正規表現に落とし込む際のコーディングミス

2番目の要因は、正規表現の基本を学び、テストを実施することで高い確率で防ぐことが可能です。但し、1番目の要因は「何をマッチングしたいのか」について明確なルールがない場合には常に起こりうることです。

この内容について、オライリー出版の「詳説 正規表現」で良い表現があったので、引用しておきます。

単純なものと完璧なものという相反する要求の間でバランスを取るためには、検索対象のデータを知ることが極めて重要な意味を持つ

詳説 正規表現第3版 P.24

電子メールで上記の文章の意味を確認してみましょう。

単純すぎる正規表現例

\w+\@\w+(\.\w+)+

完璧に近いが複雑すぎる正規表現例

RFC5322公式標準に、99.99%の電子メールアドレスにマッチングする正規表現があります。下記のURLから正規表現を引用しました。

参考 Email Address Regular Expression That 99.99% Works. Disagree? Join discussion!https://emailregex.com/
(?:[a-z0-9!#$%&’*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&’*+/=?^_`{|}~-]+)*|”(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*”)@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

この2つの正規表現には、完全性と可読性・保守性のトレードオフが存在します。自分で正規表現を書く場合は、実装コストも大きく異なるでしょう。

検索対象のデータを知ることで、このトレードオフの最適なバランスを探ることができます。

https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

自分で正規表現を書かず、継続的に検証されているライブラリやフレームワークの提供する機能で解決することが出来ないか検討することが重要なことも付け加えておこう

発展的トピック

これまでに挙げたメタ文字でも多くのことが表現できますが、入門から一歩進んで覚えておくと便利な機能が正規表現には存在します。

メタ文字内容
\1, \2, ・・・1番目、2番目、・・・の括弧内でマッチしたテキストと同じものにマッチする(正規表現パターン内)
$1, $2, ・・・1番目、2番目、・・・の括弧内でマッチしたテキストを参照する(マッチング後)
(abc)abcをキャプチャしてグループ化を行う
(?:abc)abcをキャプチャせずにグループ化のみを行う
(?=abc)「abc」が右側に存在する位置にマッチする
(?!abc)「abc」が右側に存在しない位置にマッチする
(?<=abc)「abc」が左側に存在する位置にマッチする
(?<!abc)「abc」が右側に存在しない位置にマッチする
i修飾子大文字と小文字を区別せずにマッチングを行う
g修飾子文字列に複数のマッチングが存在する場合、全てにマッチさせる
m修飾子文字列の先頭と末尾の位置にマッチするメタ文字^$を、行について判断させる

後方参照の方法は、プログラミング言語によって異なるものが多いですが機能としてはほとんどのプログラミング言語で存在します。

以下では単純な例を挙げて、それぞれの使い方について簡潔に紹介します。

マッチング文字列の後方参照

「単語の重複を見つけたい」「インターネットURLからドメイン名とパスを取り出したい」「住所から都道府県名と市町村を取り出したい」等々、マッチングした部分列を後から参照したい場合、後方参照の出番です。

但し、正規表現パターン内で参照する場合とマッチング後に参照する場合で、参照方法が異なります。それぞれについて簡単な例を見てみましょう。

正規表現パターン内での参照

最も単純な例として、連続した単語の重複を見つける際、\b([A-Za-z]+)\s+\1\bが使えます。

カッコでグループ化された正規表現はキャプチャされ、同一の正規表現内でも\numの形式で後方参照することができます。

マッチング後の参照

マッチングが成功した文字列について、全体ではなく一部分を取り出したい場合があります。

DjangoのURL正規表現を例にするとわかりやすいので、再掲します。

ul = '\u00a1-\uffff' # r文字列で扱えないことに注意

scheme_re = r'(?:http|ftp)s?://'

# IP patterns
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
ipv6_re = r'\[[0-9a-f:\.]+\]'  # (simple regex, validated later)

# Host patterns
hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?&amp;amp;amp;lt;!-))*'
tld_re = (
    r'\.'                                # dot
    r'(?!-)'                             # can't start with a dash
    r'(?:[a-z' + ul + '-]{2,63}'         # domain label
    r'|xn--[a-z0-9]{1,59})'              # or punycode label
    r'(?&amp;amp;amp;lt;!-)'                            # can't end with a dash
    r'\.?'                               # may have a trailing dot
)
host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'

url_re = (
    scheme_re
    r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?'  # user:pass authentication
    r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
    r'(?::\d{2,5})?'  # port
    r'(?:[/?#][^\s]*)?'  # resource path
    r'\Z')

(?:)は「グループ化を行うが、結果はキャプチャしない」という意味を持ちます。このようにすることで、明示的にキャプチャしないということがコード上示せることと、パフォーマンスが若干向上するというメリットがあります。

デメリットとしては、正規表現が少し読みにくくなることです。

したがってマッチング後に参照したいのであれば、(?:)ではなく()で正規表現を囲むことで、キャプチャを行い、後続の処理参照可能になります。

参照方法は言語により異なりますが、Pythonであればマッチングオブジェクトのgroupメソッドを使って、以下のように参照を行います。

import re

url_re = (
    scheme_re
    r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?'  # user:pass authentication
    r'(' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')' # ?:を外してキャプチャを行う
    r'(?::\d{2,5})?'  # port
    r'(?:[/?#][^\s]*)?'  # resource path
    r'\Z')

pattern = re.compile(url_re)
m = re.search(pattern, "https://datascientist-toolbox.com/regex-cheetsheet/")
if m:
    m.group(0) # datascientist-toolbox.com

インターネットURLからFQDN(ホスト名+ドメイン名)を取り出すことができました。

応用的な位置マッチング

基本的なメタ文字では、位置にマッチする正規表現として^$\bを挙げました。さらに応用的な位置マッチングの方法として、先読み・後読みによるマッチングが存在します。

例えば4桁以上の数字について、3桁ごとに桁区切りのカンマ(,)を加えたい場合どのようにマッチングすればよいでしょうか。

これを言い換えると、「左側に数字が存在し、右側に3の倍数個数の数字が存在する位置にカンマを振る」となります。このように位置に着目してマッチングを行わなければならない場合に、先読み・後読みによるマッチングが役に立ちます。

まずは「左側に数字が存在する」は表と照らし合わせると、(?<=)を使えば良いとわかります。ですので、(?<=\d)とすることで左側に数字が1つ存在する位置すべてにマッチングします。

同様に「右側に数字が3の倍数個数存在する」という条件は、(?=)を使い、(?=(\d\d\d)+)と表せます。これにより、右側に数字が3の倍数個数存在する位置すべてにマッチングします。

最後に境界についてです。単語境界を使っても良いのですが、これは例えば単位がついた「10000万円」のような文字列にはマッチングしてくれなくなります。ここでも先読みによるマッチングを使えば、「右側で数字とマッチングしない位置」という正規表現(?!\d)を使って、上記の文字列にもカンマを加えることが出来ます。

まとめると(?<=\d)(?=(\d\d\d)+(?!\d)という正規表現により、カンマを挿入しなくてはならない位置にマッチングできます。

あとは各種言語に備わった正規表現の置換手段を使い、置換後の文字列としてカンマ(,)を指定することで桁区切りが行われます。

正規表現オプション

正規表現によるマッチングを実行する際に、知っていると便利なオプションが存在します。

使用頻度が多いものとして、テーブルに挙げたi修飾子、g修飾子、m修飾子があります。これらはプログラミング言語によって指定方法が異なります。

例えばPythonであれば、正規表現を使った処理にはreモジュールを使用し、それぞれ以下のように実現できます。

Pythonの正規表現オプション

  • i修飾子:re.IGNORECASEをメソッドの引数に渡すことで実装
  • g修飾子:re.match, re.searchの2つのメソッドが修飾子の有無に対応。置換の場合はre.subの引数に対応。
  • m修飾子:re.MULTILINE(re.M)をメソッドの引数に渡すことで実装

本格的に学びたい人のためのリソース

正規表現の本質を理解し、良い正規表現を書くことを目指すのであればオライリーの「詳説 正規表現」がおすすめです。

1〜3章で正規表現の基本が体系的にまとまっているので、入門者は当面3章までを学習目標にすると良いと思います。「正規表現の専門家になりたい」という願望がないのであれば、1冊で実用レベルまで引き上げてくれるので非常にお買い得です。