【问题标题】:Conditional regex in RubyRuby 中的条件正则表达式
【发布时间】:2020-10-24 23:06:23
【问题描述】:

我有以下字符串:

'USD 100'

基于this post,如果USD 包含在字符串中如果USD 不包含在字符串中,我将尝试捕获100 .

例如:

'USD 100' # => '100'
'YEN 300' # => ['Y', 'E', 'N']

到目前为止,我已经解决了这个问题,但它不起作用:

https://rubular.com/r/cK8Hn2mzrheHXZ

有趣的是,如果我将USD 放在它似乎有效的数量之后。理想情况下,无论货币字符的位置如何,我都希望具有相同的行为。

【问题讨论】:

  • 模式\b(?:USD (\d+)|(?!USD\b)(\w+) \d+)\b 很接近,除了它捕获整个非美元货币符号,而不是捕获单独捕获组中的每个字母。要捕获每个字母,您可能需要一些 Ruby 技巧。
  • YEN 也可以解决问题。它不必是数组中的每个字母。不过我想知道;为什么前瞻不起作用?
  • 你是这个意思吗? (?:(USD) )?(?(1)(\d+)|([a-zA-Z]))rubular.com/r/quQSSLzsj0Om1q然后检查组2或组3
  • 我在my answer 中解释了这个问题。基本上,只有包含 USD 的字符串才会匹配,因为正向前瞻需要它。

标签: regex ruby


【解决方案1】:

你的模式剖析

(?=.*(USD))(?(1)\d+|[a-zA-Z])
|    |     | |  |   |_______
|    |     | |  |   Else match a single char a-zA-Z
|    |     | |  |   
|    |     | |  |__
|    |     | |  If group 1 exists, match 1+ digits
|    |     | |
|    |     | |__
|    |     | Test for group 1
|    |     |_________________
|    |     If Clause
|    |___
|    Capture group 1
|__________
Positive lookahead

关于你尝试的模式

正向前瞻未锚定,将在每个位置上进行尝试。如果返回true,它将继续匹配,否则匹配停止并且引擎将移动到下一个位置。

为什么模式不匹配?

在第一个位置,前瞻为真,因为它可以在右侧找到美元。 它尝试匹配 1+ 位,但第一个字符是 U,它无法匹配。

USD 100
⎸
First position

从第二个位置到最后,lookahead 为假,因为它在右边找不到美元。

USD 100
 ⎸
Second position   
  • 最终,if 子句只尝试了一次,它无法匹配 1+ 位数字。 else 子句从未尝试过,总体上没有匹配项。

  • 对于YEN 300 部分,从未尝试过 if 子句,因为前瞻永远不会在右侧找到 USD,并且总体上没有匹配项。

有关条件的有趣资源例如可以在rexegg.comregular-expressions.info 找到


如果你想要单独的匹配,你可以使用:

\bUSD \K\d+|[A-Z](?=[A-Z]* \d+\b)

说明

  • \bUSD 匹配美元和一个空格
  • \K\d+ 忘记使用 \K 匹配的内容并匹配 1+ 个数字
  • |或者
  • [A-Z] 匹配一个字符 A-Z
  • (?=[A-Z]* \d+\b) 断言右边是可选字符 A-Z 和 1+ 位

regex demo

或者使用捕获组:

\bUSD \K(\d+)|([A-Z])(?=[A-Z]* \d+\b)

Regex demo

【讨论】:

  • 看起来OP wants an explanation 的正则表达式(以及为什么它不起作用)不仅仅是一个解决方案。
  • 我可以稍后添加,刚离开我的机器。
【解决方案2】:

TLDR;

可以在Wiktor 的回答和其他帖子中找到出色的working solution

长答案:

由于我对Wiktormy solution 不起作用的解释并不完全满意,因此我决定自己深入研究一下,这是我的看法:

给定字符串USD 100,下面的正则表达式

(?=.*(USD))(?(1)\d+|[a-zA-Z])

根本行不通。整件事的精髓在于找出为什么。 事实证明,将前瞻 (?=.*(USD)) 与捕获组一起使用,隐含地表明 USD位置 (如果找到的话)后面跟着某种模式(在条件 ( (?(1)\d+|[a-zA-Z])) 在这种情况下不会产生任何结果,因为在 USD 之前什么都没有。

如果我们按步骤将其分解,以下是我认为正在发生的事情的概要:

  1. 指针设置在最开始。前瞻 (?=.*(USD)) 被解析并执行。
  2. USD 已找到,但由于表达式是前瞻的,因此指针保留在字符串的开头并且未被使用。
  3. 条件 ((?(1)\d+|[a-zA-Z])) 被解析并执行。
  4. 1 已设置(因为已找到USD然而 \d+ 失败,因为指针搜索从字符串的开头 到字符串的开头 原来是我们使用前瞻时可以搜索的最远点!毕竟这正是它被称为前瞻的原因:搜索必须在一个范围内发生,该范围在此搜索开始之前停止。
  5. 由于在USD 之前没有找到任何数字或任何内容,因此正则表达式不返回任何结果。正如Wiktor 正确指出的那样:

永远不会尝试第二种替代模式,因为您需要 USD 出现在字符串中才能进行匹配。

这基本上是说,由于USD 始终存在于字符串中,因此即使最终在USD 之前找到了某些内容,系统也永远不会跳转到“else”语句。

作为一个反例,如果在此字符串上测试相同的正则表达式,它将起作用:

'YEN USD 100'

希望这对将来的某人有所帮助。

【讨论】:

  • 您的“lookahead 隐式设置正则表达式“搜索范围”指针的“上限””解释是错误的。 “向前看”是 pleonasm,任何向前看都是“向前看”,因为向后看被称为向后看。前瞻在这里是积极的,并断言该位置遵循某种模式。在您的情况下,它是.*USDUSD 必须出现在除换行符之外的任何 0 个或多个字符之后,紧挨当前位置的右侧。当USD 在数字后面时,there is a match
  • 感谢@WiktorStribiżew。我按照你的建议更正了措辞。它现在应该是正确的。我知道我们本质上说的是完全相同的事情。
  • 我真的相信你的“指针从字符串的开头搜索到字符串的开头,结果是我们在使用前瞻时可以搜索的最远点”并且“搜索必须在一个范围内发生,该范围在此搜索开始之前停止”的解释具有误导性。前瞻不设置范围。它们是返回 truefalse 的布尔“函数”,仅此而已。您可能会说前瞻设置匹配的左侧/右侧上下文。 “范围”最好避免,因为它们与正则表达式中的不同内容有关。
【解决方案3】:

我建议提取所需的信息如下。

R = /\b([A-Z]{3}) +(\d+)\b/

def doit(str)
  str.scan(R).each_with_object({}) do |(cc,val),h|
    h[cc] = (cc == 'USD') ? val : cc.split('')
  end
end

doit 'USD 100'
  #=> {"USD"=>"100"} 
doit 'YEN 300'
  #=> {"YEN"=>["Y", "E", "N"]} 
doit 'I had USD 6000 to spend'
  #=> {"USD"=>"6000"} 
doit 'I had YEN 25779 to spend'
  #=> {"YEN"=>["Y", "E", "N"]} 
doit 'I had USD 60 and CDN 80 to spend'
  #=> {"USD"=>"60", "CDN"=>["C", "D", "N"]} 
doit 'USD -100'
  #=> {} 
doit 'YENS 4000'
  #=> {} 

Regex demo

Ruby 的正则表达式引擎执行以下操作。

\b          : assert a word boundary
([A-Z]{3})  : match 3 uppercase letters in capture group 1
\ +         : match 1+ spaces
(\d+)       : match 3 digits in capture group 2
\b          : assert a word boundary

【讨论】:

    【解决方案4】:

    您的正则表达式 (?=.*(USD))(?(1)\d+|[a-zA-Z]) 不起作用,因为

    • (?=.*(USD)) - 一个正向前瞻,在字符串内的每个位置触发(如果使用scan)匹配USD 子字符串后的任何0个或多个字符,而不是换行字符尽可能多(这意味着,会有只有在某处有USD 时才匹配)
    • (?(1)\d+|[a-zA-Z]) - 如果第 1 组匹配(如果有 USD),则匹配 1+ 位的条件构造,或者将尝试 ASCII 字母。 但是,第二个替代模式将永远尝试,因为您需要USD 出现在字符串中才能发生匹配。

    查看USD 100 regex debugger,它准确地显示了当(?=.*(USD))(?(1)\d+|[a-zA-Z]) 正则表达式试图找到匹配项时会发生什么:

    • 步骤 1 到 22:先尝试先行模式。这里的要点是,如果正向前瞻模式没有找到匹配项,则匹配将立即失败。在这种情况下,USD 位于字符串的开头(因为第一次尝试该模式,正则表达式索引位于字符串的起始位置)。前瞻找到了匹配项。
    • 步骤 23-25:由于前瞻是一种非消耗模式,因此正则表达式索引仍位于字符串起始位置。前瞻表示“继续”,然后输入条件构造。 (?(1) 条件满足,组 1 USD 已匹配。因此,第一个 then 部分被触发。 \d+ 找不到任何数字,因为开头有 U 字母。 正则表达式匹配在字符串起始位置失败,但字符串中有更多位置需要测试,因为没有 \A^ 锚点,只有在匹配时才会发生匹配位于字符串/行的开头。
    • 第 26 步:正则表达式引擎索引向右推进一个字符,现在,它位于字母 S 之前。
    • 步骤 27-40:正则表达式引擎想要找到 0+ 个字符,然后在当前位置右侧立即找到 USD,但失败了(U 已经在索引“后面”)。
    • 然后,执行与上述相同:正则表达式无法匹配当前位置右侧任何位置的USD,最终失败。

    如果USD 位于100 右侧的某个位置,那么您应该是get a match

    因此,lookahead 不会设置任何搜索范围,它只是允许匹配其余模式(如果其模式匹配)或不匹配(如果其模式未找到)

    你可以使用

    .scan(/^USD.*?\K(\d+)|([a-zA-Z])/).flatten.compact
    

    模式详情

    • ^USD.*?\K(\d+) - USD 在字符串的开头,然后尽可能少的除换行符之外的任何 0 个或多个字符,然后删除匹配的文本并将 1+ 个数字捕获到第 1 组
    • | - 或
    • ([a-zA-Z]) - 捕获到第 2 组的任何 ASCII 字母。

    Ruby demo:

    p "USD 100".scan(/^USD.*?\K(\d+)|([a-zA-Z])/).flatten.compact
    # => ["100"]
    p "YEN 100".scan(/^USD.*?\K(\d+)|([a-zA-Z])/).flatten.compact
    # => ["Y", "E", "N"]
    

    【讨论】:

    • 你已经等不及我更新它了 :-) 这种模式与锚点和非贪婪量词的原理几乎相同 ++
    • @Thefourthbird 抱歉,我认为代码会成为差异化因素。
    • 不不,我的意思是积极的,我的 Ruby 知识很少,这是一个完整的答案。
    • 很好的答案!新的正则表达式似乎有效!但是您能否解释一下为什么我的解决方案不起作用?我的字符串中确实有'USD'('USD 100'),因此条件应该评估'\d+'('100'),但它没有。另外,您提到的第二种替代模式是什么..?
    • @kstratis 请逐步查看your regex debugger。这是pattern demo。关键是积极的前瞻要求USD 在数字之后。条件构造是具有两个备选方案的组,您的(?(1)\d+|[a-zA-Z])\d+[a-zA-Z]
    【解决方案5】:

    以下模式似乎有效:

    \b(?:USD (\d+)|(?!USD\b)(\w+) \d+)\b
    

    这需要注意的是,它只有一个非美元货币符号的捕获组。正则表达式的一部分可能值得解释:

    (?!USD\b)(\w+)
    

    这使用负前瞻来断言货币符号不是美元。如果是这样,那么它会捕获该货币符号。

    【讨论】:

      猜你喜欢
      • 2011-01-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-06
      • 1970-01-01
      • 2016-09-19
      • 2012-10-17
      • 2011-10-30
      相关资源
      最近更新 更多