【问题标题】:Preventing look-behind/ahead matches overlapping防止后视/前瞻匹配重叠
【发布时间】:2015-08-18 14:10:45
【问题描述】:

我正在尝试匹配包含在 引号之间的字符串文字的所有部分。

(?<=[\"]).*?(?=(?<=[^\\])[\"]{1})

上面是一个有效的正则表达式,除了一个例外,它当然会匹配字符串文字的左右两边都有引号的所有部分,而不管引号对。

例如(星号表示匹配字符):

Hello "my" name is "Andy", nice to meet you.`
       ** ********* ****

这里的字符串文字部分“name is”被匹配只是因为它的两边都有一个引号字符。这对于我们正在寻找的东西是不正确的。理想的结果是:

Hello "my" name is "Andy", nice to meet you.`
       **           ****

完全理解这是可能的,也许应该通过编写状态引擎来完成 - 我的问题是 - 在正则表达式中 - 如果可能的话,我如何防止后视匹配字符串文字部分以前与前瞻匹配?

【问题讨论】:

  • \"((?>\\"|.)*?)\" 不可行,因为这与 between 引号不匹配。因此向后看/向前看必须用于确定引用存在但不匹配它们。
  • 根据您使用的语言,您将能够将其转换为平衡括号的问题(在这种情况下 - 平衡引号)。那么您使用的是什么语言?
  • @ndn 我不喜欢编程,因为这更像是个人好奇问题,而不是现场项目问题。但是,最好是在 .NET 正则表达式状态引擎中工作的解决方案。
  • This 可能做你想做的事,但是,恕我直言,你最好捕获引号而不是去掉它们。

标签: regex


【解决方案1】:

前奏

我使用 Ruby,因为您说您没有偏好,这更多是个人兴趣,而不是实际的现场产品。但是,请注意,虽然这里使用的一些技巧可能不适用于各种正则表达式引擎(如 javascript 所拥有的引擎)或对相同事物有不同的语法,但这里没有使用任何 Ruby 特定的东西。相同的正则表达式可以在 Perl、Sublime Text 和更多地方使用。


但在我们开始之前...

免责声明:这不是这样做的方法!不要在您的生产代码库中使用它!


现在我们解决了这个问题……这是一个非常有趣的问题。与任何其他复杂问题一样,divide and conquer 是一种方法。

我们将要使用的技巧:

  1. 命名组

正如您可以使用(group_contents) 创建编号组一样,您也可以使用(?<group_name>group_contents) 定义命名组。从技术上讲,我们不需要它,但它会让一切变得更加全面。

  1. 重新执行组模式

您可以使用\g<group_name_or_number> 执行之前定义的相同模式。例如:

(?<three_letter_word>\b\w{3}\b) \g<three_letter_word>

将匹配xyz abc

  1. 重复零次

乍一看,{0} 似乎毫无用处。但是,结合以上两者,它可以像定义函数一样工作而不执行它们。例如:

(?<even>[02468]){0}7\g<even>8\<even>9\<even>0

将匹配7x8y9z0,其中xyz 是偶数。

  1. 删除匹配的字符

许多正则表达式引擎中的一个常见限制是您不能定义可变长度的lookbehinds。即使在你可以的地方(比如在 java 中),你仍然必须定义最大长度。所以,你不能做(?&lt;=x*)之类的事情。

\K 来救援。 \K 基本上翻译为删除到目前为止匹配的所有内容。所以换句话说,(?&lt;=x*)y 可以重写为x*\Ky

有了这些技巧,让我们开始吧。


首先,让我们定义一些“函数”(使用技巧#3)。
  1. escaped_quote

转义引用是前面有奇数反斜杠(\)的"反斜杠具有转义字符的特殊含义,因此,要匹配单个反斜杠,我们需要用另一个(又名\\ = 一个文字反斜杠)。

要匹配偶数反斜杠,我们可以使用\\{2}*(也就是两个反斜杠零次或多次 - 2*n)。为了使它奇怪,我们只需添加一个反斜杠 - \\\\{2}* (2*n + 1)。

我们还想说我们要匹配此序列中的所有反斜杠。这是因为正则表达式引擎会尝试找到 偶数反斜杠 来搞砸我们,除非我们另有说明。 \\\" 将被解释为 non escaped 引号,因为它可以匹配 \\",忽略第一个斜杠。为了避免这种情况,我们将添加一个否定的lookbehind,如下所示:(?&lt;!\\)\\\\{2}*

我们的escaped_quote “函数”的最终定义如下:

(?<escaped_quote>(?<!\\)\\\\{2}*"){0}
  1. 非引用

我们要表达的另一件事是没有有意义的引号。这是一个字符序列,它们是转义的引号或根本不是引号。

请注意,对于 not 引号部分,我们需要为 escaped_quote 添加负前瞻。这是为了确保我们不会吃掉 escaped_quote 中的第一个 \,这会给我们留下剩余的未转义的引用

(?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}
  1. balanced_quotes

我们需要的最后一个 "function" 是一个序列,它没有不匹配的引号。这可以是完全没有有意义引号的东西,也可以是偶数有意义引号:

(?<balanced_quotes>\g<non_quoting>|(?:\g<non_quoting>"\g<non_quoting>){2}+){0}


完成所有准备工作后,我们就可以匹配了。

我们将从字符串的开头或单引号开始。前者很明显。后者是因为我们的比赛会留下一个报价。 (?:^|")

编辑:事实证明这些还不够。对于上次我们匹配 empty string 的情况,\K 将不允许我们在 improvised lookbehind 中保持在同一位置并匹配 empty string 再次。为了解决这个问题,我们将添加另一个替代方案 - 空字符串。请注意,此处的顺序很重要,因此我们仅在其他两个失败时才使用此替代方案:(?:^|"|)

随后是 non_quoting 序列,所有内容都被丢弃(使用 #4 技巧)以实现后视:

(?:^|"|)\g<non_quoting>"\K

之后,我们实际匹配的是一个non_quoting序列:

(?:^|"|)\g<non_quoting>"\K\g<non_quoting>

最后,我们必须确保在关闭当前报价后,在字符串的末尾还剩下 balanced_quotes

(?:^|"|)\g<non_quoting>"\K\g<non_quoting>(?="\g<balanced_quotes>$)


终于!

我们可以将我们的"function"定义和实际匹配一起添加,来实现最终的正则表达式:

(?<escaped_quote>(?<!\\)\\\\{2}*"){0}(?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}(?<balanced_quotes>\g<non_quoting>|(?:\g<non_quoting>"\g<non_quoting>){2}+){0}(?:^|"|)\g<non_quoting>"\K\g<non_quoting>(?="\g<balanced_quotes>$)

See it in action


最后的想法

这里需要注意的是,即使您的正则表达式引擎不支持某些功能,您也可以通过放置函数调用来实现相同的正则表达式。唯一没有随处可见但您需要的是\K

我希望这对阅读本文的每个人来说都是一次有趣的学习经历。

【讨论】:

  • @iismathwizard,已修复。只需要更改non_quoting 中两个选项的顺序。
  • 嗯,\\"s\"s\\"g" 看起来不太对劲
  • 既然你很擅长发现我的正则表达式中的问题,介意看看我最近的编辑,看看现在是否还好?
  • @iismathwizard,在这种情况下,您有 3 (odd) 个未转义的引号。因此,对于这种情况,问题是未定义的,任何行为都是可以接受的(只要没有无限处理)。
  • 啊,好吧。不完全确定什么行为应该是诚实的,这就是为什么它更像是一个问题而不是评论:p
【解决方案2】:

编辑

作为 .NET 正则表达式支持无限重复,正则表达式:

(?<!(.|\n)\G")(?<!(^|[^\\])(\\\\)*\\")(?:(?<=")(?:(?:\\\\|\\"|[^"])+?)(?=")|(?<=")(?="))

在 .NET 中比在 Java 中效果更好(因为在不是最佳解决方案中使用间隔)。

DEMO

以前的答案

我想我找到了一种方法,但只能在 Java 中使用正则表达式:

(?<!(.|\n)\G")(?<!(^|[^\\])(\\\\){0,20}\\")(?:(?<=")(?:(?:\\\\|\\"|[^"])+?)(?=")|(?<=")(?="))

它基于我之前的尝试,它只能在 Java 中工作(据我所知),因为它在负面的后视部分中使用,这是该语言允许的语法。

正则表达式的解释:

正则表达式以两个否定的lookbehind开头,这应确保正则表达式不会与先前引用的引号匹配,并且不会与转义的引号匹配。

(?&lt;!(.|\n)\G") - 这部分负责忽略引号,这是以前引用的一部分。所以这是一个消极的回顾:

  • 任何字符. 也可以换行\n(但是,如果您使用Java 中的DOTALL 模式,. 就足够了),然后是;
  • \G - 上一个匹配的结束位置或行首,因此如果另一个匹配在此特定字符上结束,则正则表达式在 " 之后无法匹配,
  • " - 引号,

((?&lt;!(^|[^\\])(\\\\){0,20}\\") 负责忽略引用之外的转义引号,因此它防止匹配从无效点开始。它是消极的向后看:

  • (^|[^\\]) - 行首,或引号以外的字符(防止下一部分从反斜杠序列的中间匹配,如\\\\\\\\"xxx"),后跟;
  • (\\\\){0,20} - 零个或多个(最多 20 个)组的两个反斜杠(以确保它是转义的引号),后跟;
  • \\ - 单个转义反斜杠,

在大多数语言中,lookbehind是零长度的,它需要有固定的长度,所以它不允许使用quatifires或intervals(+,*,?,@987654343 @)。但是在 Java 中,可以使用 ? 和具有最小和最大长度的间隔。所以,(\\\\){0,20} 中的 20 是一个最大值,它可能会更多,但我想没有人会连续使用超过(甚至接近)20 个双反斜杠。但它仍然是值得记住的。在这个正则表达式中,这个构造用于匹配偶数个反斜杠,并确定引号前的反斜杠是转义字符还是转义后面的字符。

这部分后面是alternative,用于匹配有内容的引用,没有任何内容。最后一部分(用于无内容匹配)更简单:(?&lt;=")(?=")),它应该匹配两个有效引号之间的点,但由于(?&lt;!(.|\n)\G") 部分,它不会匹配例如第二个和连续第三个引号(如""")。第一种选择稍微复杂一点:

(?&lt;=")(?:(?:\\\\|\\"|[^"])+?)(?=") 匹配一个前后加引号的字符串。它包括:

  • (?&lt;=") - 正向查找引号,
  • (?:(?:\\\\|\\"|[^"])+?) - 下面解释的替代方法,
  • (?=") - 引号的正向前瞻,

(?:\\\\|\\"|[^"])+?)* 可以替代:

  • \\\\ - 反斜杠,在\"之前匹配很重要,以避免将\\"这样的大小写匹配为\"
  • \\" - 引号和反斜杠,在 [^"] 之前匹配很重要,因此 \" 将作为引号的一部分进行匹配;
  • [^"] 任何不是引号的东西

Ideone demonstration in Java.

Regex demonstration on RegexPlanet - 点击 Java

【讨论】:

  • "\\" 中断。
  • 这确实很好,但我对其进行了一些修改以更好地工作。我觉得编辑我的答案是错误的,因为我什至不知道\G。这就是我所做的。随意编辑您的答案:(?&lt;!.\G")(?&lt;=")(?&gt;\\.|.)*?(?=")
  • @ndn 谢谢你的评论,确实不匹配`\`。
  • @iismathwizard 谢谢,我很感激,但无论如何我都不会使用它,因为:1)这不是我的答案,2)它也有一些缺陷demo,但是原子分组好主意。
  • @m.cekiera 哦!我没有看到那些缺陷。感谢您指出他们:o
【解决方案3】:

编辑

我认为这是现在。

(?<!.\G")(?<="|\\\\")(?<![^\\]\\")((?>\\.|[^"])*?)(?=")

Regex101

【讨论】:

  • 更好"((?&gt;\\"|[^"])*)"
  • 因为这不匹配 between 引号。因此,必须使用look-behind/aheads 来确定引用的存在但不匹配它们。
  • @AndyJames 我正在尝试匹配引号之间包含的字符串文字的所有部分。我不确定我是否理解
  • 关键字在之间不包括。
  • 那么这应该没问题,因为它没有捕获引号。
【解决方案4】:

作为一般方法,您可以执行以下操作。我在代码中添加了puts 语句来显示发生了什么。

str = 'Hello "my" name is "Andy", nice to meet "Sally"'

r = /
    (       # start capture group 1
    .*?     # match >= 0 characters lazily 
    (?<=\") # match " in a positive lookbehind
    (.*?)   # match >= 0 characters lazily in capture group 2
    (?=\")  # match " in a positive lookahead
    .       # match one character
    )       # close capture group 1
    /x      # extended mode

a = []
s = str.dup
loop do
  break a unless s =~ r
  puts
  puts "$1 = |#{$1}|"
  puts "$2 = |#{$2}|"
  a << $2
  puts "a  = #{a}"
  s = s[$1.size..-1]
  puts "s  = |#{s}|"
end

$1 = |Hello "my"|
$2 = |my|
a  = ["my"]
s  = | name is "Andy", nice to meet "Sally"|

$1 = | name is "Andy"|
$2 = |Andy|
a  = ["my", "Andy"]
s  = |, nice to meet "Sally"|

$1 = |, nice to meet "Sally"|
$2 = |Sally|
a  = ["my", "Andy", "Sally"]
s  = ||
  #=> ["my", "Andy", "Sally"] 

这里的关键是在正向前瞻之后匹配一个字符。没有它,代码将返回:

["my", " name is ", "Andy", ", nice to meet ", "Sally"]

请注意,$1 的值表明在正向前瞻之后匹配的字符不包含在匹配中。此外,', nice to meet "Sally"' 匹配,即使 'Sally' 后面没有字符。

我不明白. 匹配的是什么? @ndd 解释说它与s 中的" 匹配,考虑到环视是零宽度,这非常有意义。

【讨论】:

  • . 正在匹配当前匹配的结束 "。没有它,开始时的.*? 只匹配最后一场比赛的",它基本上就像用" 分割字符串(也就是每场比赛只消耗一个",而结束的"用于下一场比赛)。
猜你喜欢
  • 2015-09-29
  • 1970-01-01
  • 1970-01-01
  • 2021-01-31
  • 2013-06-11
  • 1970-01-01
  • 1970-01-01
  • 2018-02-02
  • 1970-01-01
相关资源
最近更新 更多