私は恥ずかしく思いますが、いくつかの正規表現の側面についてはまだ明確ではありません。形式の文字列リテラルを多数含むテキスト ファイルを解析する必要があり@"I'm a string"
ます。簡単なパターンを作成しまし/@"([^"]*)"/si
た。それは完璧に機能し、preg_match_all はコレクションを返します。ただし、文字列リテラルに のようなエスケープされた引用符が含まれていると、明らかに正しく機能しません@"I'm plain string. I'm \"qouted\" string "
。手がかりをいただければ幸いです。
2 に答える
これは、Freidl の古典的な「展開されたループ」の使用例です: (キャプチャ用に固定グループを編集)
/"((?:[^"\\]|\\.)*)"/
これは、バックスラッシュでエスケープされた引用符を考慮して、引用符で囲まれた文字列と一致します。
フィールド (を含む@
) を照合するために使用する完全な正規表現は次のようになります。
/@"((?:[^"\\]|\\.)*)"/
しかし、注意してください!このパターンが PHP では機能しないと不平を言う人をよく見かけますが、これは、文字列でバックスラッシュを使用するというやや気が遠くなるような性質があるためです。
上記のパターンのバックスラッシュは、PCRE に渡す必要があるリテラルのバックスラッシュを表しています。これは、PHP 文字列で使用する場合、ダブルエスケープする必要があることを意味します。
$expr = '/@"((?:[^"\\\\]|\\\\.)*)"/';
preg_match_all($expr, $subject, $matches);
print_r($matches[1]); // this will show the content of all the matched fields
それはどのように機能しますか?
…聞いてますよね。さて、これを実際に意味のある方法で説明できるかどうか見てみましょう. x
モードを有効にして、少し間隔を空けてみましょう。
/
@ # literal @
" # literal "
( # start capture group, we want everything between the quotes
(?: # start non-capturing group (a group we can safely repeat)
[^"\\] # match any character that's not a " or a \
| # ...or...
\\. # a literal \ followed by any character
)* # close non-capturing group and allow zero or more occurrences
) # close the capture group
" # literal "
/x
この本当に重要な点は次のとおりです。
[^"\\]|\\.
- すべてのバックスラッシュが「バランスが取れている」ことを意味します - すべてのバックスラッシュは文字をエスケープする必要があり、どの文字も複数回考慮されません。- 上記を繰り返しグループにラップ
*
することは、上記のパターンが無制限に発生する可能性があり、空の文字列が許可されることを意味します (空の文字列を許可したくない場合は、*
を aに変更します+
)。これは「展開されたループ」の「ループ」部分です。
しかし、出力文字列にはまだ引用符をエスケープするバックスラッシュが含まれていますか?
確かにそうです。これは単なるマッチング手順であり、マッチングを変更するものではありません。しかし、結果は文字列の内容であるため、シンプルstr_replace('\\"', '"', $result)
は安全で正しい結果を生成します。
ただし、この種のことを行うとき、他のエスケープシーケンスも処理したいことがよくあります。その場合、通常、結果に対して次のようなことを行います。
preg_replace_callback('/\\./', function($match) {
switch ($match[0][1]) { // inspect the escaped character
case 'r':
return "\r";
case 'n':
return "\n";
case 't':
return "\t";
case '\\':
return '\\';
case '"':
return '"';
default: // if it's not a valid escape sequence, treat the \ as literal
return $match[0];
}
}, $result);
これにより、PHP の二重引用符で囲まれた文字列と同様の動作が得\t
られ、タブに\n
置き換えられたり、改行に置き換えられたりします。
単一引用符で囲まれた文字列も許可したい場合はどうすればよいですか?
これは非常に長い間私を悩ませてきました。これは後方参照でより効率的に処理できるのではないかと私は常に感じていましたが、多くの試みが実行可能な結果をもたらすことに失敗しました。
私はこれをします:
/(?:"((?:[^"\\]|\\.)*)")|(?:'((?:[^'\\]|\\.)*)')/
ご覧のとおり、これは基本的に、OR 関係を使用して、基本的に同じパターンを 2 回適用するだけです。これにより、PHP 側でも文字列の抽出が非常に複雑になります。
$expr = '/(?:"((?:[^"\\\\]|\\\\.)*)")|(?:\'((?:[^\'\\\\]|\\\\.)*)\')/';
preg_match_all($expr, $subject, $matches);
$result = array();
for ($i = 0; isset($matches[0][$i]); $i++) {
if ($matches[1][$i] !== '') {
$result[] = $matches[1][$i];
} else {
$result[] = $matches[2][$i];
}
}
print_r($result);
バックスラッシュが前にない引用符が見つかるまで、すべてを照合します。これはJavaにあります:
public static void main(String[] args) {
final String[] strings = new String[]{"@\"I'm a string\"", "@\"I'm plain string. I'm \\\"qouted\\\" \""};
final Pattern p = Pattern.compile("@\"(.*)\"(?<!\\\\)");
System.out.println(p.pattern());
for (final String string : strings) {
final Matcher matcher = p.matcher(string);
while (matcher.find()) {
System.out.println(matcher.group(1));
}
}
}
出力:
I'm a string
I'm plain string. I'm \"qouted\"
パターン (すべての Java エスケープなし) は次のとおりです。@"(.*)"(?<!\\)