Lua中的贪婪/非贪婪模式匹配和可选后缀。

17
在Lua中,我正在尝试模式匹配和捕获:
+384 Critical Strike (Reforged from Parry Chance)

作为

(+384) (Critical Strike)

后缀(Reforged from %s)是可选的。

详细版

我正在尝试使用Lua中的模式(即strfind匹配字符串。

注意:在Lua中,他们不称之为正则表达式,而称之为模式,因为它们不是规则的

示例字符串:

+384 Critical Strike
+1128 Hit

这可以分为两个部分,我想要捕捉:

enter image description here

  • 数字,带有正负符号,此处为+384
  • 字符串,此处为Critical Strike

我可以使用一个非常简单的模式来捕获它们:

enter image description here

而且这个在lua中的模式是有效的:

local text = "+384 Critical Strike";
local pattern = "([%+%-]%d+) (.+)";
local _, _, value, stat = strfind(text, pattern);
  • value = +384
  • stat = 暴击

棘手的部分

现在我需要扩展那个正则表达式 模式,以包括一个可选的后缀:

+384 Critical Strike (Reforged from Parry Chance)

被分解为:

enter image description here

注意:我并不特别关心可选的后缀;也就是说,我没有捕获它的要求,尽管捕获它会很方便。

这就是我开始遇到贪婪捕获问题的地方。立即,我已经有的模式做了我不想要的事情:

  • pattern = ([%+%-]%d+) (.+)
  • value = +384
  • stat = 致命一击(从招架几率改造而来)

但让我们尝试在模式中包含后缀:

enter image description here

使用模式:
pattern = "([%+%-]%d+) (.+)( %(Reforged from .+%))?"

我正在使用?运算符表示后缀出现的01次,但是这匹配没有任何内容

盲目地尝试将可选的后缀组从圆括号(更改为方括号[

pattern = "([%+%-]%d+) (.+)[ %(Reforged from .+%)]?"

但现在匹配又变得贪心了:

  • value = +384
  • stat = 致命一击(从招架几率改造)

基于Lua pattern 参考文档

  • x: (其中x不是魔法字符^$()%.[]*+-?)表示字符x本身。
  • .:(点)表示所有字符。
  • %a:表示所有字母。
  • %c:表示所有控制字符。
  • %d:表示所有数字。
  • %l:表示所有小写字母。
  • %p:表示所有标点符号。
  • %s:表示所有空格字符。
  • %u:表示所有大写字母。
  • %w:表示所有字母数字字符。
  • %x:表示所有十六进制数字。
  • %z:表示表示为0的字符。
  • %x:(其中x是任何非字母数字字符)表示字符x。这是转义魔法字符的标准方法。在模式中,任何标点符号字符(即使是非魔法字符)都可以在前面加上“%”以表示自己。
  • [set]:表示集合,它是集合中所有字符的并集。可以通过用“ - ”分隔范围的结束字符来指定字符范围。所有上述% x类也可以用作集合中的组件。集合中的所有其他字符都表示它们自己。例如,[%w_](或[_%w])表示所有字母数字字符加下划线,[0-7]表示八进制数字,[0-7%l%-]表示八进制数字加小写字母加“ - ”字符。 范围和类之间的交互未定义。因此,像[%a-z]或[a-%%]这样的模式没有意义。
  • [^set]:表示集合的补集,其中集合的解释如上所述。
  • 对于由单个字母(%a,%c等)表示的所有类别,相应的大写字母表示类别的补集。例如,%S表示所有非空格字符。

    字母,空格和其他字符组的定义取决于当前语言环境。特别是,类别[a-z]可能不等同于%l。

    还有神奇的匹配器:

    • *,它匹配类中0个或多个字符的重复。这些重复项将始终匹配最长可能的序列;
    • +,它匹配类中1个或多个字符的重复。这些重复项将始终匹配最长可能的序列;
    • -,它也匹配类中0个或多个字符的重复。与“*”不同,这些重复项将始终匹配最短可能的序列;
    • ?,它匹配类中一个字符的出现次数为0或1;

    我注意到有一个贪婪的*和一个非贪婪的-修改器。由于我的中间字符串匹配器:

    (%d) (%s) (%s)
    

    似乎一直吸收文本直到结尾,也许我应该尝试使它非贪婪,通过将*更改为-
    oldPattern = "([%+%-]%d+) (.*)[ %(Reforged from .+%)]?"
    newPattern = "([%+%-]%d+) (.-)[ %(Reforged from .+%)]?"
    

    现在它无法匹配:

    • 值 = +384
    • 状态 = nil

    与其捕获中间组的"任何"字符(即.),我尝试了一个包含除了(之外的所有内容的集合:

    pattern = "([%+%-]%d+) ([^%(]*)( %(Reforged from .+%))?"
    

    从那时起,局势失控了:

    local pattern = "([%+%-]%d+) ([^%(]*)( %(Reforged from .+%))?"
    local pattern = "([%+%-]%d+) ((^%()*)( %(Reforged from .+%))?"
    local pattern = "([%+%-]%d+) (%a )+)[ %(Reforged from .+%)]?"
    

    我原以为我很亲近:

    local pattern = "([%+%-]%d+) ([%a ]+)[ %(Reforged from .+%)]?"
    

    捕获哪些内容

    - value = "+385"
    - stat = "Critical Strike "  (notice the trailing space)
    

    所以这就是我在枕头上撞头入睡的地方;我简直不敢相信我已经花了四个小时来处理这个正则表达式...模式


    @NicolBolas 所有可能的字符串集合,使用伪正则表达式语言定义如下:

    +%d %s (Reforged from %s)
    

    其中

    • +表示正号 (+)"负号" (-)
    • %d表示任何拉丁数字字符(例如0..9
    • %s表示任何拉丁大写或小写字母或嵌入的空格(例如A-Za-z
    • 剩余的字符是文字。

    如果我必须写一个显然试图做我想要的正则表达式:

    \+\-\d+ [\w\s]+( \(Reforged from [\w\s]+\))?
    

    但是如果我没有解释得足够清楚,我可以给你一个几乎完整的列表,列出我在野外可能遇到的所有值。

    • +123 格挡 正数,单词
    • +123 致命一击 正数,两个单词
    • -123 格挡 负数,单词
    • -123 致命一击 负数,两个单词
    • +123 格挡(从躲闪改造) 正数,单词,可选后缀与单词一起出现
    • +123 致命一击(从躲闪改造) 正数,两个单词,可选后缀与两个单词一起出现
    • -123 格挡(从命中率改造) 负数,单词,可选后缀与两个单词一起出现
    • -123 致命一击(从命中率改造) 负数,两个单词,可选后缀与两个单词一起出现

    有一些奖励的模式,似乎很明显这些模式也会匹配:

    • +1234 致命一击几率 四位数,三个单词
    • +12345 坐骑和奔跑速度提高 五位数,五个单词
    • +123456 坐骑和奔跑速度提高 六位数,五个单词
    • -1 坐骑和奔跑速度提高 一位数,五个单词
    • -1 命中(从致命一击转化) 负一位数,一个单词,可选后缀带有3个单词

    而理想的模式应该匹配上述奖励条目,但不一定要匹配。

    本地化

    实际上,我试图解析的所有“数字”都将被本地化,例如:

    • +123,456 在英语(en-US)中
    • +123.456 在德语(de-DE)中
    • +123'456 在法语(fr-CA)中
    • +123 456 在爱沙尼亚语(et-EE)中
    • +1,23,456 在阿萨姆语(as-IN)中

    任何答案都不应该尝试解决这些本地化问题。你不知道一个数字将从何处呈现,这就是为什么数字本地化已被从问题中删除的原因。你必须严格假设数字包含加号、减号和拉丁数字0到9。我已经知道如何解析本地化的数字。这个问题是关于尝试匹配可选后缀与贪婪模式解析器的。

    编辑:你真的不需要尝试处理本地化的数字。在不知道区域设置的情况下,尝试处理它们在某种程度上是错误的。例如,我没有包括所有可能的数字本地化。另外:我不知道未来可能存在的本地化情况。


    很难给出答案,因为您似乎没有描述所有可能的字符串。 - Nicol Bolas
    函数strfind来自于旧版本的Lua 4.0。你真的在使用那个老版本吗? - prapin
    从文本内容来看,这是魔兽世界中的Lua,使用的是Lua 5.1版本。strfind很可能只是strfind = string.find - Mud
    4个回答

    8

    嗯,我没有安装Lua4,但是这个模式在Lua5下有效。我认为它对Lua4也有效。

    更新1:由于指定了额外的要求(本地化),我已经调整了模式和测试以反映这些要求。

    更新2:根据@IanBoyd在评论中提到的另一类包含数字的文本,更新了模式和测试。添加了字符串模式的说明。

    更新3:针对最新问题中提到的单独处理本地化数字的情况,添加了变体。

    尝试:

    "(([%+%-][',%.%d%s]-[%d]+)%s*([%a]+[^%(^%)]+[%a]+)%s*(%(?[%a%s]*%)?))"
    

    或者(不尝试验证数字本地化标记) - 只需采取任何不是字母的东西,以模式末尾的数字标记为准:
    "(([%+%-][^%a]-[%d]+)%s*([%a]+[^%(^%)]+[%a]+)%s*(%(?[%a%s]*%)?))"
    

    以上两种模式均不适用于处理科学计数法中的数字(例如:1.23e+10)。

    Lua5测试(编辑以清理 - 测试变得混乱):

    function test(tab, pattern)
       for i,v in ipairs(tab) do
         local f1, f2, f3, f4 = v:match(pattern)
         print(string.format("Test{%d} - Whole:{%s}\nFirst:{%s}\nSecond:{%s}\nThird:{%s}\n",i, f1, f2, f3, f4))
       end
     end
    
     local pattern = "(([%+%-][',%.%d%s]-[%d]+)%s*([%a]+[^%(^%)]+[%a]+)%s*(%(?[%a%s]*%)?))"
     local testing = {"+123 Parry",
       "+123 Critical Strike",
       "-123 Parry",
       "-123 Critical Strike",
       "+123 Parry (Reforged from Dodge)",
       "+123 Critical Strike (Reforged from Dodge)",
       "-123 Parry (Reforged from Hit Chance)",
       "-123 Critical Strike (Reforged from Hit Chance)",
       "+122384    Critical    Strike      (Reforged from parry chance)",
       "+384 Critical Strike ",
       "+384Critical Strike (Reforged from parry chance)",
       "+1234 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+12345 Mount and run speed increase (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+123456 Mount and run speed increase (Reforged from CrItIcAl StRiKe ChAnCe)",
       "-1 MoUnT aNd RuN sPeEd InCrEaSe (Reforged from CrItIcAl StRiKe ChAnCe)",
       "-1 HiT (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+123,456 +1234 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+123.456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+123'456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+123 456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+1,23,456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
       "+9 mana every 5 sec",
       "-9 mana every 20 min (Does not occurr in data but gets captured if there)"}
     test(testing, pattern)
    

    以下是该模式的详细说明:
    local explainPattern =  
       "(" -- start whole string capture
       ..
       --[[
       capture localized number with sign - 
       take at first as few digits and separators as you can 
       ensuring the capture ends with at least 1 digit
       (the last digit is our sentinel enforcing the boundary)]]
       "([%+%-][',%.%d%s]-[%d]+)" 
       ..
       --[[
       gobble as much space as you can]]
       "%s*"
       ..
       --[[
       capture start with letters, followed by anything which is not a bracket 
       ending with at least 1 letter]]
       "([%a]+[^%(^%)]+[%a]+)"
       ..
       --[[
       gobble as much space as you can]]
       "%s*"
       ..
       --[[
       capture an optional bracket
       followed by 0 or more letters and spaces
       ending with an optional bracket]]
       "(%(?[%a%s]*%)?)"
       .. 
       ")" -- end whole string capture
    

    对于“+384暴击”,您的模式捕获了“+384”、“Critical Strik”和“e”。 - Mud
    第二个捕获包括尾随空格(当后缀存在时,即在(之前的空格)。我认为这是无法避免的。 - Mud
    实际上是有的 - 这是一个棘手的问题,但已经更新了示例。 - Anthill
    我使用了您以前的模式,因为我有一个库可以匹配当前语言环境下的数字。为了满足所有单元测试,我确实需要对答案进行一些调整。我忘记了一类文本需要解析,即 +9 Mana every 5 sec.。在这种情况下,我想捕获 +9Mana every 5 sec。这类输入文本永远不会(在实践中)后跟可选后缀。我将把它留给您来处理这种情况的调整。 - Ian Boyd
    好的工作;我永远不会想到它。也因为我现在不知道它是如何工作的! - Ian Boyd
    不用谢 - 感谢您的反馈。我已经更新了我的答案,加入了您提到的额外用例,并解释了这个模式。 - Anthill

    1
    为什么要用一个模式来解析,而不是使用多个模式呢?
    首先,获取数字:
    local num, rest = string.match(test_string, "([%+%-]?%d+)%S*(.+)")
    

    然后制作一张表格,列举出命中类型的可能性。

    local hitTypes =
    {
      "Hit",
      "Critical Strike",
      -- Insert more.
    }
    

    现在,迭代列表,对每个进行测试。
    local hitIndex = nil
    local reforge = nil
    
    for i, htype in ipairs(hitTypes) do
      local final = string.match(rest, htype .. "%S*(.*)")
      if(final) then
        hitIndex = i
        reforge = string.match(final, "%(Reforged from (.+)%)")
      end
    end
    

    Lua模式有限,因此最好使用实际代码来避免它们的限制。

    为什么要用一个模式来解析这个,而不是使用多个模式呢?这看起来像是在《魔兽世界》中解析战斗记录。你可能会在每一帧里做很多这样的事情,而你不想降低帧率。如果有可能,最好只对字符串进行一次处理,而不是 N 次处理。 - Mud
    @Mud:然后每帧只处理其中的5个;将剩下的存入缓冲区,并在每帧从缓冲区中取出。 - Nicol Bolas
    缓冲区会随着你越来越落后而无限增长。这其实不重要;我相信你的方法已经足够快了。我只是回答了你提出的问题:你采用一次性通过数据而不是多次传递,因为这样更快(可能)。无论如何,Anthill发布了一个模式,可以在一次操作中捕获OP想要的内容。 - Mud

    1

    这个方法更简单,不仅可以匹配模式,还可以直接查找所需字符串的短输出,你可以使用string.gsub函数

    例如:

    local testing = {"+123 Parry",
    "+123 Critical Strike",
    "-123 Parry",
    "-123 Critical Strike",
    "+123 Parry (Reforged from Dodge)",
    "+123 Critical Strike (Reforged from Dodge)",
    "-123 Parry (Reforged from Hit Chance)",
    "-123 Critical Strike (Reforged from Hit Chance)",
    "+122384    Critical    Strike      (Reforged from parry chance)",
    "+384 Critical Strike ",
    "+384Critical Strike (Reforged from parry chance)",
    "+1234 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+12345 Mount and run speed increase (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+123456 Mount and run speed increase (Reforged from CrItIcAl StRiKe ChAnCe)",
    "-1 MoUnT aNd RuN sPeEd InCrEaSe (Reforged from CrItIcAl StRiKe ChAnCe)",
    "-1 HiT (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+123,456 +1234 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+123.456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+123'456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+123 456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+1,23,456 Critical Strike Chance (Reforged from CrItIcAl StRiKe ChAnCe)",
    "+9 mana every 5 sec",
    "-9 mana every 20 min (Does not occurr in data but gets captured if there)"}
    
    for k,v in ipairs(testing) do
      local result = string.gsub(v, "([%+%-][',%.%d%s]-[%+%d]+)%s*([%a]+[^%(^%)]+[%a]+)%s*(%(?[%a%s]*%)?)", '(%1) (%2) %3')
      print(result)
    end
    

    输出

    (+123) (Parry) 
    (+123) (Critical Strike) 
    (-123) (Parry) 
    (-123) (Critical Strike) 
    (+123) (Parry) (Reforged from Dodge)
    (+123) (Critical Strike) (Reforged from Dodge)
    (-123) (Parry) (Reforged from Hit Chance)
    (-123) (Critical Strike) (Reforged from Hit Chance)
    (+122384) (Critical    Strike) (Reforged from parry chance)
    (+384) (Critical Strike) 
    (+384) (Critical Strike) (Reforged from parry chance)
    (+1234) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+12345) (Mount and run speed increase) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+123456) (Mount and run speed increase) (Reforged from CrItIcAl StRiKe ChAnCe)
    (-1) (MoUnT aNd RuN sPeEd InCrEaSe) (Reforged from CrItIcAl StRiKe ChAnCe)
    (-1) (HiT) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+123,456 +1234) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+123.456) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+123'456) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+123 456) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+1,23,456) (Critical Strike Chance) (Reforged from CrItIcAl StRiKe ChAnCe)
    (+9) (mana every 5 sec) 
    (-9) (mana every 20 min) (Does not occurr in data but gets captured if there)
    

    0
    function match_some_stat_thing(str)
        local sign, amount, label, note = string.match(str.."()", "^([%+%-])(%d+) ([%a ]-) ?(%b())")
        return sign == "+" and amount or -amount, label, string.match(note, "%((.*)%)")
    end
    print(string.format("%d %q %q", match_some_stat_thing("+384 Critical Strike (Reforged from Parry Chance)")))
    print(string.format("%d %q %q", match_some_stat_thing("+384 Critical Strike")))
    print(string.format("%d %q %q", match_some_stat_thing("+384 Critical Strike ")))
    

    不是单一的模式,但它有效。


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