防止后视/前方匹配重叠

时间:2015-08-18 14:10:46

标签: regex

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

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

以上是一个正常工作的正则表达式,除了一个例外,它当然会匹配字符串文字的所有部分,其左侧和右侧都有引号,无论引用对。

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

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

字符串文字部分&#34; 名称是&#34;这里匹配只是因为它的两边都有一个引号。这对我们正在寻找的东西是不正确的。理想的结果是:

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

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

4 个答案:

答案 0 :(得分:5)

<强>前奏

我使用过Ruby,因为你说你没有偏好,这比个人实际产品更具个人利益。但请注意,虽然这里使用的一些技巧可能不适用于各种正则表达式引擎(如javascript一样)或者对于相同的东西有不同的语法,但这里没有使用Ruby特定的东西。相同的正则表达式将在Perl,Sublime Text和更多地方工作。

<小时/> 但在我们开始之前......

免责声明:这不是办法!不要在生产代码库中使用它!

<小时/> 现在我们已经解决了这个问题......这是一个非常有趣的问题。与任何其他复杂问题一样,divide and conquer就是这样。

我们将要使用的技巧:

  1. 命名组
  2. 就像您可以使用(group_contents)创建编号组一样,您可以使用(?<group_name>group_contents)定义命名组。我们在技术上并不需要这样做,但它会使一切变得更加全面。

    1. 重新执行群组模式
    2. 您可以使用\g<group_name_or_number>执行之前定义的相同模式。例如:

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

      将匹配xyz abc

      1. 重复零次
      2. 乍一看,{0}可能看起来毫无用处。但是,结合上面的两个,它可以像定义函数而不执行它们那样工作。例如:

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

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

        1. 删除匹配的字符
        2. 许多正则表达式引擎中的一个常见限制是您无法定义具有可变长度的lookbehinds。即使在你可以使用的那些(比如在java中),你仍然需要定义最大长度。因此,您无法执行(?<=x*)之类的操作。

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

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

          <小时/> 首先,让我们定义一些&#34;函数&#34; (使用技巧#3 )。

          1. escaped_quote
          2. 转义引号为",前面有奇数数量的反斜杠\ )。 反斜杠具有转义字符的特殊含义,因此,为了匹配单个反斜杠,我们需要将其转义为另一个(又名\\ = < em>一个字面反斜杠)。

            要匹配偶数数量的反斜杠,我们可以执行\\{2}*(又称两个反斜杠零次或多次 - 2 * n )。要使其奇数,我们只需添加一个反斜杠 - \\\\{2}* 2 * n + 1 )。

            我们还想说我们希望在此序列中匹配所有反斜杠。这是因为正则表达式引擎会尝试找到一个甚至数量的反斜杠来搞砸我们,除非我们另外说明。 \\\"将被解释为非转义引用,因为它只能匹配\\",而忽略第一个斜杠。为了不允许这种情况,我们将添加负面的背后照,如:(?<!\\)\\\\{2}*

            escaped_quote &#34;&#34; 的最终定义如下所示:

            (?<escaped_quote>(?<!\\)\\\\{2}*"){0}
            
            1. non_quoting
            2. 我们要表达的另一件事是没有有意义的引号。这是一系列字符,它们是转义引号或根本不是引号。

              请注意,对于不是所有部分的引号,我们需要为 escaped_quote 添加否定前瞻。这是为了确保我们不会吃掉 escaped_quote 中的第一个\,这将留下剩余的未转义的报价。< / p>

              (?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}
              
              1. balanced_quotes
              2. 我们需要的最后一个&#34;函数&#34; 是一个序列,它没有不匹配的引号。这可以是完全没有有意义引号的内容,也可以是偶数数量有意义的引号:

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

                <小时/> 完成所有准备工作后,我们随时准备匹配。

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

                编辑:原来这些还不够。对于我们上次匹配空字符串的情况,\K将不允许我们保持在相同的位置并匹配中的空字符串即兴表演背后的。要解决此问题,我们将添加另一个替代方法 - 空字符串。请注意,此处的顺序很重要,因此如果其他两个失败,我们只会使用此替代方法:(?:^|"|)

                接下来是 nonquo​​ting 序列,所有内容都被删除(使用#4 技巧)以实现后视:

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

                之后,我们实际匹配的是 nonquo​​ting 序列:

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

                最后,我们必须确保在关闭当前报价后,我们留下 balanced_quotes 直到字符串的结尾:

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

                <小时/>

                最后!

                我们可以将我们的&#34;函数&#34; 定义和实际匹配加在一起,以实现最终的正则表达式:

                (?<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

                我希望每个阅读此内容的人都能获得有趣的学习经历。

答案 1 :(得分:1)

修改

由于.NET正则表达式支持无限重复,正则表达式:

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

在.NET中比在Java中工作得更好(在非最佳解决方案中使用间隔)。

DEMO

以前的答案

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

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

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

正则表达的解释:

正则表达式以两个负面的lookbehind开头,它应该确保正则表达式与先前引用的引号不匹配,并且它与转义引号无法匹配。

(?<!(.|\n)\G") - 这部分负责忽略引号,这是之前引用的一部分。所以这是一个负面的背后隐藏:

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

((?<!(^|[^\\])(\\\\){0,20}\\")负责在引文之外隐藏转义的引号,因此阻止匹配从无效点开始。对于:

,这是一种负面的观察
  • (^|[^\\]) - 一行的开头,或引号以外的字符(它是为了防止下一部分与反斜杠序列的中间匹配,如\\\\\\\\"xxx"),后跟;
  • (\\\\){0,20} - 零个或多个(最多20个)两个反斜杠组(以确保它被转义为quatation mark),然后是;
  • \\ - 单个转义反斜杠,

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

此部分之后是匹配引用与内容的替代方案,没有任何内容。最后一部分(对于没有内容的匹配)是一个更简单的部分:(\\\\){0,20},它应匹配两个有效引号之间的点,但由于(?<=")(?="))部分,它将与beet之间的点不匹配例如,连续的第二个和第三个引号(如(?<!(.|\n)\G"))。第一种选择有点复杂:

"""匹配一个前后跟引号的字符串。它包括:

  • (?<=")(?:(?:\\\\|\\"|[^"])+?)(?=") - 引号的正面背后,
  • (?<=") - 下面介绍的替代方案,
  • (?:(?:\\\\|\\"|[^"])+?) - 引号的正向前瞻,

(?=")可替代:

  • (?:\\\\|\\"|[^"])+?)* - 要反斜杠,请务必在\\\\之前对其进行匹配,以避免将\"等案例与\\"匹配,
  • \" - 引号和反斜杠,在\\"之前匹配很重要,因此[^"]将作为报价的一部分进行匹配;
  • \"任何非引号的内容

Ideone demonstration in Java.

Regex demonstration on RegexPlanet - 点击Java

答案 2 :(得分:0)

修改

我认为现在就是这样。

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

Regex101

答案 3 :(得分:0)

您可以执行以下操作作为一般方法。我在代码中添加了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,考虑到外观是零宽度,这是完全合理的。