如何在Go正则表达式中实现捕获组功能

121

我正在将一个Ruby库移植到Go,并且刚刚发现Ruby中的正则表达式与Go(google RE2)不兼容。我已经注意到Ruby和Java(以及其他语言)使用PCRE正则表达式(与Perl兼容,支持捕获组),因此我需要重写我的表达式以便它们可以在Go中编译通过。

例如,我有以下正则表达式:

`(?<Year>\d{4})-(?<Month>\d{2})-(?<Day>\d{2})`

这应该接受以下输入:

2001-01-20

捕获组允许将年、月和日捕获到变量中。要获取每个组的值非常容易;只需使用组名作为索引访问匹配的数据,即可获取相应的值。例如要获取年份,可以使用类似下面的伪代码:

m=expression.Match("2001-01-20")
year = m["Year"]

这是我在表达式中经常使用的一种模式,因此我需要大量重新编写。

那么,在 Go 正则表达式中是否有获取此类功能的方法?我应该如何重写这些表达式?

8个回答

138

我应该如何重写这些表达式?

根据这里的定义,添加一些P:

(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})

使用re.SubexpNames()交叉引用捕获组名称。

并使用以下方式:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    r := regexp.MustCompile(`(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`)
    fmt.Printf("%#v\n", r.FindStringSubmatch(`2015-05-27`))
    fmt.Printf("%#v\n", r.SubexpNames())
}

8
好的,看起来很不错,但我如何访问年、月和日这些个体数值呢? - Plastikfan
1
忘掉上一个评论,我刚刚找到了答案。就像你说的那样,一切都在于 ?P :) - Plastikfan
@KevinBurke 请参考Regexp.SubexpNames中的示例。 - thwd
1
@thwd 现在,这引出了一个问题:如果您以相同的方式命名了两个组,会发生什么?这不是一个明确定义的行为,但正则表达式编译器不会对此抱怨。例如,您的代码会丢弃第一个匹配项,但我可以想象出一些情况下保留所有匹配项或者只收集其中的所有匹配项可能是有意义的... 设计语言有很多微妙之处... - wvxvw
4
@VladimirBauer,我不确定你的意思。我知道这不是特定于Go语言的问题,我认为在Go语言中,内置库对该特性的实现很差,因为它复制了该库的另一个更简单的特性,但增加了无意义的语法元素。 - wvxvw
显示剩余3条评论

42

我已经创建了一个用于处理URL表达式的函数,但它也适用于您的需求。您可以检查片段,但它的工作原理很简单:

/**
 * Parses url with the given regular expression and returns the 
 * group values defined in the expression.
 *
 */
func getParams(regEx, url string) (paramsMap map[string]string) {

    var compRegEx = regexp.MustCompile(regEx)
    match := compRegEx.FindStringSubmatch(url)

    paramsMap = make(map[string]string)
    for i, name := range compRegEx.SubexpNames() {
        if i > 0 && i <= len(match) {
            paramsMap[name] = match[i]
        }
    }
    return paramsMap
}
您可以像这样使用此函数:

params := getParams(`(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`, `2015-05-27`)
fmt.Println(params)

输出结果为:

map[Year:2015 Month:05 Day:27]

33

自Go 1.15版本起,您可以使用Regexp.SubexpIndex来简化此过程。您可以在https://golang.org/doc/go1.15#regexp查看发布说明。

根据您的示例,您将拥有以下内容:

re := regexp.MustCompile(`(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`)
matches := re.FindStringSubmatch("Some random date: 2001-01-20")
yearIndex := re.SubexpIndex("Year")
fmt.Println(matches[yearIndex])

你可以在https://play.golang.org/p/ImJ7i_ZQ3Hu查看并执行此示例。


28

为了在不在循环内调用匿名函数和不在循环内使用“append”函数复制数组的情况下改善RAM和CPU使用情况,请参考以下示例:

你可以使用多行文本存储一个以上的子组,无需使用“+”连接字符串,也无需在循环内使用另一个循环(如此处发布的其他示例)。

txt := `2001-01-20
2009-03-22
2018-02-25
2018-06-07`

regex := *regexp.MustCompile(`(?s)(\d{4})-(\d{2})-(\d{2})`)
res := regex.FindAllStringSubmatch(txt, -1)
for i := range res {
    //like Java: match.group(1), match.group(2), etc
    fmt.Printf("year: %s, month: %s, day: %s\n", res[i][1], res[i][2], res[i][3])
}

输出:

year: 2001, month: 01, day: 20
year: 2009, month: 03, day: 22
year: 2018, month: 02, day: 25
year: 2018, month: 06, day: 07

注意:res[i][0] =~ match.group(0) Java

如果您想存储这些信息,请使用结构体类型:

type date struct {
  y,m,d int
}
...
func main() {
   ...
   dates := make([]date, 0, len(res))
   for ... {
      dates[index] = date{y: res[index][1], m: res[index][2], d: res[index][3]}
   }
}

使用匿名组会更好(性能优化)

在Github上使用"ReplaceAllGroupFunc"不是一个好主意,因为:

  1. 它在循环内部使用了循环
  2. 它在循环内部调用了匿名函数
  3. 它的代码量很大
  4. 它在循环内部使用了"append"函数,这是不好的。每次调用"append"函数时,都会将数组复制到新的内存位置

是的,如果考虑浪费的时钟周期、浪费的 RAM 等因素,有更好和更差的解决方案。谦虚一点,你会让一个农民发布生产代码。 - VasileM

8

根据@VasileM的回答,确定组名的简单方法。

免责声明:这不涉及内存/CPU/时间优化。

package main

import (
    "fmt"
    "regexp"
)

func main() {
    r := regexp.MustCompile(`^(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})$`)

    res := r.FindStringSubmatch(`2015-05-27`)
    names := r.SubexpNames()
    for i, _ := range res {
        if i != 0 {
            fmt.Println(names[i], res[i])
        }
    }
}

https://play.golang.org/p/Y9cIVhMa2pU


2
如果你需要在捕获组的基础上进行函数替换,可以使用以下方法:
import "regexp"

func ReplaceAllGroupFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
    result := ""
    lastIndex := 0

    for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
        groups := []string{}
        for i := 0; i < len(v); i += 2 {
            groups = append(groups, str[v[i]:v[i+1]])
        }

        result += str[lastIndex:v[0]] + repl(groups)
        lastIndex = v[1]
    }

    return result + str[lastIndex:]
}

例子:

str := "abc foo:bar def baz:qux ghi"
re := regexp.MustCompile("([a-z]+):([a-z]+)")
result := ReplaceAllGroupFunc(re, str, func(groups []string) string {
    return groups[1] + "." + groups[2]
})
fmt.Printf("'%s'\n", result)

https://gist.github.com/elliotchance/d419395aa776d632d897


1
您可以使用regroup库来实现这一点 https://github.com/oriser/regroup
示例:
package main

import (
    "fmt"
    "github.com/oriser/regroup"
)

func main() {
    r := regroup.MustCompile(`(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`)
    mathces, err := r.Groups("2015-05-27")
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", mathces)
}

将会打印出: map[Year:2015 Month:05 Day:27]

或者,您可以这样使用:

package main

import (
    "fmt"
    "github.com/oriser/regroup"
)

type Date struct {
    Year   int `regroup:"Year"`
    Month  int `regroup:"Month"`
    Day    int `regroup:"Day"`
}

func main() {
    date := &Date{}
    r := regroup.MustCompile(`(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`)
    if err := r.MatchToTarget("2015-05-27", date); err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", date)
}

将打印:&{年:2015 月:5 日:27}


-2

获取正则表达式参数的函数,带有空指针检查。如果出现错误,则返回map[]。

// GetRxParams - Get all regexp params from string with provided regular expression
func GetRxParams(rx *regexp.Regexp, str string) (pm map[string]string) {
    if !rx.MatchString(str) {
        return nil
    }
    p := rx.FindStringSubmatch(str)
    n := rx.SubexpNames()
    pm = map[string]string{}
    for i := range n {
        if i == 0 {
            continue
        }

        if n[i] != "" && p[i] != "" {
            pm[n[i]] = p[i]
        }
    }
    return
}

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