-
Notifications
You must be signed in to change notification settings - Fork 0
MS_RegularExpression
- 戻る(その他、開発の色々)
正規表現(regular expression)
- 「文字列の集合」を「文字列の特徴 / パターンを表現する文字列」で表現する方法の一つであり、
- 正規表現によるパターン マッチを行なうことで、各種文字列処理(検索、置換)を行なうときに利用する非手続き言語。
正規表現のパターン マッチを使用すれば、例えば、次のような処理に利用可能である。
- 文字列の「特徴」(パターン)に一致するか検証する。
- 用途:入力チェックなどに使用可能
- API:Regex.IsMatch() メソッドを使用する(型.Parse( ) メソッドの強化版)。
- 文字列の「特徴」(パターン)に一致する部分文字列を抜き出す。
- 用途:全文検索処理などに使用可能
- API:Regex.Matches() メソッドを使用する(String.IndexOf( ) メソッドの強化版)。
- 文字列の「特徴」(パターン)に一致する部分文字列を置換する。
- 用途:置換(付加、削除、書式変更など)に使用可能
- API:Regex.Replace() メソッドを使用する(String.Replace( ) メソッドの強化版)。
- 文字列の「特徴」(パターン)に一致する文字列で文字列分割する。
- 用途:文字列分割処理に使用可能
- API:Regex.Split() メソッドを使用する(String.Split( ) メソッドの強化版)。
- .etc
移行メモ(誤記): 原文の「Regex.Matchs()」は
Regex.Matches()の誤記と判断し修正した。
現在では、Perl、PHP、Java、JavaScript、.NET、Python、Ruby など、
広く様々なランタイムに実装されており、テキスト エディタ、ワード プロセッサをはじめとする、
アプリケーション ソフトの検索・置換機能などで利用することもできるようになっている。
ただし、ソフトウェアによって「方言」や利用できない機能があるため、注意が必要である。
- 言語やアプリケーションで細部の仕様が異なっているが、POSIX により標準規格も定められそれに準拠するものも増えてきている。
- .NET の正規表現は、Perl5 の正規表現に対応するようにデザインされている。
- また、API オプションを指定することによって ECMAScript(JavaScript 等)と同様の正規表現に対応するように動作を変更できる。
補足(方言の実務的な影響): 「方言」は些細な違いに見えて、
移植時に静かに壊れる原因になる。特に差が出るのは次の点。
項目 .NET JavaScript POSIX (grep) 後読み (?<=)可変長も可 可(ES2018+) 不可 名前付きグループ (?<name>)(?<name>)(ES2018+)不可 \dの範囲Unicode 数字全部(全角も!) ASCII のみ ロケール依存 文字グループ減算 [a-z-[aeiou]]ありなし なし $の位置末尾の \nの前にも一致末尾のみ 行末
\dが全角数字「123」にも一致するのは .NET 特有で、
入力チェックで想定外の値を通す原因になりやすい。
ASCII だけを意図するなら[0-9]と明記するか、
RegexOptions.ECMAScriptを指定する。同様に
$は末尾の改行の前にも一致するため、
厳密な入力チェックでは\z(改行を含まない末尾)を使う。// "abc\n" にも一致してしまう Regex.IsMatch(input, @"^[a-z]+$"); // 厳密 Regex.IsMatch(input, @"\A[a-z]+\z");
正規表現は、
- 通常の文字と、
- 「メタ文字」
- 「エスケープ文字」
を組み合わせて表記される。
正規表現の書き方を習得するには、これらの要素を覚える必要がある。
特別な意味を持った記号。
以下が、.NET Framework の正規表現で使用できるメタ文字。
| 項番 | 区分 | メタ文字 | 説明 / 注釈 |
|---|---|---|---|
| 1 | 位置指定子 | 位置指定子 | 「 ^ 」、「 $ 」の両方を指定することで全文のマッチを強制できる。 エスケープ文字に含まれる位置指定子もある。 |
| 1-1 | 位置指定子 | ^ |
文字列の先頭を表す(中間からのマッチを拒否) ※ 正規表現、部分式の先頭のものしかメタ文字として認識されない。 |
| 1-2 | 位置指定子 | $ |
文字列の末尾を表す(中間からのマッチを拒否) ※ 正規表現、部分式の終端のものしかメタ文字として認識されない。 |
| 2 | 代替構成体 | 代替構成体 | 詳細は代替構成体を参照 |
| 2-1 | 代替構成体 | | |
「 | 」 で区切られた文字列のいずれか。 通常、下記、5-2 の丸括弧と併用するが、単独で利用することも可能。 |
| 3 | 量指定子 | 量指定子 | 正規表現での量指定子 https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/quantifiers-in-regular-expressions |
| 3-1 | 量指定子 | * |
前の文字列が 0 回以上連続にマッチする( = {0,})。 |
| 3-2 | 量指定子 | + |
前の文字列が 1 回以上連続にマッチする( = {1,})。 |
| 3-3 | 量指定子 | ? |
前の文字列が 0 or 1 回マッチする( = {0,1})。 |
| 3-4 | 量指定子 | {n} |
前の文字列がn回連続にマッチする。 |
| 3-5 | 量指定子 | {n,} |
前の文字列がn回以上連続にマッチする。 |
| 3-6 | 量指定子 | {n, m} |
前の文字列がn-m回連続にマッチする。 |
| 3-7 | 量指定子 |
*? ,etc. |
*、+、?、{ } の後に「 ? 」をつけて「最短マッチ」を表す。 |
| 4 | 文字クラス | 文字クラス | 正規表現での文字クラス https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/character-classes-in-regular-expressions |
| 4-1 | 文字クラス |
.(ピリオド) |
改行(\n)以外の任意の1文字 |
| 4-2 | 文字クラス |
[ ](角括弧) |
下記、5-1 に該当 |
| 4-3 | 文字クラス | \s |
スペースやタブなどの空白文字 |
| 4-4 | 文字クラス | \S |
\s 以外([^\s] と同義) |
| 4-5 | 文字クラス | \d |
10 進数([0-9] と同義) |
| 4-6 | 文字クラス | \D |
\d 以外([^\d] と同義) |
| 4-7 | 文字クラス | \w |
単語に使用される任意の文字 ( [\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Pc}\p{Lm}] と同義) |
| 4-8 | 文字クラス | \W |
\w 以外([^\w] と同義) |
| 5 | 括弧 | ||
| 5-1 | 括弧 |
[ ] 角括弧詳細は「角括弧による文字グループ」を参照。 |
下記の角括弧内に指定した文字にマッチする。 ・文字範囲(Unicode の文字コード表)、 ・文字グループ 以下は、角括弧内でのみ使われる特殊なメタ文字である。 ・「 - 」:範囲指定、例えば [a-z] で半角英小文字の文字グループを表す。・「 ^ 」:先頭で使われたとき、「含まれない」と条件を反転する。 |
| 5-2 | 括弧 |
( ) 丸括弧詳細は「丸括弧による構成体」を参照 |
丸括弧内に指定した文字、文字範囲、文字グループに完全にマッチする。 グループ化構成体、代替構成体、その他の構成体を構成する。 |
| 6 | 日本語文字クラス(\p{xxxx}) |
日本語文字クラス | .NET TIPS 文字列のひらがな/カタカナをチェックするには? - C# - @IT http://www.atmarkit.co.jp/fdotnet/dotnettips/054iskana/iskana.html |
| 6-1 | 日本語文字クラス | \p{IsHiragana} |
平仮名 |
| 6-2 | 日本語文字クラス | \p{IsKatakana} |
片仮名 |
| 6-3 | 日本語文字クラス | \p{IsCJKUnifiedIdeographs} |
漢字 |
| 6-4 | 日本語文字クラス | \P{xxxx} |
\p{xxxx} 以外 |
移行メモ(表の展開): 原文は PukiWiki のセル結合(
>/~)を
使用していたが、GitHub Markdown にセル結合が無いため、
同じ値を展開して掲載した。
また、4-4の説明「\S以外」は\s以外の誤記、
6-4の「P{xxxx}」は\P{xxxx}の表記漏れと判断し修正した。
補足(
\d\wは Unicode 全体を指す): 前述の通り、
.NET の既定では
\d…[0-9]だけでなく、全角数字・アラビア数字なども含む\w… 日本語の漢字・かなも「単語構成文字」に含む表の「
\dは[0-9]と同義」という記述は
RegexOptions.ECMAScriptを指定した場合に限って正しい。
既定では同義ではない点に注意する。
エスケープ文字 には次のようなものがある。
| 項番 | 区分 | 文字のエスケープ | 説明 / 注釈 |
|---|---|---|---|
| 1 | 位置指定子 | 位置指定子 | |
| 1-1 | 位置指定子 | \b |
ワードの境界(\w 文字と \W 文字の間)。例えば「 \bVB\b」で単語としての「VB」となる。既定では日本語と英語の境界は「 \b」に含まれない。従って、「 \bVB\b」という正規表現で、「これは、VB です」という文字列中の VB を検索できない。 ただし、これについては、ECMAScript オプションで動作を変更できる。 角括弧の中にある場合、置換パターンの場合は、下記、3-2 を参照。 |
| 1-2 | 位置指定子 | \B |
\b 以外 |
| 1-3 | 位置指定子 | \A |
Multiline オプションを無視した文字列の先頭 |
| 1-4 | 位置指定子 | \z |
Multiline オプションを無視した文字列の末尾 |
| 1-5 | 位置指定子 | \Z |
Multiline オプションを無視した文字列の末尾(または文字列の末尾の「\n」の前) |
| 1-6 | 位置指定子 | \G |
前回の一致が終了した位置で一致する必要があることを指定する。 |
| 2 | メタ文字のエスケープ | メタ文字のエスケープ | 単純に、前に \ 記号を付与する。 |
| 2-1 | メタ文字のエスケープ | \. |
. |
| 2-2 | メタ文字のエスケープ | \^ |
^ |
| 2-3 | メタ文字のエスケープ | \$ |
$ |
| 2-4 | メタ文字のエスケープ | \* |
* |
| 2-5 | メタ文字のエスケープ | \+ |
+ |
| 2-6 | メタ文字のエスケープ | \? |
? |
| 2-7 | メタ文字のエスケープ | \[ |
[ |
| 2-8 | メタ文字のエスケープ | \] |
] |
| 2-9 | メタ文字のエスケープ | \( |
( |
| 2-10 | メタ文字のエスケープ |
|(エスケープ) |
| |
| 2-11 | メタ文字のエスケープ | \) |
) |
| 2-12 | メタ文字のエスケープ | \{ |
{ |
| 2-13 | メタ文字のエスケープ | \} |
} |
| 2-14 | メタ文字のエスケープ | \\ |
\ |
| 3 | 制御文字 | 制御文字 | 各種 制御文字を正規表現中の文字列で表す場合。 |
| 3-1 | 制御文字 | \a |
ベル(アラーム)の \u0007
|
| 3-2 | 制御文字 | \b |
角括弧の中にある場合、置換パターンの場合は、\u0008
|
| 3-3 | 制御文字 | \t |
タブの \u0009
|
| 3-4 | 制御文字 | \r |
キャリッジ リターンの \u000D
|
| 3-5 | 制御文字 | \v |
垂直タブの \u000B
|
| 3-6 | 制御文字 | \f |
フォーム フィードの \u000C
|
| 3-7 | 制御文字 | \n |
改行文字の \u000A
|
| 3-8 | 制御文字 | \e |
エスケープ文字の \u001B
|
| 3-9 | 制御文字 | \cC |
ASCII 制御文字 と一致(\cC は Ctrl + C)http://ja.wikipedia.org/wiki/ASCII#ASCII.E5.88.B6.E5.BE.A1.E6.96.87.E5.AD.97 |
| 4 | コードポイント | コードポイント | 各種 コードポイントを正規表現中の文字列で表す場合。 |
| 4-1 | コードポイント | \040 |
8 進数(3 桁まで)で表される ASCII 文字と一致 |
| 4-2 | コードポイント | \x20 |
16 進数(2 桁)で表される ASCII 文字と一致 |
| 4-3 | コードポイント | \u0020 |
16 進数(4 桁)で表される Unicode 文字と一致 |
補足(
\bが日本語で効かない理由): 表の 1-1 が述べる通り、
\bは「\wと\Wの境界」である。
既定では日本語(かな・漢字)も\wに含まれるため、
「これは、VB です」の「、VB で」の部分に境界が生まれない。
RegexOptions.ECMAScriptを指定すると\wが
[a-zA-Z_0-9]に狭まるため、日本語との境界が\bになる。ただし ECMAScript オプションは
同時に指定できるオプションが限られる(IgnoreCase、Multiline、
Compiledのみ)ため、副作用に注意する。
代替として、明示的に境界を書く方法もある。// ECMAScript を使わずに「前後が英数字でない VB」を探す Regex.IsMatch(s, @"(?<![a-zA-Z0-9])VB(?![a-zA-Z0-9])");
補足(C# のリテラルでのエスケープ): 正規表現の
\は、
C# の文字列リテラルでも\がエスケープ文字であるため、
二重にエスケープが必要になり読みにくくなる。var a = "\\d+"; // 通常の文字列リテラル(読みにくい) var b = @"\d+"; // 逐語的文字列リテラル(推奨) var c = """\d+"""; // 生文字列リテラル(C# 11+。" も書ける)
@""を使うのが定石。"を含むパターンでは
C# 11 の生文字列リテラル"""..."""が便利である。
角括弧を使用した文字グループの定義には次のようなものがある。
※ 下記の、Chn(Ch1、Ch2、Ch3、Ch4 など)は文字を表す。
xx_ChGroup(Base_ChGroup、Excluded_ChGroup)は文字グループを表す。
[Ch1Ch2]
[Ch1-Ch2]
[Ch1-Ch2Ch3-Ch4]
[Base_ChGroup-[Excluded_ChGroup]]
.etc
[\p{xxxx}-[Excluded_ChGroup]]
[Base_ChGroup-[Ch1-Ch2\p{xxxx}]]
, etc.
補足(文字グループ減算は .NET 独自):
[a-z-[aeiou]]
(英小文字から母音を除く)という減算式は .NET 固有の機能であり、
JavaScript や Java には無い。
移植性が要る場面では、否定先読みで代替する。.NET [a-z-[aeiou]] 汎用 (?![aeiou])[a-z]
丸括弧を使用した
について説明する。
-
グループ化構成体は、正規表現の部分式を表す(=グループ化)。
-
正規表現にマッチした箇所はキャプチャされ、
キャプチャ文字列として、プログラム中で利用できる。 -
このグループ化構成体によるグループ化よって、
正規表現にマッチした箇所を部分的にキャプチャすることもできる。 -
以下の表は、
- グループ化構成体の一覧で、表中の「expression」は、正規表現の部分式を意味する。
| 項番 | 区分 | グループ化構成 | 説明 / 正規表現の例 | マッチする部分文字列 / キャプチャ文字列 |
|---|---|---|---|---|
| 1 | グループ化 | |||
| 1-1 | グループ化 | (expression) |
グループ化する箇所(丸括弧内をキャプチャする)。 | |
| 1-1 | グループ化 | (expression) |
"大森(一郎|二郎)" |
大森一郎または 大森二郎 にマッチ |
| 1-1 | グループ化 | (expression) |
キャプチャ文字列 | 大森一郎、大森二郎以外に一郎、二郎が追加でキャプチャされる。 |
| 1-2 | グループ化 | (?:expression) |
グループ化する箇所(丸括弧内をキャプチャしない)。 | |
| 1-2 | グループ化 | (?:expression) |
"大森(?:一郎|二郎)" |
大森一郎または 大森二郎 にマッチ |
| 1-2 | グループ化 | (?:expression) |
キャプチャ文字列 | 大森一郎、大森二郎がキャプチャされ、一郎、二郎はキャプチャされない |
| 1-3 | グループ化 |
(?<name>expression)※ < > は ' でも良い。 |
グループ化する箇所(名前を付けて丸括弧内をキャプチャする)。 | |
| 1-3 | グループ化 | (?<name>expression) |
"大森(?<name>一郎|二郎)" |
大森一郎または 大森二郎 にマッチ |
| 1-3 | グループ化 | (?<name>expression) |
キャプチャ文字列 | 大森一郎、大森二郎以外に一郎、二郎が追加でキャプチャされる。 ※ このキャプチャ文字列は、グループ名(name)を使用して参照できる。 |
| 1-4 | グループ化 | (?imnsx-imnsx: expression) |
指定したAPI オプションを部分式に適用(imnsx の部分)または無効(-imnsx の部分)にする。 正規表現のAPI オプション https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expression-options |
|
| 1-5 | グループ化 | (?<name1-name2>expression) |
グループ定義を均等化する。 | |
| 1-6 | グループ化 | (?>expression) |
非バックトラッキング部分式。 | |
| 2 | 先読み・後読み(戻り読み) | |||
| 2-1 | 先読み | (?=expression) |
直後にこのパターンが現れることを確認する。 | |
| 2-1 | 先読み | (?=expression) |
"\d+(?=%)" |
後ろに「%」が続く数字の連続にマッチ |
| 2-1 | 先読み | (?=expression) |
キャプチャ文字列 | 「%」の部分はキャプチャされない。例えば 100% で 100 がキャプチャされる。 |
| 2-2 | 後読み | (?<=expression) |
直前にこのパターンが現れることを確認する。 | |
| 2-2 | 後読み | (?<=expression) |
"(?<=\\)\d+" |
「\」に続く数字の連続にマッチ。 |
| 2-2 | 後読み | (?<=expression) |
キャプチャ文字列 | 「\」の部分はキャプチャされない。例えば \100 で 100 がキャプチャされる。 |
| 2-3 | 否定先読み | (?!expression) |
直後にこのパターンが現れないことを確認する。 | |
| 2-3 | 否定先読み | (?!expression) |
"\d+(?![\d%])" |
後ろに「%」が続かない数字の連続にマッチ。 |
| 2-3 | 否定先読み | (?!expression) |
キャプチャ文字列 | 「%」の部分はキャプチャされない。例えば 100KS で 100 がキャプチャされる。 |
| 2-4 | 否定後読み | (?<!expression) |
直前にこのパターンが現れないことを確認する。 | |
| 2-4 | 否定後読み | (?<!expression) |
"(?<![\d\\])\d+" |
「\」に続かない数字の連続にマッチ。 |
| 2-4 | 否定後読み | (?<!expression) |
キャプチャ文字列 | 「\」の部分はキャプチャされない。例えば $100 で 100 がキャプチャされる。 |
移行メモ: 原文はセル結合と
&color()による
「リテラル中の\のエスケープ」の強調を用いていたが、
GitHub Markdown ではいずれも表現できないため、
セルを展開し、正規表現はコード表記に置き換えた
(\\は C# リテラル中でのエスケープを表す)。
補足(
(?:)と(?>)の実務上の価値):
(?:expression)(非キャプチャ)
キャプチャ不要なグループには必ず付ける。
グループ番号がずれるのを防ぎ、性能も良くなる。
(?>expression)(アトミック グループ)
一度マッチした箇所を後戻り(バックトラック)させない。
後述の性能問題(ReDoS)の主要な対策である。危険 (a+)+$ → バックトラックが爆発する 安全 (?>a+)+$ → 後戻りしないので爆発しない
- グループ番号
キャプチャ文字列には、其々グループ番号が与えられる。-
正規表現にマッチした箇所全体のキャプチャ文字列は、グループ番号0となる。
-
グループ化により、追加でキャプチャされるキャプチャ文字列のグループ番号には、
グループの位置順(左から右)に1から始まる番号が与えられる。 -
例えば
-
"(ab)(cd)(ef)"という正規表現にマッチした場合、
-
-
<----------0---------->
( a b ) ( c d ) ( e f )
<--1--> <--2--> <--3-->
- 各グループのキャプチャ文字列に割り当てられる番号は次のようになる。
入力 : xxxabcdefxxx
・グループ0 : abcdef
・グループ1 : ab
・グループ2 : cd
・グループ3 : ef
移行メモ(誤記): 原文の「・グループ1 : ad」は、
正規表現(ab)に対応するので ab の誤記と判断し修正した。
-
入れ子にグループ化された場合、
グループのキャプチャ文字列に割り当てられる番号は、
外側から内側に順に1から始まる番号が与えられる。 -
例えば
-
"(a(b(c)(d)(e(f))))"という正規表現にマッチした場合、
-
<----------------0---------------->
( a ( b ( c ) ( d ) ( e ( f ) ) ) )
<-6->
<-3-> <-4-> <----5---->
<-------------2------------->
<----------------1---------------->
- 各グループのキャプチャ文字列に割り当てられる番号は次のようになる。
入力 : xxxabcdefxxx
・グループ0 : abcdef
・グループ1 : abcdef
・グループ2 : bcdef
・グループ3 : c
・グループ4 : d
・グループ5 : ef
・グループ6 : f
移行メモ(誤記): 原文は最後の 2 行がいずれも
「・グループ5」となっていたが、図からグループ6が正しいと判断し修正した。
- なお、グループ番号は、グループ名の付与の影響を受けない。
移行メモ(正誤): 最後の一文「グループ番号は、グループ名の付与の
影響を受けない」は、.NET では成り立たない。.NET では、
- 番号付きグループ(無名)に先に 1 から採番され、
- その後で、名前付きグループに続きの番号が採番される。
var m = Regex.Match("abc", @"(?<x>a)(b)(?<y>c)"); // グループ1 = "b" ← 無名グループが先 // グループ2 = "a" ← 名前付きは後回し // グループ3 = "c"番号と名前を混在させると、番号がずれる。
混在させる場合は
- 番号で参照しない(名前で参照する)、または
RegexOptions.ExplicitCaptureで無名グループのキャプチャを止める、のいずれかを徹底するのが安全である。
- 代替構成体は、選択、条件分岐を表す。
| 項番 | 代替構成体 | 説明 / 正規表現の例 | マッチする部分文字列 / キャプチャ文字列 |
|---|---|---|---|
| 1 | (?(expression1)expression2) |
expression1 に一致した場合、そこから expression2 で検索 | |
| 1 | (?(expression1)expression2) |
"(?(^大森).*郎(\r?\n|$))" |
大森一郎、大森二郎、大森三郎、などの行 |
| 1 | (?(expression1)expression2) |
キャプチャ文字列 | 先頭が「大森」の場合、末尾が「xxx 郎」の行があれば、その行がキャプチャされる。 |
| 2 | (?(expression)yes_exp|no_exp) |
expression に一致した場合、そこから yes_exp で検索 一致しなかった場合、そこから no_exp で検索 |
|
| 2 | (?(expression)yes_exp|no_exp) |
"(?(^大森).*郎(\r?\n|$)|^品川.*郎(\r?\n|$))" |
大森一郎、大森二郎、大森三郎、品川一郎、品川二郎、品川三郎、などの行 |
| 2 | (?(expression)yes_exp|no_exp) |
キャプチャ文字列 | 先頭が「大森」の場合、末尾が「xxx 郎」の行があれば、その行がキャプチャされる。 また、先頭が「大森」でない場合、先頭「品川」~末尾「xxx 郎」の行がキャプチャされる。 |
| 3 | (?(name)yes_exp|no_exp) |
割愛 |
その他の構成体は、正規表現を変更する部分式(API オプション、コメントなど)を表す。
| 項番 | その他の構成体 | 説明 |
|---|---|---|
| 1 | (?imnsx-imnsx) |
パターンの途中でAPI オプションを適用(imnsx の部分)または無効(-imnsx の部分)にする。 |
| 2 | (?# ) |
正規表現に挿入するインライン コメント。コメントは、最初の右かっこ文字で終了する。 |
| 3 |
#[行末まで] |
X モード コメント。コメントは行末まで継続する。 このコメントを認識させるには、以下の何れかのAPI オプションを有効にする。 ・x オプション ・IgnorePatternWhitespace オプション |
で参照(利用)できる。
ついて説明する。
前後方参照は、2つ連続する文字(文字グループ)のパターンを検索する場合などに使用する。
-
前後方参照には、以下の2つの前後方参照構成体を使用する方法がある。
-
グループ番号:
\number- 「
\1」から「\9」までの式は常に前後方参照を表し、8 進の ASCII コードにはならない。 - 「
\11」以降の複数桁の式は、その番号に対応する前後方参照がある場合には、前後方参照と見なされる。 - それ以外の場合は、8 進の ASCII コードと解釈される。
ただし、先頭の桁が 8 または 9 の場合を除く。
その場合はリテラルの "8" および "9" として扱われる。
- 「
-
グループ名:
\k<name>
-
-
正規表現の中に、未定義のグループ番号への前後方参照が含まれる場合は、
解析エラーと見なされる。曖昧さが問題となる場合は、名前を使用して前後方参照する。 -
下記は、
-
2つ連続する文字(文字グループ)のパターンを検索する「前後方参照」を使用した正規表現の例である。
-
「前後方参照」を使用した正規表現の例
- 2つ連続する文字のパターンを検索する
-
| グループ番号を使用 | グループ名を使用 | |
|---|---|---|
| 正規表現 前後方参照 | "(\S)\1" |
"(?<ch>\S)\k<ch>" |
- 2つ連続する文字グループのパターンを検索する
| グループ番号を使用 | グループ名を使用 | |
|---|---|---|
| 正規表現 前後方参照 | "\b(\S+)\s+\1\b" |
"\b(?<str>\S+)\s+\k<str>\b" |
移行メモ(誤記): 原文の見出し「2つ連続する文字クループ」は
文字グループの誤字と判断し修正した。
-
文字列置換は、
-
置換パターンに指定する置換構成体には、次のものがある。
| 項番 | 置換構成体 | 説明 |
|---|---|---|
| 1 | $number |
グループ番号と一致した部分文字列 |
| 2 | ${name} |
グループ名(name)と一致した最後の部分文字列 |
| 3 | $& |
一致したパターン全体の文字列 |
| 4 | $+ |
キャプチャされた最後の文字列 |
| 5 | $_ |
入力文字列全体 |
| 6 | $` |
一致した場所より前にある入力文字列 |
| 7 | $' |
一致した場所より後にある入力文字列 |
| 8 | $$ |
単一の "$" リテラル(エスケープ) |
-
次に、正規表現による文字列置換の例を示す。
- 付加
- HTML 中の URL にリンクを付ける。
※ 正規表現にマッチした箇所全体のキャプチャ文字列は、置換構成体「$0」でも「$&」でも参照できる。
- HTML 中の URL にリンクを付ける。
- 付加
| $0版 | $&版 | |
|---|---|---|
| 正規表現 | "s?https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+" |
同左 |
| 置換パターン | "<a href=\"$0\">$0</a>" |
"<a href=\"$&\">$&</a>" |
補足(この置換は実務では危険): 上記のような「URL を見つけて
<a>タグに変換する」置換は、そのまま HTML に出力すると
XSS の温床になる。
- 元の文字列に含まれる
<>"&を先にエスケープしていないjavascript:などのスキームを弾いていない現在は、HTML の生成に正規表現を使わないのが原則であり、
Razor の@出力(自動エスケープ)や
HtmlSanitizer のようなライブラリを使う。
- 文書中の行頭に引用記号を挿入する。\
※ 正規表現に位置指定子(アトミック ゼロ幅アサーション)を単独で使用した場合もキャプチャ → 置換ができる。
| 文字列 | |
|---|---|
| 正規表現 | "^" |
| 置換パターン | "> " |
- 削除
- 文書中の行末の空白文字を削除する。
※ 削除する場合、置換パターンに空文字列を指定する。
※ 実行時、Multiline オプションを設定する必要がある。
- 文書中の行末の空白文字を削除する。
| 文字列 | |
|---|---|
| 正規表現 | "[ \t]+(?=\r?\n|$)" |
| 置換パターン |
"" (空文字列) |
- 無効な文字を削除する。\
※ 削除する場合、置換パターンに空文字列を指定する。\
※ `@`、`-` (ハイフン)、および `.` (ピリオド) 以外のすべての非英数文字を取り除いた文字列を返す。
| 文字列 | |
|---|---|
| 正規表現 | "[^\w\.@-]" |
| 置換パターン |
"" (空文字列) |
- 文書中の連続する同じ行を削除する。\
※ 上記は、[前後方参照](#前後方参照)を併用している。\
※ 実行時、Multiline オプションを設定する必要がある。
| グループ番号を使用 | グループ名を使用 | |
|---|---|---|
| 正規表現 | "^(.*)(\r?\n\1)+$" |
"^(?<line>.*)(\r?\n\k<line>)+$" |
| 置換パターン | "$1" |
"${line}" |
- 書式変更
- 日付のフォーマットを変更する1
※ 文章中から検索、変換する場合は、前後に「\b」を付与する必要がある。
また、日本語文章中の場合は、実行時、ECMAScript オプションを設定する必要がある。
- 日付のフォーマットを変更する1
| グループ名を使用 | |
|---|---|
| 正規表現 | "(?<year>\d{2,4})/(?<month>\d{1,2})/(?<day>\d{1,2})" |
| 置換パターン | "${year}年${month}月${day}日" |
- 日付のフォーマットを変更する2
※ 文章中から検索、変換する場合は、前後に「\b」を付与する必要がある。
また、日本語文章中の場合は、実行時、ECMAScript オプションを設定する必要がある。
| グループ名を使用 | |
|---|---|
| 正規表現 | "(?<month>\d{1,2})/(?<day>\d{1,2})/(?<year>\d{2,4})" |
| 置換パターン | "${day}-${month}-${year}" |
- その他、MatchEvaluator デリゲートを使用して置換(変換)するという方法もあるが、
これについては、「API」で説明する。
デフォルトでは「最長マッチ」となるため、必要に応じて「最短マッチ」を使用する。
-
「最長マッチ」とは、検索される文字列が最長になるように処理することである。
-
例えば、カギかっこ(「」)で囲まれた文字列を検索する場合、
- 次のように正規表現を表記した場合に、
"「.*」"
- カギかっこが複数あった場合、問題が起こる。
- 例えば、入力文字列に、以下のように入力されているならば、
"・・・「あ」、「い」、「う」・・・"
- 検索される部分文字列は、以下の 1 つだけになる。
"「あ」、「い」、「う」"
- これを、以下の 3 つの文字列が別々に検索されるようにするには、最短マッチを使用する。
"「あ」"、"「い」"、"「う」"
- 「最短マッチ」とは、検索される文字列が最短になるように処理することである。
- 「最長マッチ」を「最短マッチ」にする場合、以下のように、書き換える必要がある。
"「.*」" → "「.*?」"
- ただし、上記のような例であれば、
「最短マッチ」を使用しなくても、"「[^」]*」"と書けばほぼ同じことができる。
移行メモ(誤記): 原文の「「最短マッチ」を「最短マッチ」にする場合」は
「最長マッチ」を「最短マッチ」にする場合の誤記と判断し修正した。
補足(最後の一文が実は最重要): 原文が「ほぼ同じことができる」と
控えめに書いている **"「[^」]*」"(否定文字クラス)**は、
最短マッチより望ましい書き方である。
「.*?」「[^」]*」動作 1 文字進めては閉じ括弧を試す(バックトラック) 一気に読む(後戻りしない) 速度 遅い 速い ReDoS 起こり得る 起きにくい **「最短マッチより、否定文字クラス」**は
正規表現の性能改善における定石である。
詳細は性能と ReDoSを参照。
正規表現を使用したパターン マッチの例を示す。
※ IgnoreCase(大文字と小文字を区別しない)オプションを設定する必要がある。
※ 文章中から検索する場合は、前後に「\b」を付与する必要がある。
また、日本語文章中の場合は、実行時、ECMAScript オプションを設定する必要がある。
| 正規表現 | |
|---|---|
| 正規表現 | "[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}" |
| 正規表現 | "([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([A-Z]{2,4}|[0-9]{1,3})(\]?)" |
補足(メールアドレスの検証に正規表現を使わない): 原文自身も
表現できないケースがあるで
同じ趣旨を述べているが、現在の .NET には明確な代替がある。// 推奨:MailAddress に任せる(RFC に沿った解釈) bool ok = System.Net.Mail.MailAddress.TryCreate(input, out _); // ASP.NET Core / データ注釈 [EmailAddress] public string Email { get; set; }上記の表の 2 番目のような長大なパターンは、
- RFC 5322 を正しく表現できていない(引用符付きローカル部など)
- TLD を
{2,4}に決め打ちしている(.museum.travelに非対応)- バックトラックが爆発しやすい(ReDoS のリスク)
という三重の問題を抱える。
「形式が妥当か」を厳密に判定する意味は薄く、
最終的には確認メールを送るしかないというのが現在の合意である。
※ 上記は、前方参照、最小マッチを併用している。
| 正規表現 | |
|---|---|
| 正規表現 |
"href\s*=\s*(?:(?<quot>[\"'])(?<url>.*?)\k<quot>)"→「クォーテーション無し」を考慮していない例。 |
| 正規表現 |
"href\s*=\s*(?:\"(?<1>[^\"]*)\"|(?<1>\S+))"→「シングル クォーテーション」を考慮していない例。 |
※ 上記は、前方参照、最小マッチを併用している。
※ 実行時、Singleline(「.」に、\n を含める)オプションを設定する必要がある。
| 正規表現 | |
|---|---|
| 正規表現 | "<(h[1-6])\b[^>]*>(.*?)</\1>" |
※ 上記は、前方参照、最小マッチを併用している。また、url、text などの部分キャプチャを行なっている。
※ 実行時、IgnoreCase(大 / 小文字を区別しない)、Singleline(「.」に、\n を含める)オプションを設定する必要がある。
| 正規表現 | |
|---|---|
| 正規表現 | "<a\s+[^>]*href\s*=\s*(?:(?<quot>[\"'])(?<url>.*?)\k<quot>|(?<url>[^\s>]+))[^>]*>(?<text>.*?)</a>" |
補足(HTML の解析に正規表現を使わない): 上記 3 つはいずれも
HTML を正規表現で解析する例だが、これは典型的なアンチパターンである。HTML は正規言語ではない(入れ子構造を持つ)ため、
正規表現では原理的に正しく解析できない。実際、上記の例も
- 属性値中の
>、コメント<!-- -->、<script>内のテキスト- 属性の順序違い、改行を挟む属性
- 入れ子の
<h1>や不正な閉じタグで容易に破綻する。原文も注記で
「〜を考慮していない例」と限界を明示している通りである。現在の .NET では、HTML パーサーを使う。
ライブラリ 特徴 AngleSharp W3C 準拠。CSS セレクター、DOM API HtmlAgilityPack 古参。XPath で辿れる。壊れた HTML に寛容 var doc = await new HtmlParser().ParseDocumentAsync(html); var urls = doc.QuerySelectorAll("a[href]").Select(a => a.GetAttribute("href"));「使い捨てのスクリプトで、入力の形が完全に分かっている」場合を除き、
HTML には正規表現を使わない。
※ 最小マッチを使用している。また、プロトコル、ポート番号などの部分キャプチャを行なっている。
| 正規表現 | |
|---|---|
| 正規表現 | "^(?<proto>\w+)://[^/]+?(?<port>:\d+)?/" |
補足: URL の分解も、
System.Uriを使うのが確実である。if (Uri.TryCreate(s, UriKind.Absolute, out var uri)) { var proto = uri.Scheme; // "https" var port = uri.Port; // 443(省略時も既定値が入る) var host = uri.Host; }クエリ文字列の解析には
Microsoft.AspNetCore.WebUtilities.QueryHelpersや
System.Web.HttpUtility.ParseQueryStringを使う。
正規表現の処理に利用する API と、そのオプションについて説明する。
正規表現にて中心的役割を担うクラスにRegex クラスとMatch クラスがある。
Regex クラスは、正規表現パターンを格納し、パターン マッチングを実行するメソッドを備えたクラスである。
このオブジェクトからキャプチャ文字列の値を取り出して、引き続き、「手続き型の処理」を実装できる。
- 以下に、Regex クラスの主要なプロパティ / メソッドを一覧する。
| 項番 | 区分 | 名前 | 説明 |
|---|---|---|---|
| 1 | プロパティ | ||
| 1-1 | プロパティ | Options | Regex コンストラクタに渡されたオプション(オプションを参照) |
| 2 | メソッド | ||
| 2-1 | メソッド | IsMatch | 正規表現と一致する対象が入力文字列内で見つかったかどうかを示す。 主に、入力チェックを実装する場合に利用し、入力チェックでは、 正規表現の先頭と末尾に「 ^ 」と「 $ 」を付与して中間からのマッチを拒否すると良い。 |
| 2-2 | メソッド | Match | 入力文字内で正規表現と一致する対象を1つ検索し、結果をMatch オブジェクトとして返す。 取得したMatch オブジェクトの NextMatch メソッド(後述)が呼び出された場合、次の結果をMatch オブジェクトとして返す。 |
| 2-3 | メソッド | Matches | 入力文字列内で正規表現と一致する対象をすべて検索し、 結果の複数のMatch オブジェクト(見つかった対象をすべて返す)を MatchCollection として返す。 |
| 2-4 | メソッド | Replace | 第一引数の正規表現パターンに一致する文字列を、第二引数の置換パターンで置換する。 第二引数には MatchEvaluator デリゲートを使用して置換(変換)することもできる(後述)。 |
| 2-5 | メソッド | Split | 入力文字列を、正規表現によって定義されている位置で配列に分割する。 |
移行メモ(正誤): 2-1 の「入力チェックでは
^と$を付与」は、
$が末尾の\nの前にも一致するため、厳密には不十分である
(前述の方言を参照)。
入力チェックでは\Aと\zを使う方が確実である。
- オブジェクト階層は次のようになっている。
- Matches メソッドで取得できる MatchCollection オブジェクト
- Match メソッドで取得できるMatch オブジェクト
オブジェクト名 : 下位オブジェクトの取得方法 : キャプチャ文字列の詳細データ
MatchCollectionオブジェクト : インデクサ(番号)
┗ Matchオブジェクト : Groupsプロパティ
┗ GroupsCollectionオブジェクト : インデクサ(番号、名称)
┗ Groupsオブジェクト : Capturesプロパティ
┗ CaptureCollectionオブジェクト : インデクサ(番号)
┗ Captureオブジェクト : 各種プロパティ
┣ Value(string) : キャプチャ文字列の値
┣ Length(int) : キャプチャ文字列の文字列長
┗ Index(int) : 入力文字列を基にした位置(インデックス)
移行メモ(誤記): 原文の「キャプチャ文字列の長文字列」は
文字列長の誤記と判断し修正した。
- 其々のオブジェクトの説明
| 項番 | オブジェクト名 | 説明 |
|---|---|---|
| 1 | Match オブジェクト | 正規表現により「検索された1つの文字列」と対応する結果を格納。 |
| 2 | Groups オブジェクト | 上記「検索文字列」のキャプチャ、グループ化による追加キャプチャの情報を格納。 |
| 3 | Capture オブジェクト | キャプチャ文字列の詳細データを格納 |
-
1つのグループが複数の文字列をキャプチャする例
正規表現によっては、1つのグループが複数の文字列をキャプチャすることがある。
このような正規表現の例を以下、説明する。- 正規表現の例
| 正規表現 | "(123)(\d)+(789)" |
| 入力文字列 | "123456789-123456789" |
- 上記を実行した結果、
以下のオブジェクトが返される。-
Match オブジェクト(2つ)
・前の "123456789" にマッチ
・後ろの "123456789" にマッチ - Groups オブジェクト(4つ)
・全体(123456789)のキャプチャ
・グループ 1(123)のキャプチャ
・グループ 2("\d"=4)のキャプチャ
・グループ 3("\d"=5)のキャプチャ
・グループ 4("\d"=6)のキャプチャ
・グループ 5(789)のキャプチャ
-
Match オブジェクト(2つ)
移行メモ(正誤): 原文の記述はやや不正確なので補足する。
- 「Groups オブジェクト(4つ)」とあるが、
列挙されているのは 6 件であり、数が合わない。- また、正規表現
(123)(\d)+(789)のグループは 3 つであり、
グループ 4・5 は存在しない。実際の挙動は次の通りで、まさにこれが
本節が説明しようとしているCapturesの意義である。Groups[0] = "123456789" (全体) Groups[1] = "123" Groups[2] = "6" ← Value は「最後の」キャプチャだけ Groups[2].Captures = ["4", "5", "6"] ← 3 回分すべてがここに残る Groups[3] = "789"要点:
(\d)+のように量指定子の付いたグループは
複数回キャプチャするが、Group.Valueは最後の 1 回しか返さない。
すべてを取り出すにはGroup.Capturesを列挙する。
(原文が「グループ 2〜4」と書いたのは、この 3 回分の
キャプチャを指す意図だったと読める。)
- MatchEvaluator デリゲートを使用して置換(変換)する例
- また、Regex.Replace ( ) メソッドの特殊な使い方に、
MatchEvaluator デリゲートを使用した置換(変換)方法がある。 - これを使用すると、非手続の「置換パターン」を使用した置換(変換)ではなく、
手続き型言語(C#、VB)を使用した置換(変換)が可能になる。
- また、Regex.Replace ( ) メソッドの特殊な使い方に、
public class Test_Regex {
// 正規表現を使用して置換(変換)するメソッド
public string Regex_Replace(string yyyymmdd)
{
// 日付(yyyy/mm/dd形式)を正規表現を用いて1日増やす
return Regex.Replace(yyyymmdd,
@"(?<year>\d{4})/(?<month>\d{2})/(?<day>\d{2})",
new MatchEvaluator(this.IncrementDay));
}
// MatchEvaluatorデリゲートメソッド
private string IncrementDay(Match m)
{
// 日付ワーク
DateTime dt;
// 日付(yyyy/mm/dd形式)をDateTime型に変換
if (DateTime.TryParse(m.Value, out dt)) {
// DateTimeクラスを使用して1日増やし、
// 日付(yyyy/mm/dd形式)に戻す。
return dt.AddDays(1).ToShortDateString();
}
else {
// 変換できなかった場合、何もしないで戻す。
return m.Value;
}
}
}補足(MatchEvaluator は「逃げ道」として重要): 正規表現だけでは
表現できない変換(計算、辞書引き、条件分岐)が必要になったとき、
無理に正規表現を複雑化せず、この方式に切り替えるのが定石である。現在はラムダ式で簡潔に書ける。
var s = Regex.Replace(input, @"\d{4}/\d{2}/\d{2}", m => DateTime.TryParse(m.Value, out var d) ? d.AddDays(1).ToString("yyyy/MM/dd") : m.Value);なお上記の原文の例で使われている
ToShortDateString()
はカルチャ依存(実行環境の地域設定で書式が変わる)ため、
書式を固定したいならToString("yyyy/MM/dd")を明示する方がよい。
Match クラスは、Regex クラスで実行されたパターン マッチングの結果を格納するクラスである。
- 以下に、Match クラスのプロパティ / メソッドを一覧する。
| 項番 | 区分 | 名前 | 説明 |
|---|---|---|---|
| 1 | プロパティ | ||
| 1-1 | プロパティ | Success | 一致した対象が見つかったかどうかを示す bool 値を取得する。 例えば、量指定子「 * 」を指定したグループ構成体は、見つからないことが有り得る。 |
| 1-2 | プロパティ | Groups | 正規表現による「検索文字列」の ・キャプチャ(グループ0)、 ・グループ化による追加キャプチャ(グループ1 ~ ) の情報を格納。 |
| 1-3 | プロパティ | Captures | グループ0のキャプチャ |
| 1-4 | プロパティ | Value | グループ0のキャプチャ文字列の値 |
| 1-5 | プロパティ | Length | グループ0のキャプチャ文字列の文字列長 |
| 1-6 | プロパティ | Index | グループ0の入力文字列を基にした位置(インデックス) |
| 2 | メソッド | ||
| 2-1 | メソッド | NextMatch | 次の結果を Match オブジェクトとして返す。 |
| 2-2 | メソッド | Result | 指定された置換パターンを返す(結果毎に置換パターンを変更できる)。 |
補足(アロケーションを避ける新 API):
Match/Groupは
いずれもstringを生成するため、大量処理では確保が積み上がる。.NET 7 以降では、
Span<char>ベースの API が追加されている。
API 内容 Regex.EnumerateMatches(ReadOnlySpan<char>)文字列を作らずに位置と長さだけ列挙 Regex.Count()一致数だけを数える(.NET 7+) ValueMatchIndexとLengthのみを持つ構造体foreach (var m in Regex.EnumerateMatches(span, pattern)) Process(span.Slice(m.Index, m.Length)); // 文字列生成なし
以下、Regex クラスのオプションについて説明する。
これによって一部、動作(処理結果)が変更される。
| 項番 | RegexOptions 列挙体のメンバ | 説明 |
|---|---|---|
| 1 | None | オプションが何も設定されないことを指定する。 |
| 2 | ECMAScript | ECMAScript 準拠の動作とする。一部のメタ文字の意味が変更される。 ・ECMAScript の標準一致の動作 https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expression-options |
| 3 | Singleline | ワイルドカードである .(ピリオド)の意味を、「\n」を含めたすべての文字の意味に変更する。 |
| 4 | Multiline | 「^」と「$」の意味を・「 ^」:「文字列の先頭」 → 「行の先頭」・「 $」:「文字列の末尾」 → 「行の末尾」に変更する。 「 ^」と「$」の代わりに「\A」と「\Z」を使用すると、Multiline の影響を受けずに「文字列の先頭」、「文字列の末尾」を指定できる。 |
| 5 | IgnoreCase | 大文字と小文字を区別しない。 |
| 6 | CultureInvariant | 言語(カルチャ)の違いを無視する。 ・RegularExpressions 名前空間でのカルチャを認識しない操作の実行 https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expression-options |
| 7 | RightToLeft | 検索が左から右(→)ではなく右から左(←)に行われるように指定する。 |
| 8 | Compiled | 正規表現をコンパイルして実行速度を上げる。ただし、起動時間は長くなる。 ・正規表現におけるコンパイルと再利用 https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/compilation-and-reuse-in-regular-expressions |
| 9 | ExplicitCapture | 明示的に名前を指定されたグループだけが有効なキャプチャであることを指定する。 |
| 10 | IgnorePatternWhitespace | ・正規表現のパターン内にコメント文を記述するには?[C#、VB] - @IT http://www.atmarkit.co.jp/fdotnet/dotnettips/582regexcomment/regexcomment.html |
補足(現在追加されたオプション/最新化): 上記に加えて、
現在は次の 2 つが重要である。
オプション 内容 NonBacktracking(.NET 7+)バックトラックしないエンジンを使う。ReDoS を原理的に防ぐ RegexOptions.Compiled現在も有効だが、ソース生成の方が上位
NonBacktrackingは入力長に対して線形時間を保証する
(オートマトン方式のエンジンを使う)。
ただし後方参照・先読み/後読みが使えないという制約がある。// 信頼できない入力を扱うなら、これが最も安全 new Regex(pattern, RegexOptions.NonBacktracking);
補足(
[GeneratedRegex]:現在の推奨形/最新化): .NET 7 以降、
ソース ジェネレーターによる正規表現が使える。public partial class Validator { [GeneratedRegex(@"\A[a-z0-9_]+\z", RegexOptions.IgnoreCase)] private static partial Regex UserName(); public bool IsValid(string s) => UserName().IsMatch(s); }
方式 生成タイミング 起動 実行 AOT new Regex(...)(解釈)実行時 速い 遅い ○ RegexOptions.Compiled実行時に IL 生成 遅い 速い × [GeneratedRegex]コンパイル時に C# 生成 速い 速い ○
[GeneratedRegex]が全面的に優れている
(起動が速く、実行も速く、トリミング/Native AOT でも動き、
生成された C# コードをデバッグで追える)。
静的なパターンには、これを使うのが現在の第一候補である。なお、動的に組み立てるパターンには使えないため、
その場合はRegexインスタンスをstatic フィールドにキャッシュする
(毎回newする、あるいは静的メソッドRegex.IsMatch(s, pattern)を
ループ内で呼ぶのは避ける)。
補足(原文に無い節。現在は必須の知識): 本ページ執筆時点では
話題になっていなかったが、正規表現には
**ReDoS(Regular expression Denial of Service)**という
セキュリティ上の問題がある。原理: バックトラック型のエンジンは、
一致しない入力に対してすべての分割を試すことがあり、
入力長に対して指数関数的に時間がかかる場合がある。// 危険なパターン(「入れ子の量指定子」) var re = new Regex(@"^(a+)+$"); re.IsMatch(new string('a', 30) + "!"); // 数十秒〜フリーズ
aが 1 個増えるごとに試行回数が倍増する。
攻撃者が入力を送れる箇所(フォーム、URL、ヘッダー)で
このようなパターンを使うと、CPU を占有されてサービスが止まる。危険なパターンの兆候:
形 例 入れ子の量指定子 (a+)+、(a*)*、(.*)*重なり合う選択肢 (a|a)*、(a|ab)+最短マッチ + 貪欲の混在 .*?.*対策(上から順に検討する):
対策 内容 そもそも正規表現を使わない メール→ MailAddress、URL→Uri、HTML→パーサーRegexOptions.NonBacktracking線形時間を保証(.NET 7+) タイムアウトを設定する new Regex(p, opts, TimeSpan.FromSeconds(1))否定文字クラスを使う .*?より[^"]*アトミック グループ (?>a+)+で後戻りを禁止入力長を制限する 検証前に長さで弾く タイムアウトの指定は、外部入力を扱うすべての
Regexで
事実上必須である。指定がないと既定は無限(InfiniteMatchTimeout)。private static readonly Regex _re = new(pattern, RegexOptions.None, TimeSpan.FromMilliseconds(200));超過すると
RegexMatchTimeoutExceptionが送出されるので、
捕捉して「不正な入力」として扱う。なお、原文のまとめが指摘する
「妥当性の検証が困難」という論点は、
この ReDoS の存在によって一層重みを増している。
「動くこと」だけでなく「一致しない入力で暴走しないこと」も
テストの対象になる。
正規表現を使用すると、高度な、
- 入力チェック
- 全文検索
- 置換(付加、削除、書式変更)
- 文字列分割
などの処理を、高い生産性で実装することができる。
ただし、以下の点は、正規表現により解決できない。
以下のように難解なため、妥当性の検証が困難である。
-
記述(処理の実装ステップ数)は、短かくなるが、
- 正規表現を習得する必要がある。
- アサインする技術者には正規表現の知識が必要になる。
-
「非手続き言語」の特徴として、開発したステップ数に対するテスト ケースが多い。
- それを検証するテスト工数の削減はできないし、
- テストケースが網羅的であるかどうか、カバレージを指標に利用できない。
補足(この指摘は現在も有効): 「1 行のコードだが、
カバレッジでは 1 行としてしか数えられない」という指摘は本質的である。緩和策としては、
手段 内容 IgnorePatternWhitespaceでコメントを書くパターン自体を読めるようにする [GeneratedRegex]生成コードをステップ実行で追える 境界値のテストを明示的に列挙 一致する例・一致しない例の両方 regex101.com 等での可視化 マッチ過程を確認する // x モード:パターンにコメントを入れる [GeneratedRegex(""" \A # 先頭 (?<user>[\w.+-]+) # ローカル部 @ (?<host>[\w-]+(\.[\w-]+)+) # ドメイン \z # 末尾 """, RegexOptions.IgnorePatternWhitespace)] private static partial Regex Mail();
- 「手続き言語」で実装しないと表現できないケースもある。
例えば、メール フォーマット チェックなどを例に挙げて説明すると、- RFC に準拠したメール アドレスだけ許可するのか?
- 携帯メールなどに見られる RFC 非準拠のメール アドレスも許可するのか?
- また、電子メールの配送先 IP アドレスの直接指定を許可するのか?など。
- 正規表現は、
- 入力チェック処理や、
- 置換(書式変更)などの
共通処理を、比較的容易に開発できる。
- しかし、利便性と汎用性を両立する API 仕様を検討することは変わらず難しい。
このため、強度を上げるなら、汎用性を犠牲にする以外に方法が無い。
補足(結論): 本ページ全体を通じた実務上の指針を纏める。
- 専用の型があるものには、正規表現を使わない
(メール →MailAddress、URL →Uri、日付 →DateTime.TryParse、
HTML/XML/JSON → 各パーサー)- 静的なパターンは
[GeneratedRegex]- 外部入力を扱うなら、タイムアウトか
NonBacktracking.*?より否定文字クラス[^x]*- キャプチャ不要なら
(?:)、名前と番号を混ぜない- 一致しない入力のテストを書く(性能も含めて)
-
.NET の正規表現
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expressions -
正規表現の基本 - .NET Tips (VB.NET,C#...)
http://dobon.net/vb/dotnet/string/regex.html -
正規表現 (System.Text.RegularExpressions)
Programming/.NET Framework - 総武ソフトウェア推進所
http://smdn.jp/programming/netfx/regex/ -
サルにもわかる正規表現入門
http://www.mnet.ne.jp/~nakama/ -
Regular-Expressions.info
Regex Tutorial, Examples and Reference - Regexp Patterns
http://www.regular-expressions.info/index.html
- .NET 正規表現ソース ジェネレーター
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expression-source-generators - 正規表現のベスト プラクティス
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/best-practices-regular-expressions - バックトラッキング
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/backtracking-in-regular-expressions
Tags: 移行, .NET開発, その他、開発の色々
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。