嵌套的正则表达式前瞻和后顾

9

我在正则表达式中使用嵌套的'+'/'-'前瞻/后顾时遇到了问题。

假设我想将字符串中的'*'替换为'%',而'\'用于转义下一个字符(类似将正则表达式转换为SQL命令^^)。

所以,字符串

  • '*test*'应该改为'%test%'
  • '\\*test\\*' -> '\\%test\\%',但是
  • '\*test\*''\\\*test\\\*'应保持不变。

我尝试了:

(?<!\\)(?=\\\\)*\*      but this doesn't work
(?<!\\)((?=\\\\)*\*)    ...
(?<!\\(?=\\\\)*)\*      ...
(?=(?<!\\)(?=\\\\)*)\*  ...

如何正确匹配上述示例中的“ * ”?

(?<!\\(?=\\\\)*)\*(?=(?<!\\)(?=\\\\)*)\*之间有什么区别?或者如果这些本质上是错误的,那么具有此类视觉结构的正则表达式之间有什么区别?


4
你使用什么语言?你真的希望\*test\*保持不变,而不是被转换成*test*吗? - Gumbo
5个回答

11

要查找未转义的字符,您需要查找一个在偶数个(或零个)转义字符之前的字符。这相对来说是比较直接的。

(?<=(?<!\\)(?:\\\\)*)\*        # this is explained in Tim Pietzcker' answer

很不幸,许多正则表达式引擎不支持可变长度的后顾断言,因此我们需要用前瞻来替换:

(?=(?<!\\)(?:\\\\)*\*)(\\*)\*  # also look at ridgerunner's improved version

请用第一组的内容和一个%符号替换此处内容。

说明

(?=           # start look-ahead
  (?<!\\)     #   a position not preceded by a backslash (via look-behind)
  (?:\\\\)*   #   an even number of backslashes (don't capture them)
  \*          #   a star
)             # end look-ahead. If found,
(             # start group 1
  \\*         #   match any number of backslashes in front of the star
)             # end group 1
\*            # match the star itself

向前查找确保只考虑偶数个反斜杠。无论如何,必须将它们匹配到一个组中,因为向前查找不会在字符串中提前位置。


1
关于无限长度回溯的好点子(还有@ridgerunner)。并不是每个人都在使用.NET或JGSoft正则表达式引擎。 - Tim Pietzcker

9

好的,既然Tim决定不使用我建议的修改更新他的正则表达式(而Tomalak的答案没有那么简洁),这里是我推荐的解决方案:

将:((?<!\\)(?:\\\\)*)\* 替换为 $1%

这是一个注释过的PHP代码片段:

// Replace all non-escaped asterisks with "%".
$re = '%             # Match non-escaped asterisks.
    (                # $1: Any/all preceding escaped backslashes.
      (?<!\\\\)      # At a position not preceded by a backslash,
      (?:\\\\\\\\)*  # Match zero or more escaped backslashes.
    )                # End $1: Any preceding escaped backslashes.
    \*               # Unescaped literal asterisk.
    %x';
$text = preg_replace($re, '$1%', $text);

附录:非回顾式 JavaScript 解决方案

上述解决方案需要使用回顾式,因此在 JavaScript 中无法使用。以下 JavaScript 解决方案 不会 使用回顾式:

text = text.replace(/(\\[\S\s])|\*/g,
    function(m0, m1) {
        return m1 ? m1 : '%';
    });

这个解决方案将每个反斜杠-任何字符实例替换为它本身,将每个星号*实例替换为百分号%

2011年10月24日编辑:修复了JavaScript版本以正确处理诸如:**text**之类的情况。(感谢Alan Moore指出前一个版本中的错误。)


+1 简化了 @Tim 的正则表达式,但你的适用于 JavaScript 的版本在 **test** 上失败了。 :-/ 我认为这不可能在单个 JS replace 操作中完成。 - Alan Moore
@Alan Moore - 完全正确。感谢您的敏锐眼光!不过,这可以通过使用回调函数的一个 replace() 函数来完成。请查看最新版本。 - ridgerunner

5

其他人已经展示了如何使用lookbehind来完成,但我想为不使用lookarounds辩护。请考虑这个解决方案(演示在这里):

s/\G([^*\\]*(?:\\.[^*\\]*)*)\*/$1%/g;

大部分正则表达式 [^*\\]*(?:\\.[^*\\]*)*,是 Friedl 的“展开循环”习惯用法的一个例子。它尽可能地消耗除星号或反斜杠之外的单个字符,或由反斜杠后跟任何内容组成的字符对。这使它可以避免消耗未转义的星号,无论前面有多少转义的反斜杠(或其他字符)。 \G 将每个匹配锚定到上一个匹配结束的位置,或者如果这是第一次匹配尝试,则锚定到输入的开头。这可以防止正则表达式引擎简单地跳过转义的反斜杠并仍然匹配未转义的星号。因此,/g 控制的每个迭代都会消耗直到下一个未转义的星号的所有内容,在组 #1 中捕获除星号之外的所有内容。然后将其插回并用 % 替换 *
我认为这至少与环视方法一样易读,并且更容易理解。它需要支持 \G,因此在 JavaScript 或 Python 中无法使用,但在 Perl 中完全可以。

4
所以,您希望仅在反斜杠的数量为偶数时(或者换句话说,如果它没有被转义,则匹配*)?那么,您根本不需要前瞻,因为您只需要向后查找,是吗?
搜索:
(?<=(?<!\\)(?:\\\\)*)\*

将其替换为%

解释:

(?<=       # Assert that it's possible to match before the current position...
 (?<!\\)   # (unless there are more backslashes before that)
 (?:\\\\)* # an even number of backslashes
)          # End of lookbehind
\*         # Then match an asterisk

接近了,但是(你知道的),很少有正则表达式引擎支持可变长度的后顾断言。将后顾断言改为捕获组 $1,替换字符串为:$1%,然后它应该适用于大多数(但仍不适用于js)。 - ridgerunner
嗯,没错。希望他在使用.NET :) - Tim Pietzcker
现在,由于bliof已经指定使用Perl,我通常会撤回我的答案,因为由于上述限制,它在Perl中无法工作。但是,由于其他答案正在引用这个答案,所以我将把它留在这里。 - Tim Pietzcker

0
检测正则表达式中转义反斜杠的问题一直吸引着我,最近我才意识到自己完全把它搞复杂了。有几个简化这个问题的方法,据我所知,这里没有人注意到过:
  • 反斜杠会转义后面任何一个字符,不仅仅是其他反斜杠。因此 (\\.)* 将匹配一整串被转义的字符,无论它们是不是反斜杠。你不必担心反斜杠数量的奇偶性;只需检查链的开头或结尾是否有单个反斜杠(ridgerunner 的 JavaScript 解决方案就利用了这一点)。

  • 使用回顾后发断言并不是确保以链的第一个反斜杠开头的唯一方式。你也可以寻找非反斜杠字符(或字符串的开头)。

结果是一个简短、简单的模式,不需要回顾后发断言或回调函数,而且比我目前看到的任何其他内容都要短。

/(?!<\\)(\\.)*\*/g

以及替换字符串:

"$1%"

这在.NET中有效, 允许回溯,它应该也适用于Perl。在JavaScript中也可能实现,但是没有回溯或\G锚点,我无法想到一种一行代码的方法来实现。Ridgerunner的回调应该可以工作,循环也可以:

var regx = /(^|[^\\])(\\.)*\*/g;
while (input.match(regx)) {
    input = input.replace(regx, '$1$2%');
}

在这里我看到了很多我在其他正则表达式问题中认识的名字,而且我知道你们中有一些比我更聪明。如果我犯了错误,请指出来。


1
很不幸,这个正则表达式存在问题。当你尝试匹配类似 *\\* 的内容时,它会失败(你会得到第一个以 ^\* 开头的字符,但第二个字符将进入 [^\\],而 \* 将无法匹配任何内容)。 - bliof
@bliof - 你说得对。我想不出任何在JS风格的正则表达式中解决这个问题的方法,除非使用回调或循环。在其他变体中,如果你要做的不是替换*(比如计算转义反斜杠或其他什么),这仍然有效。我不认为JavaScript可以用一行代码解决这个问题,但我会编辑一些可以解决的东西。谢谢你纠正我,我有强烈的感觉这太好了,不可能是真的。 - Justin Morgan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接