【问题标题】:Repeated Capturing Matching Groups (Submatches)重复捕获匹配组(子匹配)
【发布时间】:2021-06-21 12:33:27
【问题描述】:

为了一个有趣的练习,我想知道是否可以使用正则表达式标记简单的算术表达式(仅包含正整数和四个基本运算),所以我想出了以下内容:

但由于最后列出的故障 (Go Playground),下面的测试用例的行为与我预期的不同:

func TestParseCalcExpression(t *testing.T) {
    re := regexp.MustCompile(`^(\d+)(?:([*/+-])(\d+))*$`)

    for _, eg := range []struct {
        input    string
        expected [][]string
    }{
        {"1", [][]string{{"1", "1", "", ""}}},
        {"1+1", [][]string{{"1+1", "1", "+", "1"}}},
        {"22/7", [][]string{{"22/7", "22", "/", "7"}}},
        {"1+2+3", [][]string{{"1+2+3", "1", "+", "2", "+", "3"}}},
        {"2*3+5/6", [][]string{{"2*3+5/6", "2", "*", "3", "+", "5", "/", "6"}}},
    } {
        actual := re.FindAllStringSubmatch(eg.input, -1)
        if !reflect.DeepEqual(actual, eg.expected) {
            t.Errorf("expected parse(%q)=%#v, got %#v", eg.input, eg.expected, actual)
        }
    }
}

// === RUN   TestParseCalcExpression
//      prog.go:24: expected parse("1+2+3")=[][]string{[]string{"1+2+3", "1", "+", "2", "+", "3"}}, got [][]string{[]string{"1+2+3", "1", "+", "3"}}
//      prog.go:24: expected parse("2*3+5/6")=[][]string{[]string{"2*3+5/6", "2", "*", "3", "+", "5", "/", "6"}}, got [][]string{[]string{"2*3+5/6", "2", "/", "6"}}
// --- FAIL: TestParseCalcExpression (0.00s)
// FAIL

我希望识别和分组运算符和数字 (([*/+-])(\d+)) 的非匹配子组 ((?:...)*) 的“零次或多次重复”将匹配 所有次出现子表达式,但它似乎只匹配最后一个。

一方面,这是有道理的,因为正则表达式实际上只有三个匹配组,因此任何结果匹配只能有三个匹配。然而,“零次或多次重复”使它看起来好像遗漏了失败测试中的所有“中间”重复项(例如 1+2+3 中的 +2)。

// expected parse("1+2+3")=
//     [][]string{[]string{"1+2+3", "1", "+", "2", "+", "3"}},
// got [][]string{[]string{"1+2+3", "1", "+", "3"}}

有没有办法使用 go 正则表达式解析这些类型的算术表达式,或者这是正则表达式(或 go/re2 正则表达式,或非/捕获组的一般组合)的基本限制?

(我意识到我可以按单词边界拆分并扫描标记以验证结构,但我对非/捕获组的这种限制比示例问题更感兴趣。)

【问题讨论】:

标签: regex go


【解决方案1】:
package main

import (
    "reflect"
    "regexp"
    "testing"
)

func TestParseCalcExpression(t *testing.T) {
    re := regexp.MustCompile(`(\d+)([*/+-]?)`)

    for _, eg := range []struct {
        input    string
        expected [][]string
    }{
        {"1", [][]string{{"1", "1", ""}}},
        {"1+1", [][]string{{"1+", "1", "+"}, {"1", "1", ""}}},
        {"22/7", [][]string{{"22/", "22", "/"}, {"7", "7", ""}}},
        {"1+2+3", [][]string{{"1+", "1", "+"}, {"2+", "2", "+"}, {"3", "3", ""}}},
        {"2*3+5/6", [][]string{{"2*", "2", "*"}, {"3+", "3", "+"}, {"5/", "5", "/"}, {"6", "6", ""}}},
    } {
        actual := re.FindAllStringSubmatch(eg.input, -1)
        if !reflect.DeepEqual(actual, eg.expected) {
            t.Errorf("expected parse(%q)=%#v, got %#v", eg.input, eg.expected, actual)
        }
    }
}

Playground link

正如this question 关于 Swift 中提到的(我不是 Swift 或正则表达式专家,所以我只是猜测这也适用于 Go),您只能为正则表达式中的每个匹配组返回一个匹配项。如果该组重复,它似乎只是识别最后一个匹配项。

来自 Go 标准库 regexp 包文档:

如果存在“子匹配”,则返回值是标识表达式的连续子匹配的切片。子匹配是正则表达式中带括号的子表达式(也称为捕获组)的匹配项,按照左括号的顺序从左到右编号。 子匹配 0 是整个表达式的匹配,子匹配 1 是第一个带括号的子表达式的匹配,以此类推

鉴于此约定,每个匹配组返回多个匹配项会破坏编号,因此您不会知道哪些项目与每个匹配组相关联。似乎正则表达式引擎可以为每个组返回多个匹配项,但是如果不违反文档中规定的这个约定,这个包就无法做到这一点。

我的解决方案是让您的问题更正常。我们没有将整个表达式视为一个匹配项,这给我们带来了每次匹配项只能返回有限多个字符串的问题,我们将整个表达式视为简单的一系列对。

每一对都由一个数字(\d+)和一个可选的运算符([*/+-]?)组成。

然后对整个表达式执行FindAllStringSubmatch,我们提取一系列这些对并获取每个对的数字和运算符。

例如: "1+2+3" 返回 [][]string{{"1+", "1", "+"}, {"2+", "2", "+"}, {"3", "3", ""}}}

这只是对表达式进行标记;它不验证它。如果您需要验证表达式,则需要另一个初始正则表达式匹配来验证字符串确实是这些对的完整序列。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-17
    • 1970-01-01
    • 1970-01-01
    • 2015-07-16
    相关资源
    最近更新 更多