php pcre 没有定界符的正则表达式

php pcre regular expressions without delimiter

背景/简介

通常,PHP 中的正则表达式,例如对于 preg_match(),以分隔符开始和结束,例如 /。我个人经常使用 @ 代替。

除了分隔字符,还可以使用左括号和右括号,例如 (){}[].

分隔符char需要被转义,当它应该被解释为常规字符时。例如。 preg_match('@^\w+\@\w+\.\w+$@', $mail) 需要将 '@' 转义为 '\@'.

函数 preg_quote(string $str, ?string $delimiter) 接受 null 作为 $delimiter,这表明可以用我们不必担心分隔符的方式来编写正则表达式。

使用 () 似乎我们不必担心分隔符,因为 '('')' 已经需要转义了。

[]{} 有点不同。孤儿 '[' 会导致错误,而孤儿 ']''{''}' 不会。

动机

对于包开发,我想提供用户可以指定正则表达式片段的方法,而不用担心分隔符的选择。

例如如果我在内部使用 '/' 作为分隔符,那么用户(调用代码)将需要在提供的正则表达式片段中转义 '/' 。如果我使用 '@',他们可以不转义 '/',但需要转义 '@'。如果我使用 null / '()',他们将不需要转义任何东西 - 我认为。

这是一个假想的例子。请不要问->setFragment()是干什么的,你只需要知道第二个参数接收一个正则表达式片段(即可以插入正则表达式的片段)即可。

// If regex like '/../':
$system->setFragment('email', '\w+@\w+\.\w+');  // nothing escaped.
$system->setFragment('dir', '\w+(\/\w+)*');  // '/' escaped.

// If regex like '@..@':
$system->setFragment('email', '\w+\@\w+\.\w+');  // '@' escaped.
$system->setFragment('dir', '\w+(/\w+)*');  // nothing escaped.

// If regex like '(..)':
$system->setFragment('email', '\w+@\w+\.\w+');  // nothing escaped.
$system->setFragment('dir', '\w+(/\w+)*');  // nothing escaped.

另一个例子,更类似于我实际在做的事情:

function buildMessageRegex(string $message, ?string $delimiter, array $regex_fragments = []): string {
  $quoted_message = preg_quote($message, $delimiter);
  $regex_body = strtr($quoted_message, $replacements);
  return $delimiter !== null
    ? $delimiter . '^' . $regex_body . '$' . $delimiter
    : '(^' . $regex_body . '$)';
}

// By using $delimiter === null, we don't have to escape '/' or '@'.
$regex = buildMessageRegex('Mail: %mail, Dir: %dir.', null, [
  '%mail' => '\w+@\w+\.\w+',
  '%dir' => '\w+(/\w+)*',
]);

问题

似乎 () 是编写正则表达式的唯一方法,我不必担心分隔符,并且可以调用 preg_quote($str, null) 并将 null 作为分隔符。

这个假设是否正确?

如果是这样,我总是可以使用 () 作为分隔符,而不需要在方法中提供分隔符选项。

还是我遗漏了什么?

范围

我不确定这个 problem/question 是否特定于 PHP,或者更普遍地适用于任何使用它的 PCRE(我假设是在 Perl 中?)。

我个人对 PHP 案例很感兴趣,但我认为值得一提的是在一个很好的答案中这如何适用于 PHP.

不幸的是,您认为 () 总是需要转义的逻辑是不正确的。它们通常不需要在 [] 内进行转义,但如果 () 是定界符,则需要进行转义。

例如:

preg_match('/[(]/', "foo(bar", $match);

有效,但是

preg_match('([(])', "foo(bar", $match);

收到“未找到结束匹配定界符 ')'”错误。

因此,如果您使用 () 作为分隔符,调用者将需要转义 [] 中的那些字符,这通常不需要。

比对您的问题的具体回答更实用的解决方案。

许多字符都可以用作模式分隔符,包括来自 ascii 范围的 non-printable 个字符:SOH、STX、ETX、EOT、ENQ、ACK ...

它们不太可能在字符串中找到,用户更不可能在键盘上键入它们(如果用户真的决定在模式中加入 SOH,他可能会使用转义符序列 \x01 看东西)。

因此,您可以通过这种方式合理地构建模式(例如使用 SOH):

$pattern = chr(1) . $body . chr(1) . $modifiers;

如果你寻找比SOH(开始标题U+0001)更有意义的东西,你最终可以选择控制字符RS(记录分隔符U+0030)或EOT(传输结束U+0004)。请注意,您不能使用 NUL (U+0000)。

显然,可以肯定的是,无论您选择什么分隔符,总有这两个很好的旧解决方案:转义它或删除它。