正規表現の文字クラス[ ]:範囲・否定・\p{L}を4エンジンで検証

文字クラス[abc]・範囲・否定・エスケープを.NET・Python・PHP・Dartで同じ入力のまま実測。[ぁ-ん]の範囲指定、\dと\wが全角数字まで拾う問題、\p{L}、.NETのクラス減算まで整理しました。

2026年11月改訂: この記事は2021年に韓国語で書いた講座を、日本語の読者向けに書き直したものです。元記事のパターンを .NET 10、Python 3.9・3.12 の re、PHP 8.5(PCRE2 10.47、/u あり・なし)、Dart 3.11(JavaScript の規則に従う RegExp)で、同じ入力のまま実行し直しました。元の4つの例はどのエンジンでも記事どおりにマッチしました。一方で「範囲には ASCII 文字だけが入る」「\w は [a-zA-Z0-9_]」という説明は正しくない、またはエンジン次第だったので訂正しています。日本語の範囲指定、Unicode プロパティ、エンジンごとの違いは新しく加えた内容です。

[0-9] や [^,] のような角かっこの表現を書きながら、「これは正確にどの文字まで通すのか」が気になる人向けの記事です。とくに入力に全角数字やひらがな・カタカナ、絵文字が混じる環境では、ここで差が出ます。先に結論を書くと、文字クラスは必ずちょうど1文字にマッチします。エンジンによって違うのは「どの文字をクラスの中とみなすか」で、同じパターンがエンジンを移ると黙って違う動きをするのはまさにこの部分です。

.、\、^、$ などのメタ文字そのものが初めての場合は、先にメタ文字の記事を読んでください。この記事は角かっこの内側だけを扱います。

1つの位置に1文字:元記事の例を再確認

元記事の最初の例です。

パターン:f[ie]
テキスト:fire / siren / female / finally / apple / airport
結果  :fi(fire)、fe(female)、fi(finally)

全エンジンで同じでした。[ie] は「i のあとに e」ではなく、i か e のどちらか1文字が入る枠1つです。だから f[ie] のマッチは常に2文字です。

2つ目は、4文字目が数字のファイル名を探す例でした。

パターン:...[0123456789] (= ...[0-9])
テキスト:abcd.txt abce.txt abc0.txt abcf.txt abc1.txt abc2.txt abc3.txt(1行に1つ)
結果  :abc0、abc1、abc2、abc3

どちらの書き方も全エンジンで同じ4件でした。ハイフンは範囲を作り、[a-zA-Z0-9] のように1つのクラスに複数の範囲を入れられます。

3つ目は、開きかっこの直後にキャレットを置いた否定クラスです。

パターン:...[^0-9]\.
結果  :abcd.、abce.、abcf.

これも同じでした。注意したいのは、[^0-9] も1文字を消費することです。「ここに数字がない」ではなく「数字ではない1文字」です。改行もその「数字ではない文字」に含まれます。a,b(改行)c,d に [^,]+ を使うと、全エンジンが b(改行)c を1つのマッチとして返しました。行ごとに切りたいなら [^,\n]+ と改行も除外します。こちらは a、b、c、d の4件でした。

判断の流れ図:まず通したい文字は何かを考える。半角だけなら [0-9] や [A-Za-z] と書き、\d や \w は避ける。そうでなければ \p{L} かコードポイントの範囲を使う。そのうえでエンジンを確認する:Python は regex モジュール、PHP は /u フラグ、Dart は unicode: true が必要。
文字クラスの書き方の選び方。半角だけのつもりなら範囲を明示する(\d と \w は .NET・Python・PHP /u で Unicode の文字も含む)。どの言語の文字でもよいなら \p{L} を使うが、有効にする方法はエンジンごとに違う。

範囲は ASCII ではなくコードポイントの順

元記事は「ASCII 文字なら何でも範囲に入れられ、前に小さい値を書く必要がある」としていました。後半は正しく、逆順の範囲 [z-a] は全エンジンでコンパイルエラーでした。

.NET   : Invalid pattern '[z-a]' at offset 4. [x-y] range in reverse order.
Python : bad character range z-a at position 1
PHP    : Compilation failed: range out of order in character class at offset 4
Dart   : FormatException: Range out of order in character class

前半は誤りです。範囲は Unicode のコードポイントを比べるので、日本語にもそのまま使えます。

パターン テキスト .NET / Python / PHP /u / Dart PHP /u なし
[ぁ-ん]+ ひらがなとカタカナ ひらがなと 文字列全体

PHP の列が落とし穴です。/u がないと PCRE はバイト単位で動くので、[ぁ-ん] が UTF-8 のバイト値の集合になり、カタカナのバイトまで拾ってしまいました。日本語を含む PHP のパターンには必ず /u を付けます。また、カタカナの長音「ー」(U+30FC)は [ァ-ン] の範囲外です。[ァ-ン]+ を コーヒー に使うと全エンジンで コ と ヒ に分かれ、[ァ-ンー]+ にすると コーヒー 全体が取れました。

コードポイント順は、よくある間違いも説明してくれます。[A-z] は「英字すべて」ではありません。Z(0x5A)と a(0x61)の間に [ \ ] ^ _ とバッククォートがあるため、全エンジンが Az_^[`]\ をひとかたまりでマッチしました。[A-Za-z] と書きましょう。

角かっこの中のエスケープ

ほとんどのメタ文字はクラスの中で意味を失います。だから [.] は全エンジンでピリオドそのものにマッチします。クラスの中で特別な文字はわずかで、それも位置で変わります。

パターン テキスト 結果(全エンジン共通) 理由
[.] a.b . クラス内の . は文字どおり
[a-]+ a-b a- 末尾の - は文字どおり
[a\-z]+ a-zb a-z \- なので範囲ではなく3文字
[a^]+ a^b a^ ^ は [ の直後だけ特別
[\]a]+ a]b a] エスケープした ]
[\d.]+ v1.2.3 1.2.3 クラス内でも \d が使える

結果が分かれた書き方もあります。「] をクラスの先頭に置けば文字どおり」という説明をよく見ますが、[]a]+ を a]b に使うと、.NET・Python・PHP は a]、Dart(既定)はマッチなし、Dart の unicode: true は FormatException: Lone quantifier brackets でした。JavaScript 系では [] が「何も含まないクラス」だからです。\] とエスケープすれば5つの構成すべてで通りました。

POSIX クラス [[:alpha:]]+ も、ab12:cd から ab と cd を返したのは PHP だけでした。Python は FutureWarning: Possible nested set at position 1 を出してマッチなし、.NET と Dart もマッチなしです。

\d・\w・\s が実際に拾う文字

元記事は \w を [a-zA-Z0-9_] と説明していましたが、これはエンジン次第です。

パターン テキスト .NET Python 3 PHP /u PHP /u なし Dart
\d+ 123 123 ١٢٣ 3つとも 3つとも 3つとも 123 123
\w+ café 한글 abc café、한글、abc 同じ 同じ caf、abc caf、abc
a\sb a+NBSP+b マッチ マッチ マッチ なし マッチ

つまり .NET や Python で ^\d+$ を使って入力をチェックすると、全角数字の「123」も通ります。日本語入力のフォームでは全角数字が普通に入ってくるので、正規表現は通ったのに、次の int.Parse や DB 保存で失敗する、ということが起こります。半角数字だけが目的なら、そう書きましょう。

  • [0-9] と書く(全エンジンで 123 だけ)
  • .NET:RegexOptions.ECMAScript(\d+ は 123、\w+ は caf だけ)
  • Python:re.ASCII(同じく ['123']、['caf'])

逆に全角数字も受け付けて半角にそろえたいなら、正規表現の前に Unicode 正規化(NFKC)をかけるのが定石です。Dart(Flutter)の RegExp は JavaScript と同じく、unicode: true を付けても \d と \w は ASCII だけでした。同じ \w+ でも、Flutter アプリと .NET や Python のサーバーとで判定が食い違います。

Unicode プロパティ \p{L}:ブロックとスクリプト

「どの言語でも文字なら」というときは、範囲を並べるより Unicode プロパティを使います。対応状況がばらばらなので、エラーメッセージも合わせて見ておくと役に立ちます。

パターン .NET Python re PHP /u Dart unicode: true
\p{L}+(café 한글 123) café、한글 エラー café、한글 café、한글
\p{IsHangulSyllables}+(ブロック) 한글 エラー エラー エラー
\p{Hangul}+(スクリプト) エラー エラー 한글、ㅋㅋ エラー
\p{Script=Hangul}+ エラー エラー 한글、ㅋㅋ 한글、ㅋㅋ

実際のメッセージです。

Python 3.9 / 3.12 re : bad escape \p at position 0
.NET 10              : Invalid pattern '\p{Hangul}+' at offset 10. Unknown property 'Hangul'.
PHP 8.5              : unknown property after \P or \p at offset 21   (IsHangulSyllables)
Dart 3.11            : FormatException: Invalid property name \p{IsHangulSyllables}+

.NET は Unicode のブロック(Is+ブロック名)、PCRE と JavaScript はスクリプトを使います。日本語なら .NET では \p{IsHiragana}+|\p{IsKatakana}+ が ひらがな カタカナ から ひらがな と カタカナ を返しました。ブロックは固定のコードポイント区間なので、ハングルの例のように同じ言語の文字が別ブロックに分かれていることがあります(.NET では ㅋ は IsHangulSyllables に入らず、\p{IsHangulCompatibilityJamo} で拾えました)。また Dart で unicode: true を外すと、\p{L} はエラーにならず「文字 p のあとに {L}」と解釈されて何も見つかりませんでした。Dart で \p を使うときは必ず unicode: true を付けます。

Python ではサードパーティの regex パッケージを入れます。Python 3.12+regex 2026.11.9 では、regex.findall(r'\p{L}+', 'café 한글 123') が ['café', '한글'] を返しました。

.NET のクラス減算と、どこでも使える代替

.NET は文字クラスから別のクラスを引き算できます。[a-z-[aeiou]] は「母音を除いた小文字」です。

パターン:[a-z-[aeiou]]+
テキスト:regex b] works
.NET    :r、g、x、b、w、rks

[\w-[\d_]]+ は A1 b2 c3 _9 から A、b、c を返し、[\p{L}-[\p{IsCJKUnifiedIdeographs}]]+ は 한글 漢字 カナ から漢字を除いた 한글 と カナ を返しました。「かなだけ」「漢字以外」を取り出したいときに便利です。

問題はほかのエンジンです。Python、PHP、Dart(既定)はこのパターンをエラーなしでコンパイルし、[a-z-[aeiou] というクラスと文字どおりの ] と読んだため、結果は b] だけでした。拒否したのは Dart の unicode: true だけです。黙って意味が変わるパターンは、失敗するパターンより危険です。複数のエンジンで共有するなら、先読みを組み合わせます。

(?:(?![aeiou])[a-z])+   → 全エンジンで r、g、x、b、w、rks

先読みについては先読み・後読みの記事にまとめています。Python の regex モジュールは V1 フラグで本物の集合演算に対応しており、(?V1)[[a-z]--[aeiou]]+ も同じ6件を返しました。

早見表と検証方法

やりたいこと どこでも通るパターン メモ
半角数字 [0-9] \d は .NET・Python・PHP /u で全角数字も含む
英字 [A-Za-z] [A-z] は記号を含む
ひらがな / カタカナ [ぁ-ん] / [ァ-ンー] PHP は /u 必須
クラス内の ]、-、^ \]、\-、\^ エスケープが最も安全
どの言語でも文字 \p{L} Python re は不可、Dart は unicode: true
クラスの減算 (?:(?![aeiou])[a-z]) [a-z-[aeiou]] は .NET 専用

検証環境:.NET 10.0.14(Regex.Matches)、Python 3.9.6・3.12.14(re.finditer)、PHP 8.5.6+PCRE2 10.47(preg_match_all、/u あり・なし)、Dart 3.11.5(RegExp.allMatches、既定と unicode: true)。Node.js は入っていなかったので、JavaScript の規則は Dart で確かめました。パターンと入力は1つの JSON ファイルにまとめ、全エンジンが同じバイト列を読むようにしています。+ や {n,m} などの繰り返しは量指定子の記事に続きます。

2026年11月、Apple Silicon Mac(macOS)で実測。

よくある質問

\d は 0〜9 だけにマッチする?

エンジンによります。.NET、Python 3、PHP(/u 付き)では全角数字「123」やアラビア・インド数字「١٢٣」も \d にマッチしました。ASCII だけだったのは PHP(/u なし)と Dart(JavaScript の規則)です。半角数字だけにしたいなら [0-9] と書くか、.NET は RegexOptions.ECMAScript、Python は re.ASCII を指定します。

Python の re で \p{L} は使える?

使えません。Python 3.9 と 3.12 のどちらも「bad escape \p at position 0」になりました。サードパーティの regex モジュール(検証時 2026.11.9)なら \p{L} や \p{Hiragana} が使えます。

[a-z-[aeiou]] はどのエンジンでも使える?

いいえ、.NET 専用の減算です。Python・PHP・Dart(既定モード)ではエラーにならずに別の意味で解釈され、「b]」にしかマッチしませんでした。共通で使うなら (?:(?![aeiou])[a-z])+ と書けば全エンジンで同じ結果になりました。

  • #正規表現
  • #文字クラス
  • #Unicode
  • #Python
  • #.NET
  • #PHP
  • #Dart

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑