« ◇半合の計量カップ-2 | トップページ | ◆内部散乱界面を持つ低反射パネル »

◆^,$は行頭,行末ではない;正規表現の落とし穴

正規表現の落とし穴をJavaのString#replaceAll()の実働サンプルで解説します。

サンプルは regexSample.zip に置きました。

まあ、String#replaceAllは使わない方がよい。というだけかも。

 ^は行頭ではない

正規表現で^は「データ(処理単位)の先端」であって行の先端ではありません。行単位で処理する系では行頭と同じだというだけです。

例えば次の例では"abcd"行が4行で構成されていますが、^でマッチするのはデータ先端だけです。

------------------- 単純に^を使う
"abcd\nabcd\nabcd\nabcd".replaceAll("^ab","[@]");
-->
[@]cd   <-- ^abでマッチ
abcd    <-- 行頭abなのにマッチしない
abcd    <-- 行頭abなのにマッチしない
abcd    <-- 行頭abなのにマッチしない

各行の先端にマッチさせるには後述の前置式と組み合わせ次の様に指定します。
前置式は最終的な置き換え範囲には含まれません。

------------------- 前置式と^を組み合わせ
"abcd\nabcd\nabcd\nabcd".replaceAll("(^|(?<=\n))ab","[@]");
-->
[@]cd   <-- ^abでマッチ
[@]cd   <-- (\n)abでマッチ
[@]cd   <-- (\n)abでマッチ
[@]cd   <-- (\n)abでマッチ
--- \nは置き換え範囲に含まれません。もし\n置き換えられると1行になってしまいます。
[@]cd[@]cd[@]cd[@]cd

WindowsやMacの改行形式にも対応するためには\nの代わりに(\n|\r|\r\n)または(\\n|\\r|\\r\\n)を使います。
\nと\\nの差はコンパイル時に改行コードにするか、正規表現の解釈時に改行コードにするかの差で、どちらでも構いません。

------------------- Win形式Mac形式の改行対処
"abcd\nabcd\rabcd\r\nabcd".replaceAll("(^|(?<=(\n|\r|\r\n)))ab","[@]");
-->
[@]cd   <-- ^abでマッチ
[@]cd   <-- (\n)abでマッチ
[@]cd   <-- (\r)abでマッチ
[@]cd   <-- (\rn)abでマッチ

このパターンは固定的なものなので予めfinal staticで定義して置き、使いまわすのが適切でしょう。

------------------- SOL
final static String SOL="(^|(?<=(\\n|\\r|\\r\\n)))";
//
"abcd\nabcd\rabcd\r\nabcd".replaceAll(SOL+"ab","[@]");
-->
[@]cd   <-- ^abでマッチ
[@]cd   <-- (\n)abでマッチ
[@]cd   <-- (\r)abでマッチ
[@]cd   <-- (\rn)abでマッチ

 $は行末ではない

正規表現で$は「データ(処理単位)の終端」であって行末ではありません。行単位で処理する系では行末と同じだというだけです。。

例えば次の例では"abcd"行が4行で構成されていますが、$でマッチするのはデータ終端だけです。

------------------- 単純に$を使う
"abcd\nabcd\nabcd\nabcd".replaceAll("cd$","[@]");
-->
abcd    <-- cd行末なのにマッチしない
abcd    <-- cd行末なのにマッチしない
abcd    <-- cd行末なのにマッチしない
ab[@]   <-- cd$でマッチ

各行末にマッチさせるには後述の後置式と組み合わせ次の様に指定します。
後置式は最終的な置き換え範囲には含まれません。

------------------- 後置式と$を組み合わせ
"abcd\nabcd\nabcd\nabcd".replaceAll("cd($|(?=\n))","[@]");
-->
ab[@]   <-- cd(\n)でマッチ
ab[@]   <-- cd(\n)でマッチ
ab[@]   <-- cd(\n)でマッチ
ab[@]   <-- cd$でマッチ

WindowsやMacの改行形式にも対応するためには\nの代わりに(\n|\r|\r\n)または(\\n|\\r|\\r\\n)を使います。
\nと\\nの差はコンパイル時に改行コードにするか、正規表現の解釈時に改行コードにするかの差で、どちらでも構いません。

------------------- Win形式Mac形式の改行対処
"abcd\nabcd\rabcd\r\nabcd".replaceAll("cd($|(?=(\n|\r|\r\n)))","[@]");
-->
ab[@]   <-- cd(\n)でマッチ
ab[@]   <-- cd(\r)でマッチ
ab[@]   <-- cd(\r\n)でマッチ
ab[@]   <-- cd$でマッチ

このパターンは固定的なものなので予めfinal staticで定義して置き、使いまわすのが適切でしょう。

------------------- EOL
final static String EOL="($|(?=(\\n|\\r|\\r\\n)))";
//
"abcd\nabcd\rabcd\r\nabcd".replaceAll("cd"+EOL,"[@]");
-->
ab[@]   <-- cd(\n)でマッチ
ab[@]   <-- cd(\r)でマッチ
ab[@]   <-- cd(\r\n)でマッチ
ab[@]   <-- cd$でマッチ

 開始終了条件が有る場合*?(0個最短一致)を使う

不定長の一致には

  • *:最長一致
  • *?:最短一致
があります。

例えば"ab<cd>efg<hijk>lmn"を解析し<...>が欲しいという場合、"<cd>"と"<hijk>"が欲しいのだと思います。
しかし、このとき最長一致"<.*>"を用いると"<cd>efg<hijk>"がマッチしてしまいます。
望みの形を得るためには最短一致".*?"を用いる必要があります。

------------------- .*:最長マッチ
"ab<cd>efg<hijk>lmn".replaceAll("<.*>","[@]");
-->
(ab<cd>efg<hijk>lmn)
ab[@]lmn         <-- <cd>efg<hijk>がマッチした
------------------- .*?:最短マッチ
"ab<cd>efg<hijk>lmn".replaceAll("<.*?>","[@]");
-->
(ab<cd>efg<hijk>lmn)
ab[@]efg[@]lmn   <-- <cd>と<hijk> がマッチした

「終端条件のある場合(ここの例では'>')」最長一致ではなく最短一致を使うべき場合が多いため、最短一致(*?)を標準と考えるべきです。

+(一個以上)の指定も行った例を示します。意図した結果が得られているのは「.*?:0個以上最短マッチ」のみです。

------------------- .*:0個以上最長マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<.*>","[@]");
-->
a[@]lmn
------------------- .*?:0個以上最短マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<.*?>","[@]");
-->
a[@]b[@]ef[@]g[@]lmn
------------------- .+:1個以上最長マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<.+>","[@]");
-->
a[@]lmn
------------------- .+?:1個以上最短マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<.+?>","[@]");
-->
a[@]ef[@]lmn

 開始終了条件が無い場合は+(1個以上最長一致)

開始終了条件が無い場合は+(1個以上最長一致)を用います。この例ではマッチさせる文字として\Sを使っていますがこれは空白文字以外の文字をあらわします。

------------------- \S*:空白以外の0個以上最長マッチ
" a ab abc XYZ ".replaceAll("\\S*","[@]");
-->
[@] [@][@] [@][@] [@][@] [@][@] [@]
------------------- \S*?:空白以外の0個以上最短マッチ
" a ab abc XYZ ".replaceAll("\\S*?","[@]");
-->
[@] [@]a[@] [@]a[@]b[@] [@]a[@]b[@]c[@] [@]X[@]Y[@]Z[@] [@]
------------------- \S+:空白以外の1個以上最長マッチ
" a ab abc XYZ ".replaceAll("\\S+","[@]");
-->
 [@] [@] [@] [@] 
------------------- \S+?:空白以外の1個以上最短マッチ
" a ab abc XYZ ".replaceAll("\\S+?","[@]");
-->
 [@] [@][@] [@][@][@] [@][@][@] 

 開始条件のみの場合は*(0個以上最長一致)

開始条件のみの場合は*(0個以上最長一致)を用います。

------------------- a\S*:a開始空白以外の0個以上最長マッチ
" a ab abc XYZ ".replaceAll("a\\S*","[@]");
-->
 [@] [@] [@] XYZ 
------------------- a\S*?:a開始空白以外の0個以上最短マッチ
" a ab abc XYZ ".replaceAll("a\\S*?","[@]");
-->
 [@] [@]b [@]bc XYZ 
------------------- a\S*?:a開始空白以外の1個以上最長マッチ
" a ab abc XYZ ".replaceAll("a\\S+","[@]");
-->
 a [@] [@] XYZ 
------------------- a\S*?:a開始空白以外の1個以上最短マッチ
" a ab abc XYZ ".replaceAll("a\\S+?","[@]");
-->
 a [@] [@]c XYZ 

 終了のみの場合は*?(0個以上最短一致)

終了のみの場合は*?(0個以上最短一致)を用います。

------------------- \S*q:q終了空白以外の0個以上最長マッチ
" q aq aqb aqbcq XYZ ".replaceAll("\\S*q","[@]");
-->
 [@] [@] [@]b [@] XYZ 
------------------- \S*?q:q終了空白以外の0個以上最短マッチ
" q aq aqb aqbcq XYZ ".replaceAll("\\S*?q","[@]");
-->
 [@] [@] [@]b [@][@] XYZ 
------------------- \S*?q:q終了空白以外の1個以上最長マッチ
" q aq aqb aqbcq XYZ ".replaceAll("\\S+q","[@]");
-->
 q [@] [@]b [@] XYZ 
------------------- \S*?q:q終了空白以外の1個以上最短マッチ
" q aq aqb aqbcq XYZ ".replaceAll("\\S+?q","[@]");
-->
 q [@] [@]b [@][@] XYZ 

この正解例ではaqbcqはaqとbcqの2つの領域として解析されています。

 確実な終端定義;[文字コードのセット]など

例えば<abc>にはマッチするけれど<>にはマッチしないという指定はどのようにすればよいでしょう。

"<.+?>"でよいように思えるかもしれませんが、この形では期待の結果は得られません。

------------------- \S+?:1個以上最短マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<.+?>","[@]");
-->
a[@]ef[@]lmn

これは<>にはマッチしなかったものの、'.'は'>'も許すのでさらに長い<>b<cd>がマッチしたためです。

これを避けるためには不定長パターンが終端文字を含まないように指定します。

------------------- [^\n>]+?:>以外1個以上最短マッチ
"a<>b<cd>ef<>g<hijk>lmn".replaceAll("<[^\n>]+?>","[@]");
-->
a<>b[@]ef<>g[@]lmn

パターン[...]は採用する文字コードのセットです。^は否定です。ここでは改行\nでも>でもない文字を指定しています。

 複数行一致は[\S\s]*?

.は改行を含みませんので行にまたがるパターンにマッチさせることはできません。

------------------- .*:ドットでの最長マッチ(複数行)
"ab<cd>ef\ng<hi\njk>lmn".replaceAll("<.*>","[@]");
-->
(ab<cd>ef\ng<hi\njk>lmn)
ab[@]ef (改行)
g<hi    (改行)
jk>lmn
------------------- .*?:ドットでの最短マッチ(複数行)
"ab<cd>ef\ng<hi\njk>lmn".replaceAll("<.*?>","[@]");
-->
(ab<cd>ef\ng<hi\njk>lmn)
ab[@]ef (改行)
g<hi    (改行)
jk>lmn

ドットの代わりに[\S\s]を用います。\Sは空白、\sは空白(改行を含む)を表し、組み合わすことによりすべての文字を表しています。
この時、決して最長一致を使ってはなりません。
.*の場合探索範囲はせいぜい1行ですが、[\S\s]*ではデータ全体に渡り、データが長い場合解析部がスタックオーバーフローを起こしてしまう可能性があります。

------------------- [\s\S]*:複数行探査での最長マッチ
"ab<cd>ef\n<ghi\njkl>mn".replaceAll("<[\\s\\S]*>","[@]");//行うべきではない
-->
(ab<cd>ef\ng<hi\njkl>mn)
ab[@]mn  <-- 一応動くがこの方法はとるべきではない。
------------------- [\s\S]*?:複数行探査での最短マッチ
"ab<cd>ef\ng<hi\njkl>mn".replaceAll("<[\\s\\S]*?>","[@]");
-->
(ab<cd>ef\ng<hi\njkl>mn)
ab[@]ef  (改行)
g[@]mn  (\nも含めて置き換えられている)

 前置式 肯定(?<=...) 否定(?<!...)

前置式とはマッチするパターンの前に存在すべき(肯定的)パターンまたは存在してはならない(否定的)パターンです。
前置式自体は最終マッチパターンに含まれません。

次の肯定前置の例では+または-が前にある単語文字(\w)の並びを置き換えています。\wは単語に使われる文字,\Wは単語に使わない文字を表します。

------------------- 肯定前置式:(?<=[+\-]) +または-が前につく
"ab -cd ef+ghi + jk -l - m".replaceAll("(?<=[+\\-])\\w+","[@]");
-->
(ab -cd ef+ghi + jk -l - m)
ab -[@] ef+[@] + jk -[@] - m
次の否定前置の例では+,-,単語文字(\w)が前に無い単語文字(\w)の並びを置き換えています。
否定前置式に単語文字(\w)を入れてあるのは、例えば入れない場合-cdではcは前が-なので拒否されますが、dは前がcなので採用されてしまうためです。

------------------- 否定前置式:(?<![+\-\w]) +-単語文字が前にない
"ab -cd ef+ghi + jk -l - m".replaceAll("(?<![+\\-\\w])\\w+","[@]");
-->
(ab -cd ef+ghi + jk -l - m)
[@] -cd [@]+ghi + [@] -l - [@]

否定前置式を使い\\の付かない引用符にマッチするパターンを用い、\\"で引用符を中に置ける文字列も得られます。
ここでは.*?の前置ではなく、後ろの引用符の前置になっていることに注目してください。

------------------- 否定前置式: 'から \の付かない ' まで
" x 'abcd' y 'ef\'gh' z ".replaceAll("'.*?(?<!\\\\)'","'[@]'");
-->
 x '[@]' y '[@]' z 

 後置式 肯定(?=...) 否定(?!...)

前項の前置式を後ろに置き換えたものです。

------------------- 肯定後置式:(?=[+\-]) +または-が前につく
"ab cd- ef+ghi + jk l- - m".replaceAll("\w+(?=[+\-])","[@]");
-->
ab [@]- [@]+ghi + jk [@]- - m
------------------- 否定後置式:(?![+\-\w]) +-単語文字が前にない
"ab cd- ef+ghi + jk l- - m".replaceAll("\w+(?![+\-\w])","[@]");
-->
[@] cd- ef+[@] + [@] l- - [@]

 前置式+後置式(単語のマッチなど)

前置式と後置式を一緒に使うこともできます。

例えば単語は前後に単語以外の文字がある特定文字の並びです。
次の例は"word"という単語を見つけています。

------------------- 前置否定+後置否定
"abwordxy word aword wordx".replaceAll("(?<!\w)word(?!\w)","[@]");
-->
abwordxy [@] aword wordx

次の例では/*から*/までの中身を[@]に置き換えています。複数行に渡っても大丈夫です。

------------------- 前置肯定+後置肯定
"a/* bc*/de/*f\ngi*/ ijk".replaceAll("(?<=/\*)[\s\S]*?(?=\*/)","[@]");
-->
a/*[@]*/de/*[@]*/ ijk

 後置式を重ねて用いたAND表現

後置式を重ねることによる若干トリッキーなAND表現が可能です。(が、勧めません)

次の正規表現では行の先頭SOLの後ろに不定文字列の後に単語が有る事を表す2つの後置式

  • (?=.*(?<!\\w)kochi(?!\\w))
  • (?=.*(?<!\\w)tokyo(?!\\w))
を置いています。

----------------- 後置式を重ねて用いたAND表現(行頭置き換え) ------
"tokyo,nagoya,osaka\ntokyo,nagoya,kochi\ntokyo,osaka\nosaka,kochi,tokyo\nnagoya,kochi"
   .replaceAll(SOL+"(?=.*(?<!\\w)kochi(?!\\w))(?=.*(?<!\\w)tokyo(?!\\w))","[@]");
-->
tokyo,nagoya,osaka
[@]tokyo,nagoya,kochi
tokyo,osaka,kochiX
[@]osaka,kochi,tokyo
nagoya,kochi

この正規表現は「kochi」が行内にある「行頭」で、その行頭は「tokyo」が行内にあることを表します。
マッチする行の行頭が置き換えられていることが分かります。

次の様に後ろに.*で行をマッチングさせると行全体が置き換わります。

----------------- 後置式を重ねて用いたAND表現(行全体置き換え) ------
"tokyo,nagoya,osaka\ntokyo,nagoya,kochi\ntokyo,osaka\nosaka,kochi,tokyo\nnagoya,kochi"
   .replaceAll(SOL+"(?=.*(?<!\\w)kochi(?!\\w))(?=.*(?<!\\w)tokyo(?!\\w)).*","[@]");
-->
tokyo,nagoya,osaka
[@]
tokyo,osaka,kochiX
[@]
nagoya,kochi

 グループ参照、番号および名前

正規表現でマッチした一部分を参照することができます。括弧を付けた部分の対応部をグループとして参照します。

番号で参照するか、正規表現上で(?<名前>...)の形で名前を与え、その名前で参照します。

------------------- 番号でアクセス
"REF<aaa bbb>".replaceAll("REF<(\w+)\s+(\w+)>","<a href=\"$1.html\">$2</a>");
-->
<a href="aaa.html">bbb</a>
------------------- 名前でアクセス
"REF<xxx yyy>".replaceAll("REF<(?<href>\w+)\s+(?<title>\w+)>","<a href=\"${href}.html\">${title}</a>")
-->
<a href="xxx.html">yyy</a>

なお、番号でのアクセスは${1},${2}の形式が可能なはずですが、String#replaceAll()ではできないようです。

括弧と番号の対応は0は全体を表し、1以降は括弧の出現順となります。括弧の階層と番号は関連しません。

 *の落とし穴

次の例を見てください。3行のデータです。

------------------- 前後無の.*で検索
"abc\ndef\nghi".replaceAll(".*","[@]");
-->
[@][@]
[@][@]
[@][@]

この例で各行2回マッチしているのは、まず文字列でマッチした後、長さ0でマッチしているのです。

次の例ではAの連続が2回マッチしています。

"bAbAAbAAAb".replaceAll("A*","[@]");
-->
[@]b[@][@]b[@][@]b[@][@]b[@]
期待値は
[@]b[@]b[@]b[@]

この例も.*と同じで、文字列にマッチした後長さ0でマッチしています。

 「先読み」は不適切

本記事の「前置」「後置」は「後読み(または読み戻し)」「先読み」と呼ばれることもあります。

「先読み」は手続きに関する用語であり、宣言型の言語である正規表現に用いるのは不適切です。
「後読み」にいたっては意味が通じません。

|

« ◇半合の計量カップ-2 | トップページ | ◆内部散乱界面を持つ低反射パネル »

トラックバック

この記事のトラックバックURL:
http://app.f.cocolog-nifty.com/t/trackback/489055/74091267

この記事へのトラックバック一覧です: ◆^,$は行頭,行末ではない;正規表現の落とし穴:

« ◇半合の計量カップ-2 | トップページ | ◆内部散乱界面を持つ低反射パネル »