KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

正規表現難しいですか?基本を理解

プログラミングやデータ処理において欠かせないツールの一つに「正規表現(Regular Expressions)」があります。でも、「正規表現は難しい」「暗号なんだけど」「いまさら覚える必要ある?」と感じる方も多いのではないでしょうか。実際には実務で使えると非常に強力なツールです。

正規表現の基本をわかりやすく解説し、その難しさを乗り越えるためのポイントを紹介します。

目次


正規表現とは

正規表現は、文字列のパターンを定義するための言語です。主に文字列の検索、置換、検証に使用される事になります。

例えば、メールアドレスや電話番号の形式をチェックしたり、特定の単語を抜き出したりする際に利用されるんですが、これ正規表現使わず、pythonなどの言語を使って例えばメールアドレスはおろか、電話番号が書式に合ってるかどうかすら、実は結構煩雑な処理になります。ちょっとpythonで書いてみましょうか。

_samples_ok = [
    "03-1234-5678","045-123-4567","090-1234-5678",
    "0120-123-456","0312345678","0451234567",
    "+81-3-1234-5678","+81-90-1234-5678",
    "+81312345678","+819012345678",
    "08012345678","080-1234-5678",
    "0570-123-456","0120123456",
]
_samples_ng = [
    "03-1234-567","+1-202-555-0199","090-12345678-",
    "++819012345678","(03)1234-5678","abc",
    "03 1234 5678"," 090-1234-5678 ","03-1234-5678",
    "+81 3-1234-5678","+81-3-1234-5678-",
    "03-123-45678","008012345678","090-1234-5678ext123",
    "03-1234-56789","+8190123456789","+81-090-1234-5678",
]

上記が、OKパターン、NGパターンです。これをもとに、NGパターンをはじいて、 OKパターンだけを通すような処理を書いてみましょう。

まず、正規表現を使わずに出来るだけ簡単に書いてみます。

def is_valid_phone(s):
    # Reject boundary or consecutive hyphens
    if s.startswith('-') or s.endswith('-') or '--' in s:
        return False
    # Hyphenated formats
    if '-' in s:
        parts = s.split('-')
        # International with hyphens: +81-A-B-C, A must not start with '0'
        if parts[0] == '+81':
            if len(parts) != 4:
                return False
            a, b, c = parts[1], parts[2], parts[3]
            if a.startswith('0'):
                return False
            return (a.isdigit() and b.isdigit() and c.isdigit()
                    and 1 <= len(a) <= 4
                    and 1 <= len(b) <= 4
                    and len(c) == 4)
        # Domestic with hyphens
        if parts[0].startswith('0'):
            if len(parts) != 3:
                return False
            a, b, c = parts
            # Free-dial patterns 0120, 0570, 0800: group sizes 4-3-3
            if a in ('0120', '0570', '0800'):
                return (b.isdigit() and c.isdigit()
                        and len(b) == 3 and len(c) == 3)
            # Standard: A(2-5)-B(1-4)-C(4)
            return (a.isdigit() and b.isdigit() and c.isdigit()
                    and 2 <= len(a) <= 5
                    and 1 <= len(b) <= 4
                    and len(c) == 4)
        return False
    # Non-hyphen formats
    if s.startswith('+81'):
        d = s[3:]
        return (d.isdigit() and 9 <= len(d) <= 10
                and not d.startswith('0'))
    if s.startswith('0'):
        return s.isdigit() and len(s) in (10, 11)
    return False

どうですかね。わかりやすいですか?これ、電話番号のチェックです。メールアドレスになるとこんなもんじゃすみません。次に正規表現を使ったバージョンがどうなるか見てみましょう。

import re

_pattern = re.compile(
    r'^(?:'
      r'(?:0120|0570|0800)-\d{3}-\d{3}'        # フリーダイヤル/サービス番号
      r'|0\d{1,4}-\d{1,4}-\d{4}'               # 国内ハイフン付き
      r'|0\d{9,10}'                            # 国内ハイフンなし
      r'|\+81-[1-9]\d{0,3}-\d{1,4}-\d{4}'      # 国際ハイフン付き(先頭数字0禁止)
      r'|\+81[1-9]\d{8,9}'                     # 国際ハイフンなし(先頭数字0禁止)
    r')$'
)

def is_valid_phone(s):
    return bool(_pattern.match(s))

どうでしょう?どっちがわかりやすいでしょうか?わかりやすいわかりにくいというか、 正規表現使わないともはや解読不可能ではないでしょうか? 「普通にpython版も解読できるよ」っていう人は読解力スゴイですが。

正規表現を使うことで、複雑な文字列操作を効率的に行うことが可能、と言葉で言うより コードで見たらわかりやすいのではないでしょうか。

正規表現の基本構文

さて、「正規表現めっちゃ大事やんけ」と思っていただいたところで、正規表現の基本を説明してみたいと思います。

正規表現では、文字や記号を組み合わせてパターンを作成します。以下に、基本的な構文を紹介します。

リテラル文字

リテラル文字は、そのままの文字としてマッチします。例えば、正規表現「cat」は「cat」という文字列にマッチします。

メタ文字

メタ文字は、特別な意味を持つ文字で、パターンの柔軟性を高めます。代表的なメタ文字には以下のものがあります。

  • . - 任意の一文字にマッチ
  • * - 直前の文字の0回以上の繰り返しにマッチ
  • + - 直前の文字の1回以上の繰り返しにマッチ
  • ? - 直前の文字の0回または1回の繰り返しにマッチ
  • [] - 文字クラス。指定したいずれかの文字にマッチ
  • () - グループ化。部分パターンをグループとして扱う
  • | - OR演算子。いずれかのパターンにマッチ
  • ^ - 行の先頭にマッチ
  • $ - 行の末尾にマッチ

よく使うメタ文字

以下に、正規表現でよく使用されるメタ文字とその意味をまとめます。

メタ文字 意味
. 任意の一文字にマッチ
* 直前の文字の0回以上の繰り返しにマッチ
+ 直前の文字の1回以上の繰り返しにマッチ
? 直前の文字の0回または1回の繰り返しにマッチ
[] 文字クラス。指定したいずれかの文字にマッチ
() グループ化。部分パターンをグループとして扱う
| OR演算子。いずれかのパターンにマッチ
^ 行の先頭にマッチ
$ 行の末尾にマッチ
\d 任意の数字にマッチ
\w 任意の単語文字(英数字とアンダースコア)にマッチ
\s 任意の空白文字にマッチ

正規表現の実例

正規表現を具体的な例で理解しましょう。以下に、いくつかの実例を紹介します。

メールアドレスの検証

/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/

この正規表現は、一般的なメールアドレスの形式を検証します。行の先頭( ^ )から始まり、ユーザー名部分、@マーク、ドメイン名部分、ドットとトップレベルドメイン($)で終わるパターンにマッチします。

電話番号の抽出

/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/

この正規表現は、ハイフンやドットがあるかないかに関わらず、10桁の電話番号にマッチします。単語境界(\b)で囲まれた3桁、3桁、4桁の数字のパターンです。

電話番号の検証の例でも正規表現を使いましたが、メールアドレスも電話番号も実際にはもっと細かい規則があり、これだけじゃほんとは足りません。上記はあくまで簡略化したサンプルです。

URLの抽出

/https?:\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\/\S*)?/

URLもやってみましょう。こんな感じ。なんとなーくわかりますかね?

この正規表現は、httpまたはhttpsで始まるURLにマッチします。オプションでwww.が含まれ、ドメイン名とトップレベルドメインの後に任意のパスが続くパターンです。

正規表現を学ぶためのヒント

正規表現は一見難しく感じられるかもしれませんが、以下のヒントを参考に学習を進めることで、基本を理解しやすくなります。

  • 小さなパターンから始める: 簡単なパターンを作成し、少しずつ複雑なパターンに挑戦していく。
  • オンラインツールを活用する: Regex101などのオンライン正規表現テスターを使用して、リアルタイムでパターンの動作を確認する。これはもう必須と思ってください。絶対使った方がいいです。
  • リファレンスを参照する: 正規表現のリファレンスガイドやチートシートを手元に置き、必要な時に参照する。
  • 実践的な問題を解く: 実際のプロジェクトや課題で正規表現を使用し、実践を通じて理解を深める。
  • 逃げられない事を理解する:正規表現からは逃げられません

継続的に練習することで、正規表現の理解が深まり、自然と使いこなせるようになります。

まとめ

正規表現は、初めて学ぶ際には難しく感じるかもしれませんが、活用できるようになるとめっちゃ強力で、仕事力がアップします。書き捨てのスクリプトでは特に威力を発揮します。

コーディングでのテキスト処理もそうなのですが、データ検証やログ調査が格段に、というか1000倍、一万倍のレベルで効率的になります。

正規表現をマスターすることで、日常の作業がよりスムーズになり、生産性の向上につながります。ぜひこの記事をきっかけに、正規表現になじんでいってください。