【入門】正規表現の基本を紹介|何ができる?どう使う?

【入門】正規表現の基本を紹介|何ができる?どう使う?(約5000語の詳細解説)

プログラミング、データ処理、テキスト編集。様々な場面で「文字列」はつきものです。そして、その文字列の中から特定のパターンを見つけ出したり、書き換えたり、あるいは特定の形式に合っているかを確認したりしたい、というニーズは頻繁に発生します。そんなとき、絶大な威力を発揮するのが「正規表現」です。

正規表現と聞くと、「記号がたくさん並んでいて難しそう」「複雑でとっつきにくい」といったイメージを持つ方も多いかもしれません。確かに、一見すると暗号のように見えることもあります。しかし、正規表現は文字のパターンを記述するための「言葉」のようなものであり、その基本ルールさえ理解すれば、文字列操作の達人への第一歩を踏み出すことができます。

この記事では、正規表現を全く知らない方でも理解できるよう、その基本から具体的な使い方までを約5000語のボリュームで徹底的に解説します。正規表現が「何ができるのか」「どう使うのか」を、豊富な例を交えながら丁寧に紐解いていきます。この記事を読み終える頃には、きっと正規表現が強力な武器であると同時に、決して恐れるものではないと感じているはずです。さあ、正規表現の世界へ一緒に踏み出しましょう。

第1章:正規表現とは何か? なぜ学ぶ価値があるのか?

1.1 正規表現(Regular Expression)とは?

正規表現(せいきひょうげん、Regular Expression)は、特定のパターンに一致する文字列を表現するための、形式的な文字列です。簡単に言えば、「こういう形の文字列を探したい」「こういう形の文字列をこう変えたい」という「形(パターン)」を指定するためのルールや記号の集まりです。

例えば、「a」という文字を探したい、という場合は「a」というパターンを指定します。「数字」を探したい、という場合は「\d」というパターンを指定します。「アルファベットの大文字が3つ連続する」という場合は「[A-Z]{3}」というパターンを指定します。このように、正規表現を使うことで、単純な一文字だけでなく、複雑な文字の並びや構造を持つパターンも表現することができます。

正規表現は特定のプログラミング言語やアプリケーションに固有のものではなく、多くのプログラミング言語(Python, Java, JavaScript, Ruby, PHP, Perlなど)や、テキストエディタ(VS Code, Sublime Text, Sakura Editorなど)、コマンドラインツール(grep, sed, awkなど)で共通して利用できる、汎用性の高い強力なツールです。一度正規表現の書き方を覚えれば、様々なツールや言語でその知識を活かすことができます。

1.2 なぜ正規表現を学ぶ価値があるのか?

正規表現は、文字列処理が必要な様々な場面で、手作業では困難あるいは不可能だったタスクを、効率的かつ正確にこなすことを可能にします。

  • 効率化: 大量のテキストデータから必要な情報だけを抜き出したり、一括で変換したりする作業を、手作業や単純なプログラムで記述するよりもはるかに短い時間で実現できます。
  • 柔軟性: 特定の決まった文字列だけでなく、「〜という形式の文字列」といった抽象的なパターンを指定できるため、変動するデータに対しても柔軟に対応できます。
  • 強力な検索・置換: エディタの検索・置換機能で「正規表現を使う」というオプションを見たことがある方もいるでしょう。これを利用することで、より高度で複雑な検索や置換が可能になります。
  • 入力検証: ユーザーが入力したデータが、電話番号やメールアドレス、特定のコード体系など、決められたフォーマットに従っているかどうかのチェック(バリデーション)を簡単に行えます。
  • データ抽出・整形: ログファイル、WebページのHTMLソース、設定ファイルなどから、必要なデータ要素だけを抽出し、扱いやすい形に整形する作業に不可欠です。

正規表現を習得することは、あなたのデータ処理能力やプログラミングスキルの幅を大きく広げることに繋がります。最初は少し難しく感じるかもしれませんが、基本から順に学んでいけば、その強力なパワーを必ず自分のものにできます。

第2章:正規表現で何ができる? – 具体的なユースケース

正規表現が役立つ具体的な場面をいくつか見てみましょう。これにより、正規表現を学ぶモチベーションが高まるはずです。

2.1 文字列の「検索」

  • 特定の単語を含む行を見つけ出す:
    • 巨大なログファイルから「ERROR」という単語を含む行だけを抽出したい。
    • grep "ERROR" server.log のようにコマンドラインで簡単に実行できます。
  • 特定の形式のデータを検索する:
    • 文章の中から全てのメールアドレスやURLをリストアップしたい。
    • 特定のパターン(例: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})を使って検索します。
  • 特定の範囲の単語を探す:
    • 「cat」「dog」「bird」のいずれかを含む行を探したい。
    • cat|dog|bird のように | (OR) を使ってパターンを記述できます。
  • 特定の文字で始まる、または終わる行を探す:
    • 行の先頭が「#」で始まるコメント行だけを無視したい。
    • 行末が「.」で終わる文章を探したい。
    • ^#\.$ のように位置指定子を使います。

2.2 文字列の「置換」

  • 文字列の一部を別の文字列に置き換える:
    • 文章中の「古いバージョン」という記述を全て「新しいバージョン」に置き換えたい。
    • エディタの正規表現置換機能で「検索: 古いバージョン」「置換: 新しいバージョン」と指定します。
  • 特定のパターンに一致する部分を削除する:
    • HTMLソースから全てのHTMLタグ(例: <p>, <div>)を削除して、テキストだけを取り出したい。
    • 検索パターンに <[^>]*>、置換文字列に空文字を指定します。
  • 文字列のフォーマットを変換する:
    • 日付形式「YYYY/MM/DD」で書かれたテキストを、全て「MM-DD-YYYY」形式に変換したい。
    • 検索パターンに ^(\d{4})/(\d{2})/(\d{2})$、置換文字列に \2-\3-\1 と指定します。ここで登場する ()\1 などが正規表現の強力な機能です(後述)。

2.3 文字列の「検証(バリデーション)」

  • ユーザー入力の形式チェック:
    • 会員登録フォームで、メールアドレス欄に正しい形式の文字列が入力されたか確認したい。
    • 電話番号欄に日本の電話番号として妥当な形式(例: 090-xxxx-xxxx, 03-xxxx-xxxxなど)で入力されたか確認したい。
    • パスワードが「半角英数字記号をそれぞれ1文字以上含み、8文字以上」といった条件を満たしているか確認したい。
  • 設定ファイルやデータファイルの構文チェック(簡易版):
    • 特定の行が期待するキー=値の形式になっているかを確認したい。

2.4 文字列からの「抽出」

  • 構造化されていないテキストからデータを抜き出す:
    • Webページのソースコードから、全てのリンクURL(<a href="...">... 部分)を抜き出したい。
    • ログファイルから、各アクセス元のIPアドレスとアクセス日時を抜き出して一覧にしたい。
    • 特定の区切り文字(例: カンマやタブ)で区切られたデータから、特定の列の値を抜き出したい。
  • 特定のパターンを持つ部分文字列を取り出す:
    • 文章中のカッコ (...) の中身だけを全て取り出したい。

2.5 文字列の「分割」

  • 特定のパターンを区切りとして文字列を分割する:
    • カンマ区切り(CSV)の1行の文字列を、各列の値に分割したい(ただし複雑なCSVには限界があります)。
    • 句読点や改行を区切りとして、文章を単語や文に分割したい。

これらのユースケースは、正規表現が解決できる問題のほんの一例です。正規表現をマスターすれば、これまで面倒だった文字列処理作業が、驚くほど簡単かつ正確に行えるようになります。

第3章:正規表現の基本的な書き方と要素

ここから、正規表現の具体的な書き方、つまりパターンを記述するためのルールや記号について詳しく見ていきます。正規表現は、いくつかの基本的な要素を組み合わせてパターンを構築していきます。

3.1 リテラル文字

正規表現パターン中で特別な意味を持たない文字は、その文字自身にマッチします。これを「リテラル文字」と呼びます。

  • a は文字列中の “a” にマッチします。
  • 123 は文字列中の “123” という並びにマッチします。
  • Hello は文字列中の “Hello” という並びにマッチします。

例:
正規表現:cat
対象文字列:The cat sat on the mat.
マッチする部分:「cat」

正規表現:12345
対象文字列:Zip code is 12345-6789.
マッチする部分:「12345」

3.2 メタ文字(特殊文字)

正規表現では、特定のリテラル文字ではなく、特別な意味を持つ文字や記号があります。これらを「メタ文字」と呼びます。メタ文字は正規表現の柔軟性と表現力を高めるために非常に重要です。

主なメタ文字とその意味:

  • . (ドット): 任意の1文字(改行文字を除く)。
  • *: 直前の要素が0回以上繰り返されることにマッチ。
  • +: 直前の要素が1回以上繰り返されることにマッチ。
  • ?: 直前の要素が0回または1回出現することにマッチ(オプション)。
  • |: 選択(OR)。前後のパターンのどちらかにマッチ。
  • ^: 行の先頭にマッチ。
  • $: 行の末尾にマッチ。
  • []: 文字クラス。カッコ内に指定された文字のいずれか1文字にマッチ。
  • {}: 量指定子。直前の要素が指定回数繰り返されることにマッチ。
  • (): グループ化。複数の要素をまとめて一つの単位として扱ったり、マッチした部分を記憶(キャプチャ)したりするために使用。
  • \: エスケープ文字。直後のメタ文字の特殊な意味を打ち消し、リテラル文字として扱わせるために使用。また、特別な意味を持つショートハンド文字(\d, \w など)の開始にも使用。

これらのメタ文字は、それぞれ独立した意味を持っていますが、組み合わせて使うことで複雑なパターンを表現します。

3.3 エスケープ処理 \

メタ文字をリテラル文字として扱いたい場合は、直前にバックスラッシュ \ を付けます。これを「エスケープする」と言います。

  • . (ドット) は通常「任意の1文字」ですが、\. とするとリテラルの「.」にマッチします。
  • * は通常「0回以上」ですが、\* とするとリテラルの「*」にマッチします。
  • \ (バックスラッシュ) 自体をリテラルとして扱いたい場合は、\\ と二つ重ねて書きます。

例:
正規表現:a\.b
対象文字列:a.b にはマッチしますが、axb にはマッチしません。

正規表現:C:\\Users\\
対象文字列:C:\Users\ にマッチします。

3.4 文字クラス []

[] は、カッコ内にリストされた文字のいずれか1文字にマッチさせるために使用します。

  • [abc]:”a”、”b”、または “c” のいずれか1文字にマッチします。
  • [0123456789]:数字のいずれか1文字にマッチします。
  • [aeiou]:母音のいずれか1文字にマッチします。

ハイフン - を使うことで、文字の範囲を指定できます。

  • [a-z]:小文字アルファベットのいずれか1文字にマッチします。
  • [A-Z]:大文字アルファベットのいずれか1文字にマッチします。
  • [0-9]:数字(0から9)のいずれか1文字にマッチします。\d と同じ意味です。
  • [a-zA-Z]:英字(大文字または小文字)のいずれか1文字にマッチします。
  • [a-zA-Z0-9]:英数字のいずれか1文字にマッチします。

文字クラスの先頭に ^ を置くと、「指定した文字以外」にマッチします。

  • [^0-9]:数字以外の任意の1文字にマッチします。\D と同じ意味です。
  • [^aeiou]:母音以外の任意の1文字にマッチします。
  • [^<>"‘]<,>,,‘` 以外の任意の1文字にマッチします。

例:
正規表現:gr[ae]y
対象文字列:gray にも grey にもマッチします。

正規表現:[0-9]{3}-[0-9]{4}
対象文字列:123-4567 にマッチします。abc-defg にはマッチしません。

正規表現:Windows[^0-9]
対象文字列:WindowsXPWindowsX にマッチします(Pは[^0-9]にマッチ)。Windows95 にはマッチしません。

3.5 省略表現(ショートハンド文字)

よく使われる文字クラスには、より短い省略表現(ショートハンド)が用意されています。これらは \ から始まります。

  • \d: 数字 ([0-9]) にマッチします。
  • \D: 数字以外 ([^0-9]) にマッチします。
  • \w: 単語構成文字(アルファベット、数字、アンダースコア _)([a-zA-Z0-9_]) にマッチします。
  • \W: 単語構成文字以外 ([^a-zA-Z0-9_]) にマッチします。
  • \s: 空白文字(スペース、タブ \t、改行 \n, \r、改ページ \f、垂直タブ \v など)にマッチします。
  • \S: 空白文字以外にマッチします。

例:
正規表現:\d{3}-\d{4}
対象文字列:090-1234 にマッチします。abc-defg にはマッチしません。

正規表現:\w+
対象文字列:Hello_World 123! の中の Hello_World123 にそれぞれマッチします。

正規表現:\s
対象文字列:Hello World の中のスペースにマッチします。

3.6 量指定子 (Quantifiers)

量指定子は、その直前の要素(文字、文字クラス、グループなど)がどれだけ繰り返されるか、出現回数を指定するために使用します。

  • *: 0回以上。直前の要素がない場合も含めて、繰り返しにマッチします。
  • +: 1回以上。直前の要素が少なくとも1回は出現する場合の繰り返しにマッチします。
  • ?: 0回または1回。直前の要素がある場合とない場合のどちらかにマッチします(要素がオプションであることを示します)。
  • {n}: ちょうど n 回。直前の要素が厳密に n 回繰り返されることにマッチします。
  • {n,}: n 回以上。直前の要素が n 回以上繰り返されることにマッチします。上限はありません。
  • {n,m}: n 回以上 m 回以下。直前の要素が n 回から m 回の間で繰り返されることにマッチします。

例:
正規表現:a*
対象文字列:bcaaaad の中の “” (bの前), “a”, “aa”, “aaa”, “aaaa”, “” (dの後) にマッチします。最も長いマッチは aaaa です(後述の貪欲マッチのため)。

正規表現:a+
対象文字列:bcaaaad の中の “a”, “aa”, “aaa”, “aaaa” にマッチします。

正規表現:colou?r
対象文字列:color にも colour にもマッチします。

正規表現:\d{4}
対象文字列:Year 2023 の中の 2023 にマッチします。

正規表現:\d{2,4}
対象文字列:1, 12, 123, 12345 の中の 12, 123, 1234 にマッチします。

3.7 貪欲マッチ (Greedy) と 怠惰マッチ (Lazy)

量指定子(*, +, ?, {n,}, {n,m})は、デフォルトでは可能な限り長い文字列にマッチしようとします。これを「貪欲マッチ (Greedy Match)」と呼びます。

量指定子の直後に ? を付けると、可能な限り短い文字列にマッチするようになります。これを「怠惰マッチ (Lazy Match)」または「非貪欲マッチ (Non-Greedy Match)」と呼びます。

例:
対象文字列:<p><b>Hello</b> World</p>

正規表現(貪欲):<.*>
このパターンは、< で始まり、.(任意の文字)が *(0回以上)続き、> で終わる部分にマッチします。貪欲マッチの場合、最初の < から最後の > まで全体(<p><b>Hello</b> World</p>)にマッチします。

正規表現(怠惰):<.*?>
同じパターンですが、量指定子 * の後に ? が付いています。これにより、.*?(0回以上、ただし最短で)続き、次の > にマッチする部分を探します。結果として、<p><b></b><p> の4つの部分にそれぞれマッチします。

この貪欲/怠惰の挙動は、HTMLタグのようにネストした構造から特定のパターンを抽出したい場合などに非常に重要になります。

3.8 位置指定子 (Anchors)

位置指定子は、文字そのものではなく、文字列中の「位置」にマッチします。

  • ^: 行の先頭にマッチします。
  • $: 行の末尾にマッチします。
  • \b: 単語の境界にマッチします。単語構成文字 (\w) とそうでない文字 (\W) の間、または文字列の先頭/末尾と単語構成文字の間に存在する見えない境界です。
  • \B: 単語の境界以外にマッチします。

例:
対象文字列:
apple
banana
application

正規表現:^app
マッチする部分:apple の先頭の appapplication の先頭の app。(banana は行頭が a ではないためマッチしない)

正規表現:ana$
マッチする部分:banana の末尾の ana。(apple, application は行末が ana ではないためマッチしない)

正規表現:\bcat\b
対象文字列:The cat sat on the catastrophe.
マッチする部分:cat の部分。catastrophe の中の cat は、前後に単語構成文字 (a, s) が続いているため、単語の境界 \b にはマッチしません。

正規表現:\Bcat\B
対象文字列:The cat sat on the catastrophe.
マッチする部分:catastrophe の中の cat の部分。前後に単語境界がない(単語構成文字が続いている)位置にマッチします。

これらの位置指定子を使うことで、「〜で始まる」「〜で終わる」「単語として出現する」といった、位置に関する条件をパターンに加えることができます。特に ^$ は、文字列全体が特定のパターンに完全に一致するかどうかを検証する際によく使われます。例えば、メールアドレスの検証などでは、パターン全体を ^...$ で囲み、文字列全体がメールアドレスのパターンである必要がある、と指定します。

3.9 選択 | (Alternation)

| (パイプ) は、前後のパターンのどちらかにマッチすることを意味します。論理演算の OR のようなものです。

例:
正規表現:cat|dog
対象文字列:I have a cat and a dog.
マッチする部分:「cat」と「dog」にそれぞれマッチします。

正規表現:ing|ed$
対象文字列:He is walking.She finished.
walkinging にマッチします。finisheded に(行末で)マッチします。

| はグループ化 () と組み合わせて使うことが多いです。

例:
正規表現:(cat|dog)s
対象文字列:He loves cats and dogs.
マッチする部分:「cats」と「dogs」にそれぞれマッチします。「cat」や「dog」単独にはマッチしません。カッコがない cat|dogs だと、「cat」または「dogs」にマッチしてしまいます。

3.10 グループ化とキャプチャ () (Grouping and Capturing)

() は主に二つの目的で使われます。

  1. グループ化: 複数の要素を一つのまとまりとして扱い、量指定子などを適用できるようにします。
  2. キャプチャ: マッチした部分文字列を記憶(キャプチャ)し、後で参照したり、置換時に利用したりできるようにします。

例(グループ化):
正規表現:(ab)+
対象文字列:ab, abab, ababab
マッチする部分:「ab」, 「abab」, 「ababab」にそれぞれマッチします。カッコがない ab+ だと、”a” の後に “b” が1回以上続くパターン(ab, abb, abbb など)になってしまいます。

例(キャプチャ):
正規表現:(\d{4})/(\d{2})/(\d{2})
対象文字列:Today is 2023/10/27.
マッチする部分:2023/10/27 全体にマッチします。
さらに、() で囲まれた部分がキャプチャされます。
– 1番目のグループ (\d{4})2023 をキャプチャします。
– 2番目のグループ (\d{2})10 をキャプチャします。
– 3番目のグループ (\d{2})27 をキャプチャします。

キャプチャされた文字列は、「後方参照」や置換機能で利用できます。

3.11 後方参照 (\1, \2 など)

() でキャプチャされた文字列は、パターン内や置換文字列内で \1, \2, \3, … のように番号を付けて参照できます。\1 は1番目の () の内容、\2 は2番目の () の内容、といった具合です。

例(パターン内での後方参照):
正規表現:([a-z])\1
対象文字列:bookkeeping
([a-z]) は任意の一文字をキャプチャします。\1 はそのキャプチャされた文字と同じ文字を意味します。
マッチする部分:「oo」, 「kk」, 「ee」にそれぞれマッチします(同じ文字が連続しているパターン)。

例(置換での後方参照):
対象文字列:Date: 2023/10/27
正規表現:(\d{4})/(\d{2})/(\d{2}) (キャプチャグループが3つ)
置換文字列:$2-$3-$1 または \2-\3-\1 (使用するツールや言語によります。多くの言語では $1 形式、sed などでは \1 形式が多いです。)
結果:Date: 10-27-2023
キャプチャした年、月、日の順序を入れ替えて日付形式を変換できました。

3.12 非キャプチャグループ (?:...)

() はデフォルトでキャプチャを行いますが、単にグループ化したいだけでキャプチャは不要な場合もあります。その場合は、() の代わりに (?:...) を使うことで、グループ化は行うもののキャプチャはしないようにできます。これにより、後方参照の番号がずれるのを防いだり、パフォーマンスをわずかに向上させたりすることができます。

例:
正規表現:(?:cat|dog)s
対象文字列:cats and dogs
マッチする部分:「cats」と「dogs」にそれぞれマッチします。これは (cat|dog)s と同じマッチ結果ですが、このパターンによるキャプチャグループは存在しません。

3.13 先読み・後読み (Lookahead/Lookbehind) – 少し応用

先読み・後読みは、特定のパターンが「直後にある(ない)」または「直前にある(ない)」という条件に基づいてマッチングを行う高度な機能です。マッチするのはあくまで先読み・後読みを行う「現在の位置」であり、先読み・後読みで指定されたパターン自体はマッチ結果に含まれません。

  • (?=...): ポジティブ先読み (Positive Lookahead)。現在の位置の直後... のパターンが存在する場合にマッチ。
  • (?!...): ネガティブ先読み (Negative Lookahead)。現在の位置の直後... のパターンが存在しない場合にマッチ。
  • (?<=...): ポジティブ後読み (Positive Lookbehind)。現在の位置の直前... のパターンが存在する場合にマッチ。(多くの正規表現エンジンでは ... 内のパターンは固定長である必要がありますが、可変長をサポートするものもあります)
  • (?<!...): ネガティブ後読み (Negative Lookbehind)。現在の位置の直前... のパターンが存在しない場合にマッチ。(後読み同様、固定長や可変長の制約がある場合があります)

例:
対象文字列:apple, banana, orange, pineapple

正規表現:apple(?=, banana)
このパターンは、「apple」という文字列の直後に「, banana」が続く場合に、「apple」の部分にマッチします。
マッチする部分:apple, banana, orange, pineapple の中の最初の apple
apple, banana という文字列のうち、apple の部分だけがマッチ結果となります。

正規表現:\b\w+(?!s)\b
このパターンは、「単語の境界 \b で囲まれた単語構成文字の並び \w+」のうち、「単語の末尾 \b の直後に s が来ない」ものにマッチします。つまり、複数形でない単語を探します。
対象文字列:I have a cat and two dogs.
マッチする部分:「have」, 「a」, 「cat」, 「and」, 「two」にそれぞれマッチします。「dogs」は直後に s があるためマッチしません。

先読み・後読みは、特定の条件を満たす「位置」や、その位置の直前・直後の文字を含めずにパターンにマッチさせたい場合に非常に役立ちます。

第4章:正規表現を使った具体的な例をコードで見る

ここからは、プログラミング言語(主にPythonを例にしますが、考え方は他の言語でも共通です)を使って、正規表現がどのように利用されるか具体的なコード例とともに見ていきましょう。

多くの言語で正規表現を扱うには、専用のモジュールやライブラリをインポートする必要があります。Pythonでは re モジュールを使います。

python
import re

4.1 例1:メールアドレスの検証(簡単なものから)

メールアドレスの完全な検証は非常に複雑ですが、ここでは一般的な形式(ユーザー名@ドメイン名.トップレベルドメイン)に合うかの簡単な検証パターンを考えます。

簡単なパターン: @. が含まれているか?
正規表現:.+@.+\..+
* .+: 任意の文字が1回以上 (ユーザー名 部分を想定)
* @: リテラルの @
* .+: 任意の文字が1回以上 (ドメイン名 部分を想定)
* \.: リテラルの . をエスケープ
* .+: 任意の文字が1回以上 (トップレベルドメイン 部分を想定)

“`python
import re

pattern = r”.+@.+..+” # r”” はRaw文字列。\ をエスケープせずにそのまま書ける

emails = [
[email protected]”,
[email protected]”,
“invalid-email”,
“@domain.com”,
“user@domain.”,
“user@domain”
]

print(“— 簡単なメールアドレス検証 —“)
for email in emails:
if re.fullmatch(pattern, email): # fullmatch() は文字列全体がパターンに完全に一致するかを判定
print(f”‘{email}’: Valid”)
else:
print(f”‘{email}’: Invalid”)

出力:
— 簡単なメールアドレス検証 —
[email protected]’: Valid
[email protected]’: Valid
‘invalid-email’: Invalid
‘@domain.com’: Invalid
‘user@domain.’: Invalid
‘user@domain’: Invalid
``
このパターンは非常に緩く、
invalid-emailuser@domain.のような明らかに不正な形式もInvalidと判定できますが、@domain.comなどもInvalidになってしまいます。また、valid@.` のような不正な形式もValidと判定してしまう可能性があります。

より厳密なパターン(実用的な範囲で):
正規表現:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
* ^: 文字列の先頭
* [a-zA-Z0-9._%+-]+: ユーザー名部分。英数字、., _, %, +, - が1回以上
* @: リテラルの @
* [a-zA-Z0-9.-]+: ドメイン名部分。英数字、., - が1回以上
* \.: リテラルの .
* [a-zA-Z]{2,}: トップレベルドメイン部分。英字が2文字以上
* $: 文字列の末尾

“`python
import re

より厳密なパターン(ただし完全なRFC準拠ではない)

pattern_strict = r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$”

emails = [
[email protected]”,
[email protected]”,
“invalid-email”,
“@domain.com”, # 先頭が@ではダメ
“user@domain.”, # 末尾が.ではダメ
“user@domain”, # .TLDがないとダメ
[email protected]” # TLDが1文字なのでダメ
]

print(“\n— より厳密なメールアドレス検証 —“)
for email in emails:
if re.fullmatch(pattern_strict, email):
print(f”‘{email}’: Valid”)
else:
print(f”‘{email}’: Invalid”)
出力:
— より厳密なメールアドレス検証 —
[email protected]’: Valid
[email protected]’: Valid
‘invalid-email’: Invalid
‘@domain.com’: Invalid
‘user@domain.’: Invalid
‘user@domain’: Invalid
[email protected]’: Invalid
“`
このように、パターンを工夫することで、より正確な検証が可能になります。

4.2 例2:日付形式の変換(置換機能)

日付形式「YYYY/MM/DD」を「MM-DD-YYYY」に変換します。ここではキャプチャグループと後方参照を使います。

対象文字列:Today: 2023/10/27, Tomorrow: 2023/10/28
変換したいパターン:YYYY/MM/DD
正規表現:(\d{4})/(\d{2})/(\d{2})
* (\d{4}): 4桁の数字をキャプチャ(年) -> グループ1 (\1)
* /: リテラルの /
* (\d{2}): 2桁の数字をキャプチャ(月) -> グループ2 (\2)
* /: リテラルの /
* (\d{2}): 2桁の数字をキャプチャ(日) -> グループ3 (\3)

置換文字列:\2-\3-\1 (または $2-$3-$1
* \2: 2番目のキャプチャグループ(月)
* -: リテラルの -
* \3: 3番目のキャプチャグループ(日)
* -: リテラルの -
* \1: 1番目のキャプチャグループ(年)

“`python
import re

text = “Today: 2023/10/27, Tomorrow: 2023/10/28”

検索パターン: 年(グループ1)/月(グループ2)/日(グループ3)

search_pattern = r”(\d{4})/(\d{2})/(\d{2})”

置換文字列: 月-日-年

replace_pattern = r”\2-\3-\1″

re.sub() は、パターンに一致する全ての部分を置換する

new_text = re.sub(search_pattern, replace_pattern, text)

print(f”元の文字列: {text}”)
print(f”変換後の文字列: {new_text}”)
出力:
元の文字列: Today: 2023/10/27, Tomorrow: 2023/10/28
変換後の文字列: Today: 10-27-2023, Tomorrow: 10-28-2023
“`
このように、キャプチャグループと後方参照を使うことで、文字列の構造を分解し、並べ替えたり整形したりすることが簡単にできます。

4.3 例3:HTMLタグの削除

簡単なHTMLタグ(例: <p>, <b>, <i> など)をテキストから削除する例です。ネストしたタグや属性を持つ複雑なタグには正規表現での処理は限界がありますが、シンプルなケースでは有効です。

正規表現:<[^>]*>
* <: リテラルの <
* [^>]*: > 以外の文字 [^>] が0回以上 * 続く
* >: リテラルの >

このパターンは < で始まり、> 以外の文字が続き、> で終わる、というシンプルなタグ構造にマッチします。量指定子 * はデフォルトで貪欲マッチですが、ここでは > 以外の文字を指定しているので、最初の > まででマッチが区切られます。もし .<*> のようにすると、貪欲マッチにより最初の < から最後の > まで全てにマッチしてしまう可能性があります。<[^>]*?> (怠惰マッチ) でも同じように機能しますが、この場合は貪欲でも問題ありません。

“`python
import re

html = “

これは太字のテキストです。

斜体

検索パターン: < で始まり、> 以外の文字が0回以上続き、> で終わる

search_pattern = r”<[^>]*>”

置換文字列: 空文字(削除)

replace_pattern = r””

new_text = re.sub(search_pattern, replace_pattern, html)

print(f”元のHTML: {html}”)
print(f”タグ削除後のテキスト: {new_text}”)
出力:
元のHTML:

これは太字のテキストです。

斜体
タグ削除後のテキスト: これは太字のテキストです。斜体
“`
簡単なタグの削除であれば、このように正規表現で対応可能です。ただし、HTMLの解析には専用のライブラリ(Beautiful Soupなど)を使うのがより堅牢で推奨されます。正規表現は、構造化されていないテキストや、非常に単純な構造のテキストから情報を抽出・加工するのに向いています。

4.4 例4:URLからの情報抽出

簡単なURL(例: http://www.example.com/path/to/page.html)から、プロトコル、ドメイン名、パスなどを抽出する例です。ここでもキャプチャグループが活躍します。

正規表現:^(https?|ftp)://([^/]+)(/.*)?$
* ^: 文字列の先頭
* (https?|ftp): http または https または ftp にマッチし、キャプチャ(プロトコル)-> グループ1 (\1)。s?s が0回または1回出現することを示し、httphttps の両方に対応します。
* ://: リテラルの ://
* ([^/]+): / 以外の文字 [^/] が1回以上 + 続く部分にマッチし、キャプチャ(ドメイン名)-> グループ2 (\2)
* (/.*)?: / で始まり、任意の文字 . が0回以上 * 続く部分全体 /.* にマッチし、キャプチャ(パス)。全体が ? でオプションになっているため、パスがないURLにもマッチします。-> グループ3 (\3)
* $: 文字列の末尾

“`python
import re

url = “https://www.example.com/path/to/page.html”

search_pattern = r”^(https?|ftp)://([^/]+)(/.*)?$” # 上記のパターン

re.match() は文字列の先頭からマッチングを行う

match = re.match(search_pattern, url)

if match:
# match.groups() でキャプチャされた全てのグループをタプルで取得
protocol, domain, path = match.groups()
print(f”元のURL: {url}”)
print(f”プロトコル: {protocol}”)
print(f”ドメイン: {domain}”)
print(f”パス: {path}”) # パスがない場合は None になる可能性がある
else:
print(f”‘{url}’: URLパターンにマッチしませんでした。”)

url_no_path = “http://sub.example.co.jp”
match_no_path = re.match(search_pattern, url_no_path)
if match_no_path:
protocol, domain, path = match_no_path.groups()
print(f”\n元のURL: {url_no_path}”)
print(f”プロトコル: {protocol}”)
print(f”ドメイン: {domain}”)
print(f”パス: {path}”) # パスがない場合は None になる
else:
print(f”‘{url_no_path}’: URLパターンにマッチしませんでした。”)
出力:
元のURL: https://www.example.com/path/to/page.html
プロトコル: https
ドメイン: www.example.com
パス: /path/to/page.html

元のURL: http://sub.example.co.jp
プロトコル: http
ドメイン: sub.example.co.jp
パス: None
“`
このように、正規表現のキャプチャ機能を使えば、特定の構造を持つ文字列から必要な要素だけを簡単に分解・抽出できます。

4.5 例5:ログファイルからの特定の行の抽出

ログファイルから「ERROR」という単語を含む行のうち、タイムスタンプとエラーメッセージを抽出する例を考えます。ログのフォーマットが YYYY-MM-DD HH:MM:SS ERROR: エラーメッセージ のような形式だと仮定します。

正規表現:^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ERROR: (.*)$
* ^: 行の先頭
* (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}): YYYY-MM-DD HH:MM:SS の形式にマッチし、キャプチャ(タイムスタンプ)-> グループ1 (\1)
* ERROR:: リテラルの ERROR: (スペースも含む)
* (.*): 任意の文字 . が0回以上 * 続く部分にマッチし、キャプチャ(エラーメッセージ)。行末まで全てを捉えます。-> グループ2 (\2)
* $: 行の末尾

“`python
import re

log_data = “””
INFO: User login successful at 2023-10-27 10:00:01
WARNING: Low disk space on /dev/sda1 at 2023-10-27 10:05:30
ERROR: Database connection failed at 2023-10-27 10:10:45
INFO: Data processed at 2023-10-27 10:15:00
ERROR: File not found: /path/to/file.txt at 2023-10-27 10:20:10
“””

pattern = r”^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ERROR: (.*)$”

error_entries = []

re.findall() は、パターンに一致する全ての部分(またはグループ)をリストで返す

パターンにグループが含まれる場合、グループのタプルのリストを返す

matches = re.findall(pattern, log_data, re.MULTILINE) # re.MULTILINEフラグは ^と$ が各行の先頭/末尾にマッチするようにする

print(“— エラーログ抽出 —“)
if matches:
for timestamp, message in matches:
print(f”日時: {timestamp}, メッセージ: {message}”)
else:
print(“エラーログは見つかりませんでした。”)

出力:
— エラーログ抽出 —
日時: 2023-10-27 10:10:45, メッセージ: Database connection failed at 2023-10-27 10:10:45
日時: 2023-10-27 10:20:10, メッセージ: File not found: /path/to/file.txt at 2023-10-27 10:20:10
“`
このように、正規表現を使えば、構造化が不完全なテキストデータからでも、必要な情報をパターンマッチングと抽出によって効率的に取り出すことができます。

第5章:正規表現を使う上での注意点と落とし穴

正規表現は非常に強力ですが、使う上で注意すべき点や、思わぬ落とし穴があります。

5.1 可読性

複雑な正規表現は、記号が羅列されていて非常に読みにくくなります。書いた本人でさえ、しばらく経つと意味を理解するのが難しくなることがあります。

対策:
* コメントを使う: 多くの正規表現エンジンやライブラリでは、コメントを埋め込むための構文が用意されています(例: Pythonの re.VERBOSE フラグや (?#...) 構文)。これを利用して、パターンの各部分が何を示しているのかを記述しましょう。
* 分割する: あまりに巨大で複雑なパターンは、複数の小さなパターンに分割できないか検討しましょう。
* 名前付きキャプチャグループを使う: (?P<name>...) のように名前を付けてキャプチャグループを定義できる場合、後方参照を \g<name> のように名前で指定できるため、可読性が向上します。
* テストコードを書く: 意図した通りに動作するか、様々な入力でテストしましょう。

“`python

例: re.VERBOSE を使ったコメント付きパターン

import re

pattern_verbose = re.compile(r”””
^ # 行の先頭
(?P\d{4}) # 4桁の数字 (年) をキャプチャ
/ # リテラルの ‘/’
(?P\d{2}) # 2桁の数字 (月) をキャプチャ
/ # リテラルの ‘/’
(?P\d{2}) # 2桁の数字 (日) をキャプチャ
$ # 行の末尾
“””, re.VERBOSE) # re.VERBOSE フラグを付ける

match = pattern_verbose.match(“2023/10/27”)
if match:
print(match.group(“year”)) # 名前でキャプチャグループにアクセス
print(match.group(“month”))
print(match.group(“day”))
“`

5.2 パフォーマンス

ほとんどの正規表現は高速に動作しますが、特定のパターン(特にバックトラックが多発するもの)や非常に長い入力文字列に対しては、著しくパフォーマンスが低下することがあります。これは「正規表現のカタストロフィック・バックトラッキング」などと呼ばれます。

典型的な例として、^(a+)+$ のようなパターンで、長い文字列 aaaaaaaaaaaaaaaaaaaaaaaaaaaaX を検証しようとすると、正規表現エンジンは大量のバックトラックを試行し、処理に非常に時間がかかります。

対策:
* 冗長な量指定子を避ける: (a+)+ のように、同じようなパターンを繰り返し量指定子で囲む場合は注意が必要です。
* 否定文字クラスを使う: .* の代わりに [^...] のように、マッチしない文字を具体的に指定できる場合はそちらを使った方が効率的なことが多いです(例: HTMLタグ解析の <.*?> より <[^>]*?>)。
* オンラインテスターで性能をチェックする: regex101.com などでは、パターンのマッチングステップを表示してくれる機能があり、バックトラックの回数などを確認できます。
* 代替手段を検討する: 正規表現でパフォーマンス問題が発生する場合、よりシンプルな文字列操作関数を使ったり、専用のパーサーライブラリを使ったりすることも検討しましょう。

5.3 正規表現ではできないこと

正規表現は強力ですが万能ではありません。特に、以下のような構造を正確に解析するのは苦手、あるいは不可能です。

  • ネストした構造: HTML/XMLのようにタグが入れ子になった構造や、プログラムコードのようにカッコ (), {}, [] が対応して入れ子になっている構造を、そのネストの深さに関わらず正確に解析することはできません。これは、正規表現が基本的に有限オートマトンに基づいており、再帰的な構造を追跡する能力がないためです。(ただし、一部の正規表現エンジンでは再帰パターンや平衡グループなどがサポートされている場合もありますが、一般的ではありませんし可読性が低下します)
    • 例: <tag1><tag2>...</tag2>...</tag1> のような構造。
  • 意味解析: 文字列の文法的な正しさはある程度チェックできますが、その内容の「意味」を理解したり、文脈を判断したりすることはできません。

対策:
* 適切なツールを選ぶ: HTML/XMLの解析にはDOMパーサーやSAXパーサー、JSON/YAMLの解析にはそれぞれのライブラリを使うべきです。プログラムコードの解析には専用のパーサーやリンターが存在します。
* 正規表現はあくまでパターンマッチングに特化していると理解する。

5.4 方言 (Dialect)

正規表現の基本的な構文は多くの環境で共通していますが、細かな機能や挙動、サポートされているメタ文字の種類などが、使用する正規表現エンジン(ライブラリやツール)によって異なる場合があります。これを「方言」と呼びます。

例:
* POSIX正規表現(grepなど)は機能が比較的少ない。
* Perl互換正規表現(PCRE, PHP, Pythonのデフォルト, Ruby, JavaScriptなど)は機能が豊富でよく使われる。
* Javaの正規表現はPCREに近いが、一部異なる点がある。
* JavaScriptの正規表現はPCREをベースとしているが、最近まで後読みがサポートされていなかったなど、機能が制限されていることがあった(ES2018でサポートされた)。

対策:
* 使用する言語やツールのドキュメントを確認する: 自分が使っている環境の正規表現の仕様を必ず確認しましょう。特に、先読み・後読み、原子グループ化、Unicodeプロパティなどの高度な機能を使う場合は注意が必要です。
* 移植性に注意する: 複数の環境で同じ正規表現を使いたい場合は、共通してサポートされている基本的な構文のみを使うのが安全です。

これらの注意点を意識することで、正規表現をより安全かつ効率的に、そして意図した通りに使いこなすことができるようになります。

第6章:正規表現の学習方法とツール

正規表現は一度に全てを覚える必要はありません。基本的な要素から始め、必要に応じて新しい記号や機能を学び、実践を通じて慣れていくのが最も効果的な方法です。

6.1 オンラインテスターを活用する

正規表現を学習する上で最も役立つツールの一つがオンラインテスターです。

  • regex101.com: 非常に高機能で有名です。正規表現を入力すると、対象文字列のどこにマッチしたかがハイライトされ、さらにパターンの各部分が何にマッチしているのか、各メタ文字の意味は何なのかを詳細に解説してくれます。また、マッチングステップを表示するデバッガー機能や、パフォーマンス分析機能もあります。使用する言語(Python, JavaScript, PHP, Goなど)を選択して、その方言に合わせたテストが可能です。
  • regexr.com: こちらも直感的で使いやすいテスターです。メタ文字の説明や使用例がサイドバーに表示されるため、リファレンスとしても役立ちます。
  • rubular.com: Rubyの正規表現に特化したテスターですが、基本的な構文は共通なので他の言語でも参考になります。

これらのツールを使って、自分で作った正規表現が期待通りに動くか確認したり、他の人が書いた正規表現を読み解いたり練習したりしましょう。

6.2 ドキュメントや書籍を読む

使用しているプログラミング言語やツールの公式ドキュメントの正規表現に関する章は、正確な仕様を知る上で非常に重要です。また、正規表現の入門書や解説サイトもたくさんあります。図解や具体的な例が豊富なものを選ぶと理解が進みやすいでしょう。

6.3 練習問題を解く

オンラインには正規表現の練習問題を提供しているサイトがいくつかあります。例えば、「〜という条件を満たす文字列にマッチする正規表現を書きなさい」といった形式の問題を解くことで、実践的なスキルが身につきます。

6.4 少しずつ、必要に応じて学ぶ

最初から全てのメタ文字や機能を覚えようとすると挫折しやすいかもしれません。まずはリテラル文字、., *, +, ?, [], \d, \w, ^, $ など、基本的な要素から始めましょう。そして、「こういうパターンにマッチさせたいんだけど、どうすればいいんだろう?」と思った時に、その解決策として新しいメタ文字や機能を調べて学ぶ、というスタイルがおすすめです。

例えば、「数字が3回から5回繰り返すパターンにマッチさせたい」 -> 「回数を指定する量指定子 {} があるのか」 -> {3,5} と学ぶ、といった具合です。

第7章:まとめ – 正規表現をあなたの武器に

この記事では、正規表現の基本から、その用途、主要な構文要素、具体的なコード例、そして使う上での注意点や学習方法までを詳細に解説しました。

正規表現は、文字列の中に潜む複雑なパターンを表現し、検索、置換、抽出、検証といった処理を効率的に行うための非常に強力なツールです。最初は多くの記号に戸惑うかもしれませんが、それぞれの記号が持つ意味と、それらを組み合わせるルールを一つずつ理解していけば、決して難しいものではありません。

正規表現を習得することは、あなたのプログラミングスキルやデータ処理能力を一段階引き上げることに繋がります。この記事で紹介した基本的な要素と具体的な例を参考に、まずは簡単なパターンから実際に書いて動かしてみてください。オンラインテスターを活用したり、身近な文字列処理のタスクに正規表現を応用してみたりしながら、実践を繰り返すことが上達への一番の近道です。

正規表現の世界は奥深く、この記事で紹介したのはその入り口に過ぎません。しかし、この入門編で学んだ知識があれば、さらに高度なパターンや機能にも挑戦していくことができるはずです。

さあ、今日から正規表現をあなたの強力な武器として、文字列処理の様々な課題に立ち向かっていきましょう!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール