(正規表現, PHP, JavaScript)ある囲み記号で囲まれた文字列を抜き出す。ただし、エスケープされた囲み記号にも対処する。


最終更新: 2012-07-20 17:46


例えば、下記のようにクオート(')で囲まれた文字列を取り出したい場合。


(検索したい文字列)
'aaa' 'bbb' 'ccc'
とりあえず、単純に。

(PHP)
$str = "'aaa' 'bbb' 'ccc'";
preg_match_all('/\'([^\']*)\'/us', $str, $matches);
var_dump($matches);
uオプションはパターン文字列をUTF-8として扱うため(日本語対策)、sオプションは複数行にまたがって検索するためにつけています。


しかし、囲まれた文字列の中のエスケープされたクオート(\')に対応するとなると、途端にややこしくなります。


(検索したい文字列)
'', 'a\'aaa', 'bbbb\\', 'c\\\'ccc', 'dddd\\\\', '\'\'eee\'\'\'', 'ff \'(改行)
\'ff',
注目するのは、クオートの直前のバックスラッシュの連続数です。
『奇数回の連続 + クオート』ならば、クオートはエスケープされています。
『偶数回の連続 + クオート』ならば、バックスラッシュそのものを表すための記述が連続しているだけで、クオートはエスケープされていません。
よって、正規表現で表す条件は下記となります。

クオートから次のクオートまでの、
クオートでない文字、
または、
奇数回の連続したバックスラッシュの後のクオート、
を含む0文字以上の文字列を抜き出せ。
では実例を。

(PHP)
$str = "('', 'a\\'aaa', 'bbbb\\\\', 'c\\\\\\'ccc', 'dddd\\\\\\\\', '\\'\\'eee\\'\\'\\'', 'ff \\'\n\\'fff')";
preg_match_all('/\'((?:(?:(?!\\\\).)?(?:(?:\\\\\\\\)*\\\\)\'|[^\'])*)\'/us', $str, $matches);
var_dump($matches);
正規表現の部分を、わかりやすく改行とタブを入れて表示します。

\'
(
(?:
(?:(?!\\\\).)?
(?:(?:\\\\\\\\)*\\\\)\'

|[^\']
)*
)
\'

特に解説が必要なのは下記の部分でしょう。

(?:(?!\\\\).)?
(?:(?:\\\\\\\\)*\\\\)\'
『(?:(?:\\\\\\\\)*\\\\)\'』は、『奇数回の連続したバックスラッシュの後のクオート』を表しています。
『(?:\\\\\\\\)*』は、2連続バックスラッシュの0回以上の繰り返し。
つまり、偶数回の連続したバックスラッシュのことです。
その後の1個のバックスラッシュと組み合わせることで、"奇数回"を表しています。


これだけでも十分な気がしますが、『(?:(?!\\\\).)?』を前に置く必要があります。
これは、『奇数回の連続バックスラッシュの直前の文字は、バックスラッシュであってはならない』という、重要な条件です。
『(?:』は、キャプチャしない単なるグループ化です。
『(?!』は、否定先読みです。


【参考】
PHP :: 正規表現 / 前後読み(先読み と 戻り読み) [Tipsというかメモ]
http://tm.root-n.com/programming:php:regex:special_constructs


否定戻り読みを使えば、『(?!\\\\).?』は『(?<!\\\\)』と書けます。
ほんのちょっと文字量を節約できます。
が、JavaScript先読みはできても戻り読みはできないので、汎用性を考えて先読みを使いました。


JavaScriptでは正規表現の中でバックスラッシュをニ重にエスケープする必要はありません。
クオートも正規表現の中ではエスケープする必要はありません。
実例は下記のようになります。


(JavaScript)
var str = "'', 'a\\'aaa', 'bbbb\\\\', 'c\\\\\\'ccc', 'dddd\\\\\\\\', '\\'\\'eee\\'\\'\\'', 'ff \\'\n\\'fff'";
var matches = str.match(/'((?:(?:(?!\\).)?(?:(?:\\\\)*\\)'|[^'])*)'/gm);
var result = '';
for (i=0; i<matches.length; i++) {
result += '[' + i + ']:' + matches[i] + '\n';
}
alert(result);
gオプションはマッチするすべての文字列を抜き出すため、mオプションは複数行にまたがって検索するためにつけています。