8

最近,许多正则表达式问题在查询中都有某种环视元素,在我看来,这对于匹配的成功并不是必需的。是否有一些教学资源正在推广它们?我试图弄清楚使用正面/后面的积极展望会更好地解决哪种情况。我可以看到的主要应用是尝试匹配元素时。但是,例如,最近一个问题中的这个查询有一个简单的解决方案来捕获.*,但是为什么要使用后面的查找呢?

(?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span

而这个来自另一个问题:

$url = "www.example.com/id/1234";
preg_match("/\d+(?<=id\/[\d])/",$url,$matches);

什么时候使用积极的环视真的更好?你能举一些例子吗?

我意识到这接近于基于意见的问题,但我认为答案将非常有启发性。正则表达式在没有让事情变得更复杂的情况下足够令人困惑......我已经阅读了这个页面,并且对何时使用它们而不是它们如何工作的一些简单指南更感兴趣。


感谢所有的答复。除了以下内容,我建议您在此处查看m.buettner 的出色答案

4

8 回答 8

8
  1. 您可以捕获重叠匹配,并且您可以找到可能位于其他匹配环视中的匹配。
  2. 您可以表达有关您的匹配的复杂逻辑断言(因为许多引擎允许您使用多个后向/前瞻断言,所有这些断言都必须匹配才能使匹配成功)。
  3. Lookaround 是表达常见约束“匹配 X,如果它后跟/之前是 Y”的一种自然方式。添加必须通过后处理丢弃的额外“匹配”部分(可以说)不太自然。

当然,否定的环视断言甚至更有用。结合#2,它们可以让你做一些漂亮的向导技巧,这些技巧甚至可能很难用通常的程序逻辑来表达。


应普遍要求的示例:

  • 重叠匹配:假设您想找到给定基因序列中的所有候选基因。基因一般以 ATG 开头,以 TAG、TAA 或 TGA 结尾。但是,候选人可能会重叠:可能存在错误的开始。因此,您可以使用这样的正则表达式:

    ATG(?=((?:...)*(?:TAG|TAA|TGA)))
    

    这个简单的正则表达式查找 ATG 起始密码子,然后是一些密码子,然后是一个终止密码子。它提取所有看起来像基因的东西(无起始密码子),并正确输出基因,即使它们重叠。

  • 零宽度匹配:假设您想tr在计算机生成的 HTML 页面中查找具有特定类的每个。你可能会做这样的事情:

    <tr class="TableRow">.*?</tr>(?=<tr class="TableRow">|</table>)
    

    这涉及</tr>在行内出现裸露的情况。(当然,一般来说,HTML 解析器是更好的选择,但有时您只需要一些快速而肮脏的东西)。

  • 多重约束:假设您有一个文件,其中包含类似 的数据id:tag1,tag2,tag3,tag4,标签以任意顺序排列,并且您希望找到所有带有标签“green”和“egg”的行。这可以通过两个前瞻轻松完成:

    (.*):(?=.*\bgreen\b)(?=.*\begg\b)
    
于 2013-09-30T23:02:57.220 回答
4

环视表达式有两个很棒的地方:

  • 它们是零宽度断言。它们需要匹配,但它们不消耗任何输入字符串。这允许描述不包含在匹配结果中的字符串部分。通过在环视表达式中使用捕获组,它们是多次捕获部分输入的唯一方法。
  • 他们简化了很多事情。虽然它们不扩展正则语言,但它们很容易允许组合(相交)多个表达式以匹配字符串的同一部分。
于 2013-09-30T23:30:05.583 回答
1

Lookaround assertions也可用于减少可能是正则表达式性能不佳backtracking的主要原因。

例如: 正则表达式^[0-9A-Z]([-.\w]*[0-9A-Z])*@(1) 也可以写成^[0-9A-Z][-.\w]*(?<=[0-9A-Z])@(2) 使用正面的外观(简单验证电子邮件地址中的用户名)。

正则表达式(1)本质上会导致大量回溯,因为它是嵌套量词[0-9A-Z]的子集。[-.\w]正则表达式 (2) 减少了过多的回溯,更多信息在这里Backtracking,部分Controlling Backtracking > Lookbehind Assertions

有关更多信息backtracking

于 2013-10-10T12:40:53.700 回答
1

它们很方便的一个简单情况是,当您将模式锚定到一行的开始或结束时,并且只想确保某些东西在您匹配的模式的前面或后面。

于 2013-09-30T23:40:14.447 回答
1

我试图解决你的观点:

  • 在我看来,查询中的某种环视元素对于匹配的成功不是必需的

    当然,它们是比赛所必需的。一旦环视断言失败,就没有匹配项。它们可用于确保模式周围的条件,这些条件也必须是真的。整个正则表达式只匹配,如果:

    1. 该模式确实适合并且

    2. 环顾断言是正确的。

    ==> 但是返回的匹配只是模式。

  • 什么时候使用积极的环视真的更好?

    简单的答案:当你想要东西在那里,但你不想匹配它!

    正如Bergi 在他的回答中提到的,它们是零宽度断言,这意味着它们不匹配字符序列,它们只是确保它在那里。所以环视表达式中的字符不是“消耗”的,正则表达式引擎在最后一个“消耗”字符之后继续。

  • 关于你的第一个例子:

    (?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span
    

    我认为你有一个误解,当你写“有一个简单的解决方案来捕获.*”时。不是“捕获”的.*,它是表达式匹配的唯一内容。但只有那些<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">前面有“ <\/a><span”,后面有“”的字符才会被匹配(这两个不是匹配的一部分!)。

    “已捕获”仅是已由捕获组匹配的内容。

  • 第二个例子

    \d+(?<=id\/[\d])
    

    很有趣。它匹配一个数字序列 ( \d+) 并且在序列之后,lookbehind 断言检查是否有一个数字前面带有“id/”。意味着如果有多个数字或数字之前的文本“id/”丢失,它将失败。意味着当之前有合适的文本时,这个正则表达式只匹配一个数字。

  • 教学资源

于 2013-10-09T08:48:37.443 回答
1

我假设您了解环视的良好用途,并询问为什么在没有明显原因的情况下使用它们。

我认为人们使用正则表达式的方式主要有四类:

验证
验证通常对整个文本进行。像您描述的环顾四周是不可能的。

匹配
提取文本的一部分。使用 Lookarounds 主要是由于开发人员的懒惰:避免捕获
例如,如果我们在设置文件中有一行Index=5,我们可以匹配/^Index=(\d+)/并获取第一组,或者匹配/(?<=^Index=)\d+/并获取所有内容。
正如其他答案所说,有时您需要在匹配之间重叠,但这些相对较少。

替换
这类似于匹配,但有一个区别:整个匹配被删除并被替换为新字符串(以及一些捕获的组)。
示例:我们要突出显示 中的名称"Hi, my name is Bob!"
我们可以用 替换/(name is )(\w+)/$1<b>$2</b>
用 - 替换更简洁,/(?<=name is )\w+/而且<b>$&</b>根本没有捕获。

Split
split获取文本并将其分解为一组标记,您的模式是分隔符。这是通过以下方式完成的:

  • 找一个match. 这场比赛之前的一切都是象征性的。
    • 匹配的内容被丢弃,但是:
    • 在大多数情况下,匹配中每个捕获的组也是一个标记(特别是在 Java 中不是)。
  • 当没有更多匹配时,文本的其余部分是最后一个标记。

在这里,环视是至关重要的。匹配一个字符意味着将其从结果中删除,或者至少将其与其标记分开。
示例:我们有一个逗号分隔的引用字符串列表:用逗号分隔"Hello","Hi, I'm Jim."
/,/错误的:{ "Hello", "Hi, I'm Jim."}
我们不能添加引号,/",/: { "Hello, "Hi, I'm Jim."}
唯一好的选择是向后看,/(?<="),/: { "Hello", "Hi, I'm Jim."}

就个人而言,只要可能,我更喜欢匹配标记而不是用分隔符分割。

结论

要回答主要问题 - 使用这些外观是因为:

  • 有时您无法匹配需要的文本。
  • 开发人员无动于衷。
于 2013-10-09T12:31:32.037 回答
1

我前一阵子输入了这个,但很忙(现在仍然很忙,所以我可能需要一段时间才能回复)并且没有时间发布它。如果您仍然愿意接受答案...


是否有一些教学资源正在推广它们?

我不这么认为,我相信这只是巧合。

但是,例如,最近一个问题的这个查询有一个简单的解决方案来捕获.*,但是为什么要使用后面的查找呢?

(?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span

这很可能是一个 C# 正则表达式,因为我的许多正则表达式引擎不支持可变宽度后视。好吧,这里当然可以避免环顾四周,因为为此,我相信拥有捕获组确实更简单(并让.*我们变得懒惰):

(<td><a href="\/xxx\.html\?n=[0-9]{0,5}">).*?(<\/a><span)

如果是为了更换,或者

<td><a href="\/xxx\.html\?n=[0-9]{0,5}">(.*?)<\/a><span

比赛。尽管在这里使用 html 解析器肯定会更可取。

在这种情况下,我相信环视会更慢。请参阅regex101 演示,其中匹配是捕获组的 64 步,但环视的匹配是 94+19 = 1-3 步。

什么时候使用积极的环视真的更好?你能举一些例子吗?

好吧,lookarounds 具有零宽度断言的属性,这意味着它们不会真正有助于匹配,而它们有助于决定匹配什么并且还允许重叠匹配。

想一想,我也认为,消极的环顾四周使用得更频繁,但这并不会使积极的环顾四周变得不那么有用!

在浏览我的一些旧答案时,我可以找到一些“漏洞利用”(下面的链接将是来自 regex101 的演示)。当/如果你看到你不熟悉的东西,我可能不会在这里解释它,因为问题集中在积极的环顾四周,但你总是可以查看我提供的演示链接,其中有正则表达式的描述,如果您仍然需要一些解释,请告诉我,我会尽力解释。

要获得某些字符之间的匹配:

在某些比赛中,正向前瞻使事情变得更容易,前瞻也可以做,或者当不使用前瞻不太实际时:

狗叹了口气。“我不是超级狗,也不是特别狗,”狗说,“我是普通狗,别管我!” 狗推开他,走向另一只狗。

我们希望获得所有dog(无论大小写)外部报价。通过积极的展望,我们可以做到这一点

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*$)

以确保前面有偶数个报价。使用负前瞻,它看起来像这样

\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

以确保前面没有奇数的报价。或者如果您不想要前瞻,请使用类似的东西,但您必须提取第 1 组匹配项

(?:"[^"]+"[^"]+?)?(\bdog\b)

好的,现在说我们想要相反的;在引号找到“狗” 。带有环视的正则表达式只需要反转符号,firstsecond

\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*$)

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

但如果没有前瞻,这是不可能的。你能得到的最接近的可能是这样的:

"[^"]*(\bdog\b)[^"]*"

但这并没有得到所有的比赛,或者你可以使用这个

"[^"]*?(\bdog\b)[^"]*?(?:(\bdog\b)[^"]*?)?"

但这对于更多的出现是不切实际的,dog并且您会在变量中获得越来越多的数字...这确实更容易使用环视,因为它们是零宽度断言,您不必担心环视内部的表达式匹配dog与否,否则正则表达式不会获得dog引号中的所有出现。

当然,现在这个逻辑可以扩展到字符组,例如获取单词之间的特定模式,例如startend

重叠匹配

如果你有这样的字符串:

abcdefghijkl

并且想要提取所有可能的连续 3 个字符,您可以使用这个

(?=(...))

如果你有类似的东西:

1A Line1 Detail1 Detail2 Detail3 2A Line2 Detail 3A Line3 Detail Detail

并且想要提取这些,知道每一行都以#A Line#(其中#是一个数字)开头:

1A Line1 Detail1 Detail2 Detail3
2A Line2 Detail
3A Line3 Detail Detail

你可以试试这个,因为贪婪而失败......

[0-9]+A Line[0-9]+(?: \w+)+

或者这个,当它变得懒惰时不再起作用......

[0-9]+A Line[0-9]+(?: \w+)+?

但是通过积极的展望,你会得到

[0-9]+A Line[0-9]+(?: \w+)+?(?= [0-9]+A Line[0-9]+|$)

并适当地提取需要的东西。

另一种可能的情况是您有这样的情况:

#ff00fffirstword#445533secondword##008877thi#rdword#

您要转换为三对变量(其中第一对是 # 和一些十六进制值 (6) 以及它们后面的任何字符):

#ff00ff and firstword
#445533 and secondword#
#008877 and thi#rdword#

如果“单词”中没有散列,使用 就足够了(#[0-9a-f]{6})([^#]+),但不幸的是,情况并非如此,您必须求助于.*?而不是[^#]+,这还不能完全解决杂散散列的问题。然而,积极的前瞻使这成为可能

(#[0-9a-f]{6})(.+?)(?=#[0-9a-f]{6}|$)


验证和格式化

不推荐,但您可以使用积极的前瞻来快速验证。例如,以下正则表达式允许输入包含至少 1 个数字和 1 个小写字母的字符串。

^(?=[^0-9]*[0-9])(?=[^a-z]*[a-z])

当您检查字符长度但在 a 字符串中具有不同长度的模式时,这可能很有用,例如,具有有效格式的 4 字符长字符串,其中#表示数字,连字符/破折号/减号-必须在中间:

##-#
#-##

这样的正则表达式可以解决问题:

^(?=.{4}$)\d+-\d+

否则,您^(?:[0-9]{2}-[0-9]|[0-9]-[0-9]{2})$现在可以想象最大长度为 15;您需要的更改次数。

如果您想要一种快速而肮脏的方式以“混乱”格式重新排列某些日期mmm-yyyyyyyy-mm使用更统一的格式mmm-yyyy,您可以使用以下方法:

(?=.*(\b\w{3}\b))(?=.*(\b\d{4}\b)).*

输入:

Oct-2013
2013-Oct

输出:

Oct-2013
Oct-2013

另一种方法可能是使用正则表达式(正常匹配)并分别处理所有不合格的格式。

我在 SO 上遇到的其他东西是印度货币格式,它是##,##,###.###(小数点左侧 3 位数字和所有其他数字成对分组)。如果你有一个输入122123123456.764244,你期望1,22,12,31,23,456.764244并且如果你想使用一个正则表达式,这个是这样的:

\G\d{1,2}\K\B(?=(?:\d{2})*\d{3}(?!\d))

(?:\G|^)仅使用链接中的 ,因为\G仅在字符串的开头和匹配后匹配)并且我认为如果没有积极的前瞻,这将无法工作,因为它会在不移动替换点的情况下向前看。)

修剪

假设你有:

   this    is  a   sentence    

并想用一个正则表达式修剪所有空格。您可能很想对空格进行一般替换:

\s+

但这会产生thisisasentence. 好吧,也许用一个空格替换?它现在产生“这是一个句子”(使用双引号,因为反引号会吃掉空格)。但是,您可以这样

^\s*|\s$|\s+(?=\s)

这确保留下一个空格,以便您可以用任何内容替换并得到“这是一个句子”。

分裂

好吧,积极的环顾可能有用的其他地方是,假设你有一个字符串ABC12DE3456FGHI789并且想要将字母+数字分开,那就是你想要得到ABC12,DE3456FGHI789。您可以轻松地使用正则表达式:

(?<=[0-9])(?=[A-Z])

而如果你使用([A-Z]+[0-9]+)(即捕获的组被放回结果列表/数组/等中,你也会得到空元素。

请注意,这也可以通过匹配来完成,使用[A-Z]+[0-9]+


如果我不得不提到负面的环视,这篇文章会更长:)

于 2013-10-28T07:43:51.617 回答
0

请记住,正则表达式引擎的正/负环视是相同的。环顾四周的目标是在“正则表达式”中的某处进行检查。

主要兴趣之一是在不使用捕获括号的情况下捕获某些内容(捕获整个模式),例如:

细绳:aaabbbccc

正则表达式:(?<=aaa)bbb(?=ccc)

(您获得整个模式的结果)

代替:aaa(bbb)ccc

(您使用捕获组获得结果。)

于 2013-09-30T23:34:23.340 回答