正規表現とは?初心者向けに基本をわかりやすく解説

はい、承知いたしました。正規表現について、初心者向けに約5000語の詳細な解説記事を記述します。記事の内容を直接表示します。


正規表現とは?初心者向けに基本をわかりやすく徹底解説

はじめに:文字列操作の強力な武器、正規表現の世界へようこそ

私たちのデジタルライフは、文字列に囲まれています。ウェブサイトのテキスト、メールの本文、プログラムのコード、データベースのエントリ、コマンドラインでの操作…これらすべては文字列で構成されています。そして、私たちはこれらの文字列の中から特定の情報を見つけ出したり、決まったパターンを持つ部分を別の文字列に置き換えたりする作業を日常的に行っています。

簡単な検索や置換であれば、多くのソフトウェアに備わっている「検索・置換」機能で十分かもしれません。例えば、「猫」という単語を「犬」に置き換える、といった単純な作業です。しかし、もしあなたが以下のような課題に直面したらどうでしょうか?

  • 「電話番号の形式(例: 090-xxxx-yyyy や 03-xxxx-yyyy)に合致する文字列だけをリストアップしたい」
  • 「全てのメールアドレスを文字列の中から探し出したい」
  • 「HTMLタグ <p></p> に囲まれたテキストだけを取り出したい」
  • YYYY-MM-DD 形式の日付を MM/DD/YYYY 形式に一括変換したい」
  • 「特定のキーワード(例えば『セール』や『割引』)が含まれている行だけを抽出したい」

これらの少し複雑な、あるいは「パターン」を持つ文字列の操作を行う際に、手作業や単純な検索・置換機能では対応が困難になります。ここで登場するのが、正規表現(Regular Expression)です。

正規表現とは、文字列のパターンを表現するための特殊な文字列です。この特殊な文字列を使うことで、あなたは非常に柔軟で強力な文字列の検索、置換、抽出、検証を行うことができるようになります。プログラミング言語、テキストエディタ、コマンドラインツール、データベースなど、正規表現は実に様々な場所で利用できます。

「正規表現は難しい」「記号がたくさんあってとっつきにくい」というイメージをお持ちの方もいるかもしれません。確かに、最初は見慣れない記号の羅列に戸惑うことがあるかもしれません。しかし、正規表現は決して魔法の呪文ではなく、一つ一つの記号やルールには明確な意味があります。基本から順序立てて学んでいけば、その強力な力を理解し、使いこなせるようになります。

この記事は、正規表現に初めて触れる方を対象に、その基本概念から応用的なテクニックまでを、約5000語のボリュームで徹底的に、そして分かりやすく解説することを目的としています。複雑な理論は避け、具体的な例を豊富に交えながら、「どう書けば、どんなパターンにマッチするのか」を直感的に理解できるように努めます。

さあ、正規表現という強力なツールを手に入れて、文字列操作の達人への第一歩を踏み出しましょう!

第1章:正規表現の超基本 ― リテラル文字と最初のメタ文字

正規表現は、特定の「パターン」を持つ文字列を探し出すための道具です。そのパターンを記述するのが、正規表現そのものです。正規表現は、大きく分けて二種類の文字で構成されます。

  1. リテラル文字 (Literal Characters): その文字自体にマッチする普通の文字です。例えば、正規表現 a は文字列中の a という文字にマッチします。正規表現 cat は文字列中の cat という並びにマッチします。
  2. メタ文字 (Metacharacters): 特殊な意味を持つ文字です。これらの文字が、正規表現を強力で柔軟なものにしています。例えば、. というメタ文字は「任意の1文字」にマッチするという意味を持ちます。

まずは、最も基本的なリテラル文字と、いくつかの重要なメタ文字から見ていきましょう。

1.1 リテラル文字:文字通りにマッチ

これは最も簡単です。正規表現の中で特殊な意味を持たない文字(アルファベット、数字など)は、その文字自体にマッチします。

  • 正規表現 hello は、文字列中の "hello" という並びに正確にマッチします。
  • 正規表現 123 は、文字列中の "123" という並びに正確にマッチします。
  • 正規表現 . は、通常はメタ文字ですが、もしあなたが . そのものにマッチさせたい場合は、後述するエスケープが必要です。

文字列 "hello world" に対して正規表現 world を適用すれば、world の部分がマッチします。

1.2 最初のメタ文字:. (ドット) ― 任意の1文字

ドット . は、正規表現で最もよく使われるメタ文字の一つです。これは、改行文字を除く任意の1文字にマッチします(正規表現エンジンの設定によっては、改行文字にもマッチするように変更できますが、デフォルトでは多くの環境で改行にはマッチしません)。

例:
* 正規表現 a.c は、"abc""aac""axc" など、a で始まり、c で終わり、その間に任意の1文字がある文字列にマッチします。
* 文字列 "abc": a.c にマッチします (a -> a, . -> b, c -> c)
* 文字列 "axc": a.c にマッチします (a -> a, . -> x, c -> c)
* 文字列 "ac": マッチしません (ac の間に1文字がないため)
* 文字列 "abbc": マッチしません (ac の間に1文字しかないパターンなので)

1.3 最初のメタ文字:* (アスタリスク) ― 直前の要素の0回以上の繰り返し

アスタリスク * は、直前の要素(文字、メタ文字、または後述するグループなど)が0回以上繰り返されるパターンにマッチします。

例:
* 正規表現 a* は、"a""aa""aaa" などの "a" が繰り返される文字列にマッチします。また、"b""" (空文字列) のように、"a" が全く出現しない場合にもマッチします(0回の繰り返し)。
* 文字列 "aaaa": a* にマッチします (a が4回)
* 文字列 "b": a* にマッチします (a が0回)
* 文字列 "" (空文字列): a* にマッチします (a が0回)

  • 正規表現 ab*c は、"ac" (bが0回)、"abc" (bが1回)、"abbc" (bが2回) などにマッチします。
    • 文字列 "ac": ab*c にマッチします (a -> a, b* -> (bが0回), c -> c)
    • 文字列 "abbbc": ab*c にマッチします (a -> a, b* -> (bが3回), c -> c)
    • 文字列 "axc": マッチしません (bが0回以上必要)

1.4 最初のメタ文字:+ (プラス) ― 直前の要素の1回以上の繰り返し

プラス + は、直前の要素が1回以上繰り返されるパターンにマッチします。* と似ていますが、0回の場合はマッチしない点が異なります。

例:
* 正規表現 a+ は、"a""aa""aaa" など、"a" が1回以上繰り返される文字列にマッチします。"b""" (空文字列) にはマッチしません。
* 文字列 "aaaa": a+ にマッチします (a が4回)
* 文字列 "a": a+ にマッチします (a が1回)
* 文字列 "b": マッチしません (a が0回)

  • 正規表現 ab+c は、"abc" (bが1回)、"abbc" (bが2回) などにマッチします。"ac" にはマッチしません。
    • 文字列 "abc": ab+c にマッチします (a -> a, b+ -> (bが1回), c -> c)
    • 文字列 "abbbbc": ab+c にマッチします (a -> a, b+ -> (bが4回), c -> c)
    • 文字列 "ac": マッチしません (bが1回以上必要)

1.5 最初のメタ文字:? (クエスチョン) ― 直前の要素の0回または1回の出現

クエスチョン ? は、直前の要素が0回または1回出現するパターンにマッチします。「省略可能」や「あってもなくても良い」といった意味で使われます。

例:
* 正規表現 colou?r は、"color""colour" の両方にマッチします。u が0回または1回出現するためです。
* 文字列 "color": colou?r にマッチします (u が0回)
* 文字列 "colour": colou?r にマッチします (u が1回)
* 文字列 "colouur": マッチしません (u が2回)

  • 正規表現 a?b は、"ab" (aが1回) と "b" (aが0回) にマッチします。
    • 文字列 "ab": a?b にマッチします (a? -> a, b -> b)
    • 文字列 "b": a?b にマッチします (a? -> (aが0回), b -> b)

1.6 最初のメタ文字:\ (バックスラッシュ) ― エスケープ文字

正規表現で特殊な意味を持つメタ文字(., *, +, ?, \, (, ), [, ], {, }, ^, $, | など)を、文字通りの意味でマッチさせたい場合があります。このようなときには、メタ文字の直前にバックスラッシュ \ を付けます。これをエスケープと呼びます。

例:
* 正規表現 . は任意の1文字にマッチしますが、正規表現 \.文字通りのドット . にマッチします。
* 正規表現 * は直前の要素の0回以上の繰り返しにマッチしますが、正規表現 \*文字通りのアスタリスク * にマッチします。

  • 正規表現 コストは\$50です は、文字列 "コストは$50です" にマッチします。ここで \$ は文字通りの $ にマッチしています。もし $ をエスケープしないと、後述する「行末」という意味になってしまいます。
  • 正規表現 ファイル名はfile\.txtです は、文字列 "ファイル名はfile.txtです" にマッチします。ここで \. は文字通りの . にマッチしています。もし . をエスケープしないと、file の後の任意の1文字にマッチしてしまい、fileatxtfilebtxt にもマッチしてしまう可能性があります。

まとめると、特殊な意味を持つ文字そのものにマッチさせたいときは、その文字の前に \ を付けると覚えておきましょう。

第2章:文字クラス ― 特定の文字群にマッチさせる

特定の「種類」の文字のいずれか1文字にマッチさせたい場合があります。例えば、「数字のどれか1つ」とか「アルファベットのどれか1つ」といったパターンです。このようなときに便利なのが文字クラスです。文字クラスは [] (角括弧) を使って表現します。

2.1 []:指定した文字のいずれか1文字にマッチ

角括弧 [] の中に文字を列挙すると、その中に含まれるいずれか1文字にマッチします。

例:
* 正規表現 [abc] は、"a""b"、または "c" のいずれか1文字にマッチします。
* 文字列 "apple": [abc] は最初の a にマッチ
* 文字列 "banana": [abc] は最初の b にマッチ
* 文字列 "cherry": [abc] は最初の c にマッチ
* 文字列 "date": [abc] はいずれにもマッチしない

  • 正規表現 [aeiou] は、英語の母音のいずれか1文字にマッチします。
  • 正規表現 [0123456789] は、数字のいずれか1文字にマッチします。

2.2 ハイフン -:文字の範囲を指定

文字クラス [] の中でハイフン - を使うと、文字の範囲を指定できます。これは連続した文字にマッチさせたい場合に非常に便利です。文字コード順(ASCIIやUnicodeなど)に基づいて範囲が指定されます。

例:
* 正規表現 [a-z] は、小文字のアルファベット a から z までのいずれか1文字にマッチします。
* 正規表現 [A-Z] は、大文字のアルファベット A から Z までのいずれか1文字にマッチします。
* 正規表現 [0-9] は、数字 0 から 9 までのいずれか1文字にマッチします。これは [0123456789] と同じ意味です。

複数の範囲や個別の文字を組み合わせて指定することもできます。
* 正規表現 [a-zA-Z] は、大文字または小文字のアルファベットのいずれか1文字にマッチします。
* 正規表現 [a-zA-Z0-9] は、アルファベットまたは数字のいずれか1文字にマッチします。これは「英数字」にマッチする際に非常によく使われます。
* 正規表現 [0-9a-fA-F] は、16進数の数字(0-9とa-f, A-F)のいずれか1文字にマッチします。

文字クラス内でハイフン - そのものにマッチさせたい場合は、ハイフンを文字クラスの最初か最後に置くか、または \- のようにエスケープします。
* 正規表現 [-az] は、-a、または z のいずれか1文字にマッチします。
* 正規表現 [az-] は、az、または - のいずれか1文字にマッチします。
* 正規表現 [a\-z] は、a-、または z のいずれか1文字にマッチします。

2.3 [^...]:指定した文字以外の1文字にマッチ

文字クラス [] の先頭に ^ (カレット) を置くと、その文字クラスに含まれていない以外の文字のいずれか1文字にマッチします。これは否定文字クラスと呼ばれます。

例:
* 正規表現 [^0-9] は、数字以外の任意の1文字にマッチします。
* 正規表現 [^aeiou] は、母音以外の任意の1文字にマッチします。
* 正規表現 [^a-zA-Z0-9] は、英数字以外の任意の1文字にマッチします(空白、記号などが含まれます)。

  • 文字列 "Hello 123!":
    • [^aeiou]H にマッチ
    • [^0-9]H にマッチ
    • [^a-zA-Z0-9] は空白にマッチ

注意点として、否定文字クラス [^...] は、あくまで「何か1文字」にマッチしますが、その文字が [] の中に指定されたものではない、という意味です。空文字列にはマッチしません。また、多くの正規表現エンジンでは、デフォルトで改行文字にはマッチしません。

2.4 よく使う文字クラスの省略記法

特定のよく使われる文字クラスには、便利な省略記法(エスケープシーケンス)が用意されています。これらを使うことで、正規表現をより短く、読みやすく記述できます。

  • \d: 数字にマッチします。[0-9] と同じ意味です。
    • \d"7""3" にマッチします。
  • \D: 数字以外の文字にマッチします。[^0-9] と同じ意味です。
    • \D"a""$"、空白などにマッチします。
  • \w: 単語構成文字にマッチします。これは通常、アルファベット、数字、アンダースコア _ のいずれかです。多くの環境で [a-zA-Z0-9_] と同じ意味です。
    • \w"A""7""_" にマッチします。
  • \W: 単語構成文字以外の文字にマッチします。[^a-zA-Z0-9_] と同じ意味です。
    • \W は空白、"#% などにマッチします。
  • \s: 空白文字にマッチします。スペース、タブ (\t)、改行 (\n, \r)、フォームフィード (\f)、垂直タブ (\v) などが含まれます。多くの環境で [\t\n\r\f\v ] と同じ意味です。
    • \s は半角スペース、タブ、改行にマッチします。
  • \S: 空白文字以外の文字にマッチします。[^\t\n\r\f\v ] と同じ意味です。
    • \S はアルファベット、数字、記号など、空白以外の全てにマッチします。

これらの省略記法と、先に学んだ *, +, ? などの量指定子を組み合わせることで、より複雑なパターンを簡単に記述できます。

例:
* 正規表現 \d+ は、1桁以上の数字の並び(整数など)にマッチします。"123""99" にマッチします。
* 正規表現 \w+\s+\w+ は、「単語 + 1つ以上の空白 + 単語」のようなパターンにマッチします。例えば "hello world" にマッチします。
* \w+: 単語構成文字が1回以上 (hello)
* \s+: 空白文字が1回以上 (スペース)
* \w+: 単語構成文字が1回以上 (world)

第3章:量指定子 ― 繰り返しの回数を指定する

第1章で *, +, ? といった繰り返しに関するメタ文字を学びました。これらは量指定子 (Quantifiers) と呼ばれ、直前の要素が何回繰り返されるかを指定します。ここでは、より細かく繰り返しの回数を制御できる量指定子を紹介します。

3.1 {n}:直前の要素のちょうどn回の繰り返し

波括弧 {} の中に数字 n を入れると、直前の要素が厳密にn回繰り返されるパターンにマッチします。

例:
* 正規表現 \d{3} は、数字がちょうど3回繰り返されるパターン、つまり3桁の数字にマッチします。"123" にはマッチしますが、"12""1234" にはマッチしません。
* 正規表現 a{2} は、"aa" にだけマッチします。
* 正規表現 .{5} は、改行を除く任意の文字がちょうど5回繰り返されるパターンにマッチします。

3.2 {n,}:直前の要素のn回以上の繰り返し

波括弧 {} の中に n, と記述すると、直前の要素がn回以上繰り返されるパターンにマッチします。

例:
* 正規表現 a{2,} は、"aa""aaa""aaaa" など、"a" が2回以上繰り返されるパターンにマッチします。"a" にはマッチしません。
* 正規表現 \d{5,} は、5桁以上の数字にマッチします。"12345""987654" にマッチします。

+{1,} と、*{0,} とそれぞれ同じ意味になります。

3.3 {n,m}:直前の要素のn回以上m回以下の繰り返し

波括弧 {} の中に n,m と記述すると、直前の要素がn回以上m回以下の範囲で繰り返されるパターンにマッチします。

例:
* 正規表現 a{2,4} は、"aa" (2回)、"aaa" (3回)、"aaaa" (4回) にマッチします。"a""aaaaa" にはマッチしません。
* 正規表現 \d{3,5} は、3桁、4桁、または5桁の数字にマッチします。"123""1234""12345" にマッチします。

?{0,1} と同じ意味になります。

3.4 貪欲(Greedy)マッチと非貪欲(Lazy)マッチ

量指定子 (*, +, ?, {n,}, {n,m}) には、デフォルトで貪欲(Greedy)マッチという性質があります。これは、マッチ可能な最も長い文字列を捉えようとする振る舞いです。

例:
文字列 "<a>bbb</a>" に対して正規表現 <.*> を適用してみましょう。
* < にマッチ
* . (任意の1文字) が * (0回以上) 繰り返されます。
* > にマッチ

Greedyマッチの場合、. は可能な限り多くの文字にマッチしようとします。結果、.*"a>bbb</a" という部分全体にマッチし、最後の > が正規表現の最後の > にマッチします。
マッチ結果: <a>bbb</a>

しかし、あなたが <a.*?> のように、それぞれのタグだけ (<a></a>) にマッチさせたい場合、このGreedyマッチは意図しない結果を招きます。

Greedyマッチの振る舞いを変更するには、量指定子の直後に ? を付けます。これを非貪欲(Lazy)マッチまたは最小マッチと呼びます。Lazyマッチは、マッチ可能な最も短い文字列を捉えようとします。

例:
文字列 "<a>bbb</a>" に対して正規表現 <.*?> を適用してみましょう。
* < にマッチ
* . (任意の1文字) が *? (0回以上、Lazy) 繰り返されます。
* > にマッチ

Lazyマッチの場合、.*? は最小限の繰り返しで後続の > にマッチできるかを探します。最初の < の直後から試し、> が見つかればそこでマッチを確定します。
1. < にマッチ。
2. . が0回 (*?) の繰り返しで、次の文字は a。正規表現の次は > だが、a> ではない。
3. . が1回 (*?) の繰り返しで、次の文字は a。正規表現の次は > だが、a> ではない。
4. …
5. . が1回 (*?) の繰り返しで、次の文字は a。正規表現の次は > だが、a> ではない。
6. . が1回 (*?) の繰り返しで、次の文字は >。正規表現の次は > で、一致する! ここでマッチを確定する。

結果、最初の <.*?>"<a>" にマッチします。

同様に、文字列 "<a>bbb</a>" に対して正規表現 <.+?> を適用すると、これもLazyマッチになります。+? なので直前の要素 (.) は1回以上繰り返されます。
* < にマッチ。
* .+?. が1回以上繰り返しで、次の > にマッチできる最短を探す。
* . が1回: a
* . が2回: ab
* …
* . が3回: abb。次に > が来るので、abb.+? にマッチする最短となる。
* > にマッチ。

結果、<.+?>"<a>" にマッチします。

別の例:文字列 "<b>Hello</b><i>World</i>"
* 正規表現 <.*> (Greedy): "<b>Hello</b><i>World</i>" 全体にマッチ
* 正規表現 <.*?> (Lazy): "<b>" にマッチし、次に "</b>" にマッチし、次に "<i>" にマッチし、次に "</i>" にマッチします(繰り返し検索した場合)。

このように、HTMLタグのような「開始タグ…終了タグ」のパターンで、内側の内容に別のタグが含まれる可能性がある場合、Greedyマッチでは意図せず複数のタグをまたいでマッチしてしまうことがあります。このようなケースではLazyマッチ (*?, +?) が役立ちます。

Lazyマッチは、直前の量指定子に対して ? を追加するだけで有効になります。
* *?: 0回以上、最小マッチ
* +?: 1回以上、最小マッチ
* ??: 0回または1回、最小マッチ (これは ? 自体が {0,1} のGreedyなので、ほとんど使われませんが文法的には可能です)
* {n,}?: n回以上、最小マッチ
* {n,m}?: n回以上m回以下、最小マッチ

どちらを使うべきかは、マッチさせたいパターンによって異なります。デフォルトのGreedyマッチの振る舞いを理解し、必要に応じてLazyマッチを使い分けることが重要です。

第4章:位置指定子 (アンカー) ― 特定の位置にマッチさせる

これまでに学んだ正規表現は、文字列中の「どこか」にあるパターンにマッチしました。しかし、特定の「位置」にパターンが出現するかどうかを指定したい場合があります。例えば、「行の先頭」に特定の単語があるか、あるいは「単語の境界」にマッチさせたい、といったケースです。このようなときに使用するのが位置指定子 (Anchors) です。位置指定子は、文字列の文字そのものではなく、文字と文字の間、または文字列の端といった「位置」にマッチします。

4.1 ^:行の先頭

^ (カレット) は、行の先頭にマッチします。

例:
* 正規表現 ^Hello は、文字列が "Hello" で始まる場合にマッチします。
* 文字列 "Hello world": マッチします
* 文字列 "Oh, Hello world": マッチしません
* 文字列 "Hello\nworld": マッチします (デフォルトでは)

多くの正規表現エンジンでは、複数行モード (Multiline mode、後述のフラグ m など) を有効にしない限り、^ は文字列全体の先頭にのみマッチします。複数行モードが有効な場合、^ は各行の先頭(改行文字 \n の直後)にもマッチするようになります。

4.2 $:行の末尾

$ (ドル記号) は、行の末尾にマッチします。これは、行の最後の文字の直後、または文字列の末尾にマッチします。

例:
* 正規表現 world$ は、文字列が "world" で終わる場合にマッチします。
* 文字列 "Hello world": マッチします
* 文字列 "Hello world!": マッチしません
* 文字列 "Hello world\n": マッチします (多くの環境で、末尾の改行文字の直前にもマッチします)

^ と同様に、複数行モード (フラグ m) が有効な場合、$ は各行の末尾(改行文字 \n の直前)にもマッチするようになります。

^パターン$ のように組み合わせると、「パターン」が文字列全体(または行全体)に完全に一致する場合にのみマッチさせることができます。これは、入力値のバリデーション(検証)によく使われます。
* 正規表現 ^\d{3}$ は、文字列全体がちょうど3桁の数字である場合にのみマッチします。"123" にはマッチしますが、"1234""abc""123a" にはマッチしません。

4.3 \b:単語境界 (Word Boundary)

\b は、単語境界にマッチします。単語境界とは、単語構成文字 (\w) と非単語構成文字 (\W) の間、または文字列の先頭/末尾のことです。具体的には、以下の4つのいずれかの位置にマッチします。

  1. 文字列の先頭で、最初の文字が \w の場合
  2. 文字列の末尾で、最後の文字が \w の場合
  3. \w の文字の直後に \W の文字が続く場合
  4. \W の文字の直後に \w の文字が続く場合

例:
* 正規表現 \bcat\b は、単独の単語として "cat" が出現する場合にマッチします。
* 文字列 "The cat sat.": "cat" の部分にマッチします。((\W) と c(\w) の間、t(\w) と .(\W) の間)
* 文字列 "category": マッチしません (c の前も y の後も \w)
* 文字列 "concatenate": マッチしません (c の前も e の後も \w)

\b は、特定の単語を正確に検索したい場合に非常に役立ちます。

4.4 \B:非単語境界 (Non-Word Boundary)

\B は、非単語境界にマッチします。これは、\b がマッチする位置以外の全ての位置です。つまり、単語構成文字と単語構成文字の間、または非単語構成文字と非単語構成文字の間にマッチします。

例:
* 正規表現 \Bcat\B は、"cat" という並びが単語の一部として出現する場合にマッチします。
* 文字列 "category": "cat" の部分にマッチします。(c の前は t(\w) の後、t の後は e(\w) の前)
* 文字列 "The cat sat.": マッチしません。
* 文字列 "concatenate": "cat" の部分にマッチします。(c の前は e(\w) の後、t の後は e(\w) の前)

\b\B は、セットで覚えておくと理解しやすいでしょう。

4.5 \A:文字列の先頭

\A は、文字列全体の先頭にのみマッチします。これは複数行モード (フラグ m) の影響を受けません。常に文字列の絶対的な先頭にマッチします。

例:
* 文字列 "Hello\nworld" に対して
* 正規表現 ^Hello (複数行モードでない場合): "Hello" にマッチ
* 正規表現 ^world (複数行モードでない場合): マッチしない
* 正規表現 \AHello: "Hello" にマッチ
* 正規表現 \Aworld: マッチしない

多くの言語では、複数行モードでない場合の ^\A は同じ振る舞いをしますが、複数行モードでは ^ が各行の先頭にマッチするのに対し、\A は文字列全体の先頭にしかマッチしません。

4.6 \Z / \z:文字列の末尾

\Z は文字列全体の末尾にマッチします。多くの環境では、末尾が改行文字で終わっている場合、その改行の直前にもマッチします。
\z は文字列全体の末尾にのみマッチし、末尾の改行文字による影響を受けません。つまり、文字列の最後の文字の直後にのみマッチします。

例:
* 文字列 "Hello world\n" に対して
* 正規表現 world$ (複数行モードでない場合): マッチします (\n の直前)
* 正規表現 \Z : マッチします (\n の直前)
* 正規表現 \z : マッチしません (\n の直前ではなく、文字列の絶対的な末尾)
* 文字列 "Hello world" (末尾に改行なし) に対して
* 正規表現 world$ (複数行モードでない場合): マッチします
* 正規表現 \Z : マッチします
* 正規表現 \z : マッチします

\Z\z の違いは微妙ですが、特に末尾の改行を厳密に区別したい場合に重要になります。多くの場合は $ で十分ですが、厳密性を求めるなら \A\z を使うと良いでしょう。

第5章:グループ化とキャプチャ ― パターンの一部をまとめて扱う

複数の文字やパターンをひとまとまりとして扱いたい場合があります。例えば、「ab という並び全体を3回繰り返したい」とか、「マッチした文字列のうち、特定の部分だけを取り出したい」といったケースです。このようなときには、() (丸括弧) を使ってパターンをグループ化します。

5.1 ():パターンのグループ化とキャプチャ

丸括弧 () は、以下の二つの主な目的で使用されます。

  1. グループ化: 複数の文字やメタ文字をまとめて一つの単位として扱います。これにより、直後に量指定子を適用したり、後述の | (選択) と組み合わせて複雑なパターンを記述したりできます。
  2. キャプチャ: マッチした文字列のうち、括弧で囲まれた部分(グループ)を記憶しておき、後で参照したり(後方参照)、プログラム側で取り出したりできるようにします。

例(グループ化):
* 正規表現 abc+ は、"abcc""abccc" など、"abc"c だけが繰り返されるパターンにマッチします。
* 正規表現 (abc)+ は、"abc""abcabc""abcabcabc" など、"abc" という並び全体が1回以上繰り返されるパターンにマッチします。量指定子 +(abc) というグループ全体に適用されています。
* 正規表現 (ab){2} は、"abab" にマッチします。

例(キャプチャ):
正規表現でマッチングを行う際、多くの場合、マッチした文字列全体だけでなく、その中の特定の部分を取り出したいニーズがあります。() で囲まれたグループは、自動的にその部分文字列を「キャプチャ」します。これらのキャプチャされた部分は、通常、1から順に番号が割り当てられます(0番目はマッチした文字列全体)。

  • 正規表現 (\d{4})-(\d{2})-(\d{2})
    • この正規表現は、YYYY-MM-DD 形式の日付にマッチします。
    • (\d{4}): 4桁の数字のグループ (年) -> キャプチャグループ1
    • -: ハイフンにマッチ
    • (\d{2}): 2桁の数字のグループ (月) -> キャプチャグループ2
    • -: ハイフンにマッチ
    • (\d{2}): 2桁の数字のグループ (日) -> キャプチャグループ3

文字列 "今日は2023-10-27です。" に対してこの正規表現を適用すると、"2023-10-27" がマッチします。
そして、各キャプチャグループには以下の内容が格納されます。
* グループ0 (全体): "2023-10-27"
* グループ1: "2023"
* グループ2: "10"
* グループ3: "27"

これらのキャプチャされた内容は、プログラミング言語の正規表現ライブラリなどを使って後で簡単に取り出すことができます。例えば、「YYYY-MM-DD形式の日付を見つけたら、それをMM/DD/YYYY形式に変換して置換する」といった処理に使えます。置換の際に、キャプチャした内容を参照して新しい文字列を組み立てるのです。

5.2 後方参照 (Backreferences)

キャプチャされた内容は、正規表現の同じパターン内で後から参照することも可能です。これを後方参照と呼びます。後方参照は、\ とグループの番号を組み合わせて記述します。\1 は1番目のキャプチャグループの内容、\2 は2番目のキャプチャグループの内容、といった具合です。

例:
* 正規表現 (\w+)\s+\1
* (\w+): 1つ以上の単語構成文字のグループ -> キャプチャグループ1
* \s+: 1つ以上の空白文字
* \1: 1番目のキャプチャグループと同じ内容にマッチ

この正規表現は、「同じ単語が空白を挟んで繰り返されているパターン」にマッチします。
* 文字列 "hello hello": マッチします (\1"hello")
* 文字列 "world world": マッチします (\1"world")
* 文字列 "hello world": マッチしません

  • 正規表現 "(\w+)"\s+'\1'
    • (\w+): 単語のグループ -> キャプチャグループ1
    • \s+: 1つ以上の空白
    • ': シングルクォート
    • \1: 1番目のキャプチャグループと同じ内容
    • ': シングルクォート

この正規表現は、例えば "apple" 'apple' のようなパターンにマッチします。

後方参照は、繰り返しの検出や、対応する括弧や引用符に囲まれた文字列の検索など、複雑なパターンマッチングに役立ちます。

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

グループ化はしたいけれど、その部分をキャプチャする必要がない場合もあります。キャプチャはメモリを消費したり、後で取り出す際のグループ番号がずれたりすることがあります。キャプチャが不要なグループは、(?:...) のように ?: を付けて記述します。これを非キャプチャグループまたはノンキャプチャグループと呼びます。

例:
* 正規表現 (?:abc)+ は、"abcabc" にマッチします。量指定子 +abc 全体に適用するためにグループ化していますが、このグループはキャプチャされません。
* もし (abc)+ とした場合は、グループ1に "abc" がキャプチャされます。
* (?:abc)+ とした場合は、グループはキャプチャされません。

非キャプチャグループを使うことで、パフォーマンスが向上したり、必要なキャプチャグループの番号をシンプルに保つことができます。

5.4 名前付きキャプチャグループ (言語依存)

多くの現代的な正規表現エンジン(Perl, Python, Java, .NETなど)では、キャプチャグループに番号ではなく名前を付ける名前付きキャプチャグループが利用できます。これは (?<name>...) または (?P<name>...) のような構文で記述します。

例(Python風):
* 正規表現 (?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})

この場合、マッチ後には "2023"year という名前で、"10"month という名前で、"27"day という名前で取り出すことができます。後方参照も (?P=name) のように名前で参照できる場合があります。

名前付きキャプチャグループは、正規表現の可読性を高め、プログラム側でキャプチャ内容を取り出す際に番号よりも分かりやすく管理できるという利点があります。ただし、使えるかどうかは使用する正規表現エンジンによります。

第6章:選択 (|) ― OR条件を指定する

正規表現で「このパターン、またはあのパターン」のように、複数のパターンのうちどれか一つにマッチさせたい場合があります。このようなOR条件を指定するには、パイプライン | (縦棒) を使用します。

6.1 |:OR条件

| は、その左側のパターン、または右側のパターンのいずれかにマッチします。

例:
* 正規表現 cat|dog は、文字列中の "cat" または "dog" のいずれかにマッチします。
* 文字列 "The cat sat on the mat.": "cat" にマッチ
* 文字列 "The dog barked.": "dog" にマッチ
* 文字列 "The cat and dog.": "cat""dog" の両方にマッチ(全てのマッチを検索する場合)

6.2 グループ化と組み合わせる

| は、デフォルトでは正規表現全体に適用されます。つまり、PatternA|PatternB は「PatternA にマッチ、または PatternB にマッチ」という意味になります。

もし、パターンの一部分に対してOR条件を適用したい場合は、第5章で学んだグループ化 () を使います。

例:
* 正規表現 (cat|dog) food は、「"cat food"」または「"dog food"」のいずれかにマッチします。
* 文字列 "I like cat food.": "cat food" にマッチ
* 文字列 "I like dog food.": "dog food" にマッチ
* 文字列 "I like cat or dog food.": "cat food" にマッチ(最初のマッチ)

このように、() でOR条件を適用したい部分を囲むことで、意図したパターンを作成できます。

6.3 | の優先順位に注意

| は、正規表現の中で最も優先順位が低い演算子の一つです。これは、正規表現全体(またはグループ)の中で最後に評価される傾向があるということです。

例えば、正規表現 ^abc|def$ を考えてみましょう。これは ^abc にマッチするか、または def$ にマッチするという意味になります。つまり、「行の先頭にabcがある」か「行の末尾にdefがある」かのどちらかにマッチします。
* 文字列 "abc xyz": ^abc の部分にマッチ
* 文字列 "xyz def": def$ の部分にマッチ
* 文字列 "abc def": ^abc の部分にマッチ(最初のマッチ)

もしあなたが「行の先頭にabcまたはdefがある」というパターンを表現したいのであれば、グループ化が必要です。
* 正規表現 ^(abc|def) は、「行の先頭にabcという並び、またはdefという並びがある」というパターンにマッチします。

このように、| を使う際は、グループ化が必要かどうかをよく検討することが重要です。

第7章:先読み・後読み (Lookahead/Lookbehind) ― マッチせず条件を指定する

これまでに学んだ正規表現の要素は、実際に文字列中の文字を「消費」しながらマッチを進めていきました。しかし、正規表現には、特定のパターンが「存在する」または「存在しない」という条件を、文字を消費せずに指定できる強力な機能があります。これが先読み (Lookahead)後読み (Lookbehind) です。これらを総称して肯定先読み・後読み (Lookarounds) と呼びます。

Lookaroundsはパターンにマッチしますが、マッチした文字列は最終的なマッチ結果には含まれず、正規表現エンジンの位置も移動しません。あくまで「その位置の前後で特定のパターンが見つかるか?」という条件チェックに使われます。

7.1 肯定先読み (?=...) (Positive Lookahead)

(?=...) は、現在の位置の直後に括弧内のパターンが存在する場合にマッチします。このマッチは文字を消費せず、正規表現エンジンの位置は移動しません。

例:
* 正規表現 Windows(?=\d)
* Windows: 文字列 "Windows" にマッチします。
* (?=\d): その直後に数字 (\d) が続くかどうかをチェックします。

この正規表現は、文字列中の "Windows" という単語そのものにマッチしますが、それはその "Windows" の直後に数字がある場合だけです。
* 文字列 "Windows95 and WindowsXP": 最初の "Windows" にマッチします。("Windows" の直後に "9" があるため)
* 2番目の "Windows" にはマッチしません。("Windows" の直後に "X" があり、数字ではないため)
* 文字列 "Windows is great": マッチしません。("Windows" の直後に空白があり、数字ではないため)

マッチ結果は "Windows" そのものであり、Lookaheadの (?=\d) の部分は結果に含まれません。

肯定先読みは、「特定の条件を満たすものの直前にあるもの」や「特定の条件を満たすもの自体だが、後続の状況も条件に含めたい」といった場合に有用です。例えば、「価格リストの中から、通貨記号 $ の直前にある数字だけを抽出したい」といった場合に (\d+)(?=\$?) のように使えます (例: 100$100 にマッチ)。

7.2 否定先読み (?!...) (Negative Lookahead)

(?!...) は、現在の位置の直後に括弧内のパターンが存在しない場合にマッチします。これも文字を消費せず、位置は移動しません。

例:
* 正規表現 Windows(?!\d)
* Windows: 文字列 "Windows" にマッチします。
* (?!=\d): その直後に数字 (\d) が続かないかどうかをチェックします。

この正規表現は、文字列中の "Windows" という単語そのものにマッチしますが、それはその "Windows" の直後に数字がない場合だけです。
* 文字列 "Windows95 and WindowsXP": 最初の "Windows" にはマッチしません。("Windows" の直後に "9" があり、数字であるため)
* 2番目の "Windows" にマッチします。("Windows" の直後に "X" があり、数字ではないため)
* 文字列 "Windows is great": マッチします。("Windows" の直後に空白があり、数字ではないため)

否定先読みは、「特定のパターンを含まない行を検索する」や「特定の拡張子を持たないファイル名にマッチする」といった場合に役立ちます。例えば、\.txt(?!(\.bak|\.old))$ のように使うと、「.txt で終わるが、.txt.bak.txt.old で終わらない」ファイル名にマッチさせることができます。

7.3 肯定後読み (?<=...) (Positive Lookbehind)

(?<=...) は、現在の位置の直前に括弧内のパターンが存在する場合にマッチします。これも文字を消費せず、位置は移動しません。

例:
* 正規表現 (?<=\$)\d+
* (?<=\$)\d+: その直前に $ が存在するかどうかをチェックします。
* \d+: 1つ以上の数字にマッチします。

この正規表現は、$ の直後に続く1つ以上の数字にマッチします。$ そのものはマッチ結果に含まれません。
* 文字列 "Price: $100 and $200": "$100" から 100 の部分と、"$200" から 200 の部分にそれぞれマッチします。
* 文字列 "Total: 500": マッチしません。(500 の直前に $ がないため)

肯定後読みは、「特定の接頭辞に続く部分文字列だけを抽出したい」といった場合に非常に有用です。

注意点として、多くの正規表現エンジンでは、Lookbehind (?<=?<!) の括弧内のパターンは固定長である必要があります。つまり、(?<=abc)(?<=\d{3}) のように、マッチする文字数が決まっているパターンでなければならないことが多いです。(?<=a*)(?<=\w+) のような可変長のLookbehindは、サポートされていないか、パフォーマンスが非常に低下する場合があります。ただし、Perlや最新のJava、.NETなど、一部のエンジンでは可変長のLookbehindもサポートされています。

7.4 否定後読み (?<!...) (Negative Lookbehind)

(?<!...) は、現在の位置の直前に括弧内のパターンが存在しない場合にマッチします。これも文字を消費せず、位置は移動しません。

例:
* 正規表現 (?<!\$)\d+
* (?<!\$)\d+: その直前に $ が存在しないかどうかをチェックします。
* \d+: 1つ以上の数字にマッチします。

この正規表現は、$ 以外の文字の直後に続く1つ以上の数字にマッチします。
* 文字列 "Price: $100 and Total: 500": "Total: 500" から 500 の部分にマッチします。(500 の直前が : で、$ ではないため)
* "$100" から 100 の部分にはマッチしません。(100 の直前が $ で、$ であるため)

否定後読みは、「特定の接頭辞を持たないパターン」にマッチさせたい場合に有用です。

Lookaroundsは非常に強力ですが、その分複雑さも増します。最初は肯定・否定の先読み・後読みの概念を理解し、簡単な例で試してみることから始めましょう。そして、必要に応じて実際のパターンで活用してみてください。特に、マッチ結果に特定の文字列を含めずに、その「文脈」だけを条件にしたい場合に役立ちます。

第8章:フラグ / オプション ― 正規表現の振る舞いを変更する

正規表現の検索・置換の際には、そのマッチングの振る舞いを変更するための様々なフラグ (Flags) またはオプション (Options) を指定できます。これらのフラグは、正規表現パターンの外部(多くのプログラミング言語やツールでの正規表現関数の引数として)で指定するか、またはパターンの内部で特殊な構文を使って指定します。

代表的なフラグをいくつか紹介します。

8.1 i (大文字小文字を区別しない – Case-insensitive)

このフラグを指定すると、正規表現はマッチングの際に大文字と小文字を区別しなくなります。

例:
* 正規表現 apple (フラグなし): "apple" にはマッチしますが、"Apple""APPLE" にはマッチしません。
* 正規表現 apple (フラグ i 付き): "apple""Apple""APPLE""aPpLe" など、大文字小文字を問わず "apple" という並びにマッチします。

これは、ユーザー入力の検索や、特定の単語の出現を拾い集めたい場合などに非常に便利です。

8.2 g (全てのマッチを検索 – Global)

このフラグを指定すると、正規表現エンジンは文字列中で最初に見つかったマッチだけでなく、全てのマッチを検索するようになります。プログラミング言語の検索関数や、エディタの「すべて置換」機能などでは、このフラグがデフォルトで有効になっていることも多いです。

例:
* 文字列 "cat dog cat" に対して正規表現 cat を適用した場合:
* フラグなし: 最初の "cat" にのみマッチします。
* フラグ g 付き: 最初の "cat" と2番目の "cat" の両方にマッチします。

置換を行う際にも、この g フラグがなければ最初に見つかった1箇所しか置換されません。全てを置換したい場合は g フラグが必須です。

8.3 m (複数行モード – Multiline)

デフォルトでは、^ は文字列全体の先頭に、$ は文字列全体の末尾(または末尾の改行の直前)にのみマッチします。複数行モード (フラグ m) を有効にすると、^各行の先頭(文字列の先頭および改行 \n の直後)に、$各行の末尾(文字列の末尾および改行 \n の直前)にマッチするようになります。

例:
* 文字列 "Line 1\nLine 2\nLine 3" に対して正規表現 ^Line を適用した場合:
* フラグなし: 最初の "Line" (Line 1 の部分) にのみマッチします。
* フラグ m 付き: 最初の "Line" (Line 1 の部分)、2番目の "Line" (Line 2 の部分)、3番目の "Line" (Line 3 の部分) の全てにマッチします。

行単位で処理を行いたい場合や、各行の先頭・末尾に特定のパターンがあるかチェックしたい場合に非常に役立ちます。

8.4 s (ドットオール / シングルラインモード – Dotall / Singleline)

デフォルトでは、メタ文字 . は改行文字 \n にはマッチしません。ドットオールモード (フラグ s) を有効にすると、メタ文字 .改行文字を含む全ての文字にマッチするようになります。

例:
* 文字列 "Line 1\nLine 2" に対して正規表現 Line.*Line を適用した場合:
* フラグなし: マッチしません。(.* が改行 \n にマッチしないため、Line 1 の後でマッチが止まってしまう)
* フラグ s 付き: "Line 1\nLine 2" 全体にマッチします。(.* が改行 \n にもマッチするため)

テキスト全体を一つのまとまりとして扱い、改行を跨いでマッチングを行いたい場合にこのフラグが有用です。HTMLタグのように複数行にまたがる可能性のある構造にマッチさせる際に役立つことがあります。

8.5 その他のフラグ (言語・エンジン依存)

上記以外にも、様々なフラグが正規表現エンジンによって提供されています。例えば、コメント (#) を利用可能にするフラグ (x または (?x)), Unicodeに関連するフラグ (u など) などがあります。

8.6 フラグの指定方法

フラグの指定方法は、利用するプログラミング言語やツールによって異なります。

  • 多くのプログラミング言語では、正規表現オブジェクトを作成する際や、正規表現関数の引数としてフラグを指定します。
    • Python: re.search(pattern, text, flags=re.IGNORECASE | re.DOTALL)
    • JavaScript: /pattern/gi (パターンの最後に記述)
    • Java: Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.DOTALL)
  • 一部の正規表現エンジンでは、パターンの先頭に (?flags) のようなインラインフラグを記述することで、パターン内でフラグを有効にできます。例えば、(?i)apple はパターン全体で大文字小文字を区別しなくなります。(?i:apple) のように特定のグループだけに適用できる場合もあります。

使用している環境のドキュメントを確認して、利用可能なフラグと指定方法を把握することが重要です。

第9章:実践的な例で学ぶ ― これまでの知識を組み合わせる

これまでに学んだ基本要素、文字クラス、量指定子、位置指定子、グループ化、選択、Lookarounds、フラグなどを組み合わせることで、様々な現実的な文字列パターンにマッチする正規表現を作成できます。ここでは、いくつか実践的な例を見ていきましょう。

例1:簡単なメールアドレスの検証

非常に厳密なメールアドレスの検証は非常に複雑ですが、簡単な形式(例: [email protected])にマッチする正規表現を作成してみましょう。

要素を分解します:
* ユーザー名の部分 (user):英数字、アンダースコア _、ピリオド .、ハイフン - あたりが使われることが多い。
* アットマーク (@):必須
* ドメイン名の部分 (domain):英数字、ハイフン - あたりが使われることが多い。
* ドット (.):必須(ただし文字通りのドットなのでエスケープ \. が必要)
* トップレベルドメイン (com):アルファベット2文字以上。

これらの要素を組み合わせます。量指定子 + を使って、「1文字以上繰り返す」を指定します。

  • ユーザー名: [\w.-]+ (\w は英数字とアンダースコア、.- を文字クラスに追加)
  • アットマーク: @
  • ドメイン名: [\w-]+
  • ドット: \. (エスケープを忘れない)
  • トップレベルドメイン: [a-zA-Z]{2,} (アルファベットが2回以上)

これらを結合すると、次のようになります:
正規表現:[\w.-]+@[\w-]+\.[a-zA-Z]{2,}

この正規表現を文字列 "私のメールアドレスは [email protected] です。" に適用してみましょう。
* [\w.-]+: "test.user-123" にマッチ
* @: "@" にマッチ
* [\w-]+: "example" にマッチ
* \.: "." にマッチ
* [a-zA-Z]{2,}: "co" にマッチ
* \.: "." にマッチ
* [a-zA-Z]{2,}: "jp" にマッチ

したがって、"[email protected]" 全体にマッチします。

この正規表現はシンプル化されたものであり、RFCに準拠した厳密なメールアドレス形式全てには対応していません(例えば、サブドメインや特殊文字など)。しかし、多くの一般的な形式にはマッチさせることができます。

もし、この正規表現が文字列全体のメールアドレスに完全に一致するかを検証したい場合は、位置指定子 ^$ を使います。
正規表現:^[\w.-]+@[\w-]+\.[a-zA-Z]{2,}$
この場合、文字列全体がこのパターンに完全に一致しないとマッチしません。

例2:URLのプロトコル部分の抽出

URLの http:// または https:// の部分だけを抽出したいとします。

要素を分解します:
* http または https:選択 | を使います。http|https
* その後に s があってもなくても良い:s? を使います。(http|https)s? とも書けますが、より簡潔に https? とも書けます。(shttp の後に来るので、https? の方が自然でしょう)
* コロン ::文字通りのコロンにマッチ。
* スラッシュ2つ //:文字通りのスラッシュ2つにマッチ。//

これらを結合します。
正規表現:https?://

この正規表現は、文字列中の "http://" または "https://" という並びにマッチします。

もし、このプロトコル部分をキャプチャして取り出したい場合は、グループ化 () を使います。
正規表現:(https?://)
この場合、マッチした "http://" または "https://" という部分がキャプチャグループ1に格納されます。

例3:YYYY-MM-DD 形式から MM/DD/YYYY 形式への置換

文字列中の "YYYY-MM-DD" 形式の日付を "MM/DD/YYYY" 形式に変換したいとします。これは、正規表現の検索機能と、キャプチャグループを使った置換機能を組み合わせることで実現できます。

まず、"YYYY-MM-DD" 形式にマッチする正規表現を作成します。キャプチャグループを使って、年、月、日をそれぞれ個別に取得できるようにします。
正規表現:(\d{4})-(\d{2})-(\d{2})
* (\d{4}): 4桁の数字 (年) -> グループ1
* -: ハイフン
* (\d{2}): 2桁の数字 (月) -> グループ2
* -: ハイフン
* (\d{2}): 2桁の数字 (日) -> グループ3

次に、置換後の文字列を定義します。多くの正規表現ツールやプログラミング言語では、置換文字列の中で後方参照 (\1, \2, \3 など) を使って、キャプチャした内容を埋め込むことができます。

新しい形式は "MM/DD/YYYY" なので、置換文字列は以下のようになります。
置換文字列:\2/\3/\1
* \2: 2番目のキャプチャグループ (月)
* /: スラッシュ
* \3: 3番目のキャプチャグループ (日)
* /: スラッシュ
* \1: 1番目のキャプチャグループ (年)

例:
文字列 "会議は2023-10-27に開催され、締切は2023-10-20です。"
正規表現:(\d{4})-(\d{2})-(\d{2})
置換文字列:\2/\3/\1
全て置換 (グローバルフラグ g を使用) を実行すると:

  • 最初のマッチ "2023-10-27" (グループ1:”2023″, グループ2:”10″, グループ3:”27″) が 10/27/2023 に置換される。
  • 2番目のマッチ "2023-10-20" (グループ1:”2023″, グループ2:”10″, グループ3:”20″) が 10/20/2023 に置換される。

結果文字列:"会議は10/27/2023に開催され、締切は10/20/2023です。"

このように、グループ化と後方参照を組み合わせることで、複雑な文字列のフォーマット変換を自動化できます。

例4:HTMLタグの簡単な抽出 (注意点あり)

<p>...</p><b>...</b> のようなHTMLタグの内容を抽出したいとします。

要素を分解します:
* 開始タグ <: 文字通りの <
* タグ名: アルファベット1文字以上 [a-zA-Z]+
* 終了タグ >: 文字通りの >
* タグ内の内容: 任意の文字の繰り返し .*
* 終了タグ </: 文字通りの </
* 対応するタグ名: 開始タグと同じタグ名。後方参照 \1 を使います。
* 終了タグ >: 文字通りの >

これらを組み合わせ、タグ名をキャプチャグループ1に入れます。
正規表現:<([a-zA-Z]+)>.*<\/\1>

  • <: マッチ
  • ([a-zA-Z]+): タグ名をキャプチャ (例: "p", "b") -> グループ1
  • >: マッチ
  • .*: タグの内容にマッチ (Greedyマッチ)
  • <\/\1>: 文字通りの </ の後、キャプチャグループ1と同じタグ名が来て、> で閉じるパターンにマッチ

文字列 "This is a <p>paragraph</p> and <b>bold text</b>." に対してこの正規表現を適用してみましょう。
* 最初の <([a-zA-Z]+)><p> にマッチし、グループ1は "p"
* .* は Greedyマッチなので、可能な限り右側に進みます。次の <\/\1> (</p>) にマッチできる位置まで進むと、.*"paragraph</p> and <b>bold text" の部分にマッチします。
* <\/\1></p> にマッチ。

結果、"<p>paragraph</p> and <b>bold text</b>" 全体にマッチしてしまいます。Greedyマッチの問題ですね。

これを解決するには、タグの内容 .* を Lazyマッチ .*? に変更します。
正規表現:<([a-zA-Z]+)>.*?<\/\1>

  • <: マッチ
  • ([a-zA-Z]+): タグ名をキャプチャ (例: "p") -> グループ1
  • >: マッチ
  • .*?: タグの内容にマッチ (Lazyマッチ – 最短で次のパターンまで)
  • <\/\1>: 文字通りの </ の後、キャプチャグループ1と同じタグ名が来て、> で閉じるパターンにマッチ

文字列 "This is a <p>paragraph</p> and <b>bold text</b>."
* 最初の <([a-zA-Z]+)><p> にマッチし、グループ1は "p"
* .*? は Lazyマッチなので、次の <\/\1> (</p>) にマッチできる最短を探します。結果、"paragraph" の部分にマッチ。
* <\/\1></p> にマッチ。
マッチ結果1: "<p>paragraph</p>"

全てのマッチを検索すると:
* 次の <([a-zA-Z]+)><b> にマッチし、グループ1は "b"
* .*?"bold text" にマッチ。
* <\/\1></b> にマッチ。
マッチ結果2: "<b>bold text</b>"

このように、Lazyマッチを使うことで意図した結果を得られます。

重要な注意点: 正規表現でHTMLやXMLのような構造化されたマークアップ言語を正確にパース(解析)することは推奨されません。正規表現はフラットな文字列パターンには強いですが、HTMLのようなネストした構造(タグの中にさらにタグがある場合など)を正確に扱うのは非常に困難であり、予期しないバグやセキュリティ問題を引き起こす可能性があります。HTML/XMLの解析には、専用のパーサーライブラリ(Beautiful Soup, DOMパーサー, SAXパーサーなど)を使用すべきです。上記の例はあくまで正規表現の機能を示すためのものであり、実務でHTMLパースに正規表現を使うべきではありません。

例5:特定の単語を含む行の抽出

ログファイルなどから、特定のキーワード(例えば “ERROR”)を含む行だけを抽出したいとします。

  • 行全体にマッチさせたいので、行の先頭 ^ と末尾 $ を使います(複数行モード m が有効な場合)。
  • 行の中に特定のキーワード "ERROR" が含まれているかをチェックします。
  • キーワードの前後に任意の文字がある可能性があります。.* (0回以上の任意の文字) を使います。

正規表現:^.*ERROR.*$ (複数行モード m 付き)

  • ^: 行の先頭にマッチ
  • .*: 行の先頭から "ERROR" の直前までの任意の文字 (0回以上) にマッチ (Greedy)
  • ERROR: "ERROR" という文字列にマッチ
  • .*: "ERROR" の直後から行末までの任意の文字 (0回以上) にマッチ (Greedy)
  • $: 行の末尾にマッチ

例:文字列
INFO: Processing request...
ERROR: Database connection failed.
WARNING: Disk space low.
ERROR: File not found.

この文字列に対して正規表現 ^.*ERROR.*$ を複数行モードで適用すると、以下の2行にそれぞれマッチします。
* ERROR: Database connection failed.
* ERROR: File not found.

もし行の先頭に特定の単語がある行だけを抽出したい場合は、^ERROR.*$ のように記述できます。

これらの例のように、正規表現の各要素(基本文字、メタ文字、文字クラス、量指定子、位置指定子、グループ、選択、Lookarounds、フラグ)を組み合わせることで、様々なパターンを表現し、複雑な文字列操作を実現できます。

第10章:正規表現のデバッグとツール

複雑な正規表現を作成する際には、意図通りにマッチするかどうかを確認しながら進めることが重要です。間違った正規表現は、期待通りの結果が得られないだけでなく、意図しない文字列にマッチしてしまったり、処理速度が極端に遅くなったりする原因にもなります。正規表現の作成とデバッグに役立つツールや考え方を紹介します。

10.1 オンライン正規表現テスターの活用

正規表現を試したりデバッグしたりするのに最も便利なのが、オンラインの正規表現テスターサイトです。これらのサイトには、以下のような機能が備わっています。

  • 正規表現入力欄: 作成した正規表現を入力します。
  • テスト文字列入力欄: 正規表現を適用したい文字列を入力します。
  • リアルタイムマッチング: 入力した正規表現とテスト文字列に対して、リアルタイムでマッチ結果を表示してくれます。
  • マッチの詳細表示: マッチした部分文字列、キャプチャグループの内容などを詳しく表示してくれます。
  • 解説機能: 正規表現の各部分がどのような意味を持っているのか、初心者にも分かりやすく解説してくれる機能を持つサイトもあります。
  • フラグ指定: i, g, m, s などのフラグを簡単にオン/オフできます。
  • 言語/エンジン選択: Perl互換、JavaScript、Python、Java、.NETなど、様々な正規表現エンジンの種類を選択できるサイトもあります。これは、エンジンによって微妙に振る舞いが異なる場合があるため便利です。

代表的なオンライン正規表現テスターサイト:
* Regex101: 非常に高機能で、正規表現の各部分の解説、ステップ実行、置換機能なども備わっています。
* RegExr: 視覚的に分かりやすく、よく使うパターンやリファレンスも参照できます。
* Debuggex: 正規表現を視覚的な図で表示してくれるため、構造の理解に役立ちます。

これらのツールを積極的に活用することで、正規表現の学習効率を高め、正確なパターンを作成できるようになります。

10.2 正規表現を段階的に構築する

一度に全てを記述しようとせず、簡単な部分から段階的に構築していくのが良い方法です。

例えば、メールアドレスの正規表現を作る場合:
1. まずはユーザー名の部分の基本 \w から始め、\w+ と量を指定。
2. 次に許容される特殊文字(., -)を追加して [\w.-]+ とする。
3. アットマーク @ を追加。
4. ドメイン名部分 [\w-]+ を追加。
5. ドット \. を追加。
6. トップレベルドメイン [a-zA-Z]{2,} を追加。
7. 全体のパターン [\w.-]+@[\w-]+\.[a-zA-Z]{2,} が完成。
8. 必要であれば、文字列全体にマッチさせるために ^$ を追加。

このように、小さな部品を作り、それらを組み合わせていくことで、複雑なパターンも管理しやすくなります。各段階でテスト文字列を使って意図通りにマッチするか確認しましょう。

10.3 テスト文字列の重要性

作成した正規表現をテストする際には、様々な種類のテスト文字列を用意することが重要です。

  • マッチするべき文字列: 想定通りのパターンが含まれている文字列。
  • マッチしないべき文字列: 想定外のパターンや、似ているが少し違うパターンが含まれている文字列。
  • エッジケース: 空文字列、非常に長い文字列、特殊文字が含まれる文字列など。

これらのテスト文字列に対して正規表現を適用し、期待通りの結果が得られるかを確認することで、正規表現の正確性を高めることができます。

10.4 可読性を高める工夫 (エンジン依存)

複雑な正規表現は、自分自身でも後から読み解くのが難しくなることがあります。一部の正規表現エンジンでは、以下のような方法で正規表現の可読性を高めることができます。

  • コメント: (?# comment) のような構文や、フラグ x (または (?x)) を使ってパターン中にコメントを記述できるようになります。フラグ x を使うと、パターン中の空白文字が無視されるため、改行やインデントを使ってパターンを整形することも可能になります。
    “`regex
    (?x) # コメントと空白を有効にするフラグ
    ^ # 行の先頭
    (?P\d{4}) # 年 (4桁の数字)

    • ハイフン

      (?P\d{2}) # 月 (2桁の数字)

    • ハイフン

      (?P\d{2}) # 日 (2桁の数字)
      $ # 行の末尾
      “`
      このように書くと、正規表現の意味が格段に分かりやすくなります。ただし、この機能はすべてのエンジンでサポートされているわけではありません。

  • 名前付きキャプチャグループ: 第5章で紹介した名前付きキャプチャグループも、可読性向上に役立ちます。

複雑な正規表現を保守していくためには、これらの可読性向上の工夫を取り入れることを検討しましょう。

第11章:正規表現の限界と注意点

正規表現は非常に強力なツールですが、万能ではありません。また、使い方によっては予期しない問題を引き起こす可能性もあります。ここでは、正規表現の限界と使用上の注意点について触れておきます。

11.1 構造化されたデータやネストした構造の解析には不向き

第9章のHTMLタグの例でも触れましたが、正規表現はHTML、XML、JSONなどのように要素がネスト(入れ子)になった構造化されたデータの解析には基本的に不向きです。

例えば、対応する開始タグと終了タグのペアを、ネストの深さに関係なく正確にマッチさせることは、正規表現単独では非常に困難、あるいは不可能です(一部のエンジンで再帰参照などを使えば可能になることもありますが、非常に複雑で可読性が低くなります)。

これらの構造化されたデータを扱う場合は、専用のパーサーライブラリを使用すべきです。パーサーは、データの構造を正しく解釈し、効率的かつ安全にデータにアクセスする機能を提供します。

11.2 可読性の問題

複雑な正規表現は、記号の羅列のように見え、作成者本人でさえ後から見ると意味を読み解くのが困難になることがあります。チームで開発している場合など、他の人が理解できない正規表現は保守の妨げになります。

  • 正規表現が複雑になりすぎる場合は、処理を複数のステップに分けたり、より可読性の高い別の方法(文字列操作関数の組み合わせなど)を検討したりしましょう。
  • コメントや名前付きキャプチャグループなど、利用可能な可読性向上の工夫を取り入れましょう(ただし、エンジン互換性に注意)。
  • 正規表現の意味をドキュメントに残すことも重要です。

11.3 パフォーマンスの問題 (バックトラック)

正規表現のマッチング処理は、特定のパターンにおいて計算量が爆発的に増大し、処理が極端に遅くなることがあります。これはバックトラックと呼ばれる正規表現エンジンの探索メカニズムに起因することが多いです。

正規表現エンジンは、パターンを文字列にマッチさせようと試み、一致しない場合に前の位置に戻って別の可能性を試す(バックトラック)ことで解を探します。非常に曖昧な量指定子(例: .*.+ の Greedy マッチ)や、入れ子になった量指定子(例: (.*)*)を含む複雑なパターンを、特定の種類の文字列(例: 多数の同じ文字の繰り返しや、バックトラックを誘発する構造を持つ文字列)に適用した場合、バックトラックの回数が指数関数的に増加し、CPU時間を大量に消費してしまうことがあります。これは ReDoS (Regular Expression Denial of Service) と呼ばれる脆弱性にもつながることがあります。

対策としては:
* 曖昧な量指定子の使用を避けるか、Lazyマッチ (.*?) を検討する。
* 文字クラス [] や否定文字クラス [^...] を積極的に活用し、マッチする文字の種類を限定する。
* Lookaroundsなどでマッチしない部分のパターンを正確に指定する。
* 複雑すぎるパターンは分割する。
* 信頼できない外部からの入力に対して正規表現を適用する場合は、タイムアウトを設定するなどの対策を検討する。
* パフォーマンスが問題になる場合は、プロファイリングツールなどで正規表現のマッチに時間がかかっていないか確認する。

11.4 エンジンによる振る舞いの違い

正規表現の標準仕様は存在しますが、実際の正規表現エンジンはPerl互換、POSIX準拠、Java固有など、様々な実装があり、サポートしているメタ文字や構文、フラグ、そして微妙な振る舞い(特に改行コードの扱い、Lookbehindの固定長制約、バックトラックの挙動など)に違いがあります。

ある環境で期待通りに動作した正規表現が、別の環境では動作しない、あるいは異なる結果になるということが起こり得ます。特定の言語やツールで正規表現を使用する際は、その環境の正規表現エンジンのドキュメントを参照し、サポートされている機能や仕様を確認することが重要です。

まとめ:正規表現マスターへの道

この記事では、正規表現の最も基本的な概念から、文字クラス、量指定子、位置指定子、グループ化、選択、Lookarounds、そしてフラグといった応用的な機能までを、初心者向けに体系的に解説してきました。また、実践的な例を通じて、これらの知識を組み合わせて実際の課題を解決する方法を示しました。

正規表現は、一見複雑そうに見える記号の羅列ですが、それぞれの記号や構文には明確な意味があります。一つずつ基本要素を理解し、それらを組み合わせていくことで、強力な文字列処理の能力を手に入れることができます。

正規表現を使いこなせるようになるためには、以下のステップを続けることが重要です。

  1. 基本をマスターする: リテラル文字、., *, +, ?, \, [], \d, \w, \s, ^, $, () といった基本的な要素の意味と使い方をしっかりと覚えましょう。
  2. 実際に試す: オンラインテスターやプログラミング環境を使って、簡単な正規表現から実際に書いて試してみましょう。様々な文字列に対して試すことで、その振る舞いを感覚的に掴めます。
  3. 複雑なパターンに挑戦する: 学んだ要素を組み合わせて、少しずつ複雑なパターンに挑戦しましょう。メールアドレスやURLなどの例は良い練習台になります。
  4. デバッグツールを活用する: 意図通りにマッチしない場合は、オンラインテスターの解説機能やステップ実行機能を使って、正規表現エンジンのマッチングプロセスを理解しましょう。
  5. リファレンスを参照する: 忘れそうな構文や、特定の機能(Lookaroundsなど)の使い方をすぐに参照できるリファレンスを用意しておきましょう。インターネット上には多くの優れた正規表現リファレンスがあります。
  6. 実践で使う: 日々のプログラミング作業、テキスト編集、コマンドライン操作などで、正規表現が使えそうな場面を見つけたら積極的に活用してみましょう。実践が最も効率的な学習方法です。

正規表現は、データを扱う多くの場面であなたの作業効率を劇的に向上させてくれるポテンシャルを持っています。最初は難しく感じるかもしれませんが、諦めずに一歩ずつ学び進めれば、必ず使いこなせるようになります。

この記事が、あなたの正規表現学習の強力な第一歩となれば幸いです。さあ、正規表現の世界へ飛び込み、文字列操作の新たな可能性を切り開きましょう!


コメントする

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

上部へスクロール