正規表現の亡者です。なおかつPerlネタですので、低機能な正規表現しか利用できない環境では一部の正規表現を再現できない場合があります。
なおここに書いてある正規表現の多くは元ネタのWebページやらブログやらがあった気がするんですが、どうも出てこないので出典はあげられません。すみません。
というか出てこないのでまとめの記事をあげとくと誰かの役に立つかもと思ったり。
このへんに類例はあるけどみつかんない。
クオート文字列をエスケープする
問題
クオート文字列、つまり一般的にこういうやつ"This string is \"very\" normal."
。
両端境界の文字は別に["'/なんでもござれ]
なのだが一般的に普及しているのでこれで代表させてもらう。
これを\"
を無視しつついかに"なかみ"
を取り出すかというのが課題である。
エスケープがない場合はごく安直に/"([^"]*)"/
とすればいいのだが、ある場合はそうはいかない。
とくにエスケープ自身をエスケープする事例\\
が格段に問題をややこしくしている。これがなければ前方に\"
として(?<!\\)"
(ゼロ幅の否定後読み表明)あたりを駆使するだけでいいのだろうが……。
これがあるからCSVは扱いにくいと思われたり思われなかったり。
境界文字列が1文字の場合
まず境界文字列が1文字の場合を考える。
首をかしげる方がいるかもしれないが、適当な乱数文字列とかで区切りたい場合もあるだろう。とりあえず解答を示す。
/"((?:\\\\|\\"|[^"])*)"/
見てのとおり[^"]
が要なので1文字しか純粋には対応できないのだ。
なお\\
がいつも重なっているのはこいつが正規表現そのものにとってもエスケープ文字であるからで、
たとえばこれをA
とか無害な文字にすれば/"((?:AA|A"|[^"])*)"/
となる。
また逆に"
が|
とかなら\|
にしてやる必要がある。
このへんはPerlならquotemeta
で処理できる。
my $str = '...';
my $delim = '"';
my $escape = '\\';
my $re_delim = quotemeta $tag_delim;
my $re_escape = quotemeta $tag_escape;
my $re_read_str = "$re_delim((?:$re_escape$re_escape|$re_escape$re_delim|[^$re_delim])*)$re_delim";
my $re_read = qr/$re_read_str/;
while($str =~ /$re_read/g){
my $value = $1;
...
}
まあ、先頭からたどっていって、\\
であればスルー、そうでないなら\"
であればスルー、そうでないなら[^"]
な文字が続けばいいよねっていうやつである。
これは[^"]
が内部に入っているし"
が終端にあるので*
でも*?
でもいい気がする。
境界文字列が複数文字の場合
まあ、需要は少ないと思うけど。
これはマジもんの曲者である気がするでしょう?だって[^"]
の拒否が使えないんだから。
とは思ったのだが、解答をどっかのサイトで見たら実にあっけないものだった。
/"((?:\\\\|\\"|(?!").)*?)"/
さっきのの文字を拒否する[^"]
をゼロ幅の否定先読み表明の前置文字(?!").
にかえただけ。
先頭からたどっていって、\\
であればスルー、そうでないなら\"
であればスルー、そうでないならってのはおなじだが、これはゼロ幅の否定先読み表明を前置するとはどういうことかというのが焦点になる。
.(?!")
ならいいだろう。例は山とある。後ろが"
でない任意文字だ。
では(?!").
は?これだって後ろが"
でない任意文字だ。ただし「後ろ」は任意文字自身だ!
こういう理屈でこの論法でも1文字の境界文字ははねられるのである。よくできている。
一方で複数文字の境界文字列の場合は加えて別の作用が働く。
境界文字列がたとえばPerl
であった場合を考えよう。正規表現は/Perl((?:\\\\|\\Perl|(?!Perl).)*?)Perl/
だ。
(?!Perl).
はあくまで1文字づつの判定していく。だから(?!Perl).
ではねられるのはPerl
のl
のところ、最後の文字であって、Per
まではそれまでに通されてしまっている。
しかしこれでは全体にマッチしないことになってしまう。ここで正規表現マッチするエンジンは考え直し、別のマッチがあるのではとやりなおすために引き返すのだ。
するとPer
を差し戻したところに、正規表現のもっと後ろにある境界文字列であるPerl
がうまく当てはまる。そしてマッチを終了するのだ。
実によくできていて関心し通しである。Webページどこだろ……。
エスケープ文字列が複数文字の場合
たいがい何を言っているのかと思われると思う。
しかし適当な乱数ry
これに関しては簡単で、前述の表現の\
を単に複数文字列にすればよい。
ただし制限がある。
- 境界文字列がエスケープ文字列の部分文字列でないこと(境=java/エ=javascriptとかはNG)
- エスケープ文字列の前後が境界文字列の後前でないこと(境=if/エ=fiとかはNG)
こうした制限はうまくすれば取っ払えるのかもしれないが、そんな調べる労力を費やすような需要もないと思うので調べてはいない。
以上
まあこれだけ書いておけばたいていの需要は事足りるだろうと思うので筆をおく。あ、元サイト見つけたらよかったら掲示板などに書いてください。
なおほかに考えつかなかったので別段ベンチとかは取っていません。適当にやってください。
ついでに境界文字列が複数ある場合
['"]
どっちも使うってやつですね。これは/(["'])((?:\\\\|\\\1|(?!\1).)*?)\1/
のようにすればいいだけです。
Edited by Narazaka 2013/01/10