Golang:正则
#go #100daysofgolang #regex

介绍

在该系列的第26部分中,我们将涵盖Golang中使用正则表达式的基础知识。本文将涵盖基本操作,例如从字符串源或文件内容的正则表达式模式中匹配,查找,替换和子匹配。这将有每个概念的示例,并且类似的变体将遵循自探索语法的相同意识形态。

Golang的正则是

所以,让我们从什么是正则表达式开始。

正则表达式是使用计算逻辑的文本来源的搜索,模式匹配和操纵的基本构建块。

这不是正式的定义,而是用文字写成的,以理解我对正则表达式的理解。可能不是准确的,但是在我演奏和探索它之后对我来说很有意义(不是完全)。

因此,正则表达式使用一些模式匹配技术,使用基本逻辑运算符,例如串联,量词等。这些都与计算理论的研究有关了解正则表达式的工作。但是,如果您对此感到好奇并想进一步探索它不会伤害您。

一些资源来学习正则表达式的基础知识:

REGEXP软件包

我们将在Golang Standard库中使用regexp软件包,以获取一些快速,整洁的模式匹配和搜索的快速而重要的方法。它在源文本中提供了匹配,查找,替换和子匹配。

此软件包还支持两种类型的方法,这些方法适用于不同的用途和用例,用于字符串和字节的用例,这对于从缓冲区,文件等读取的读数可能很有用,并且也足够灵活地搜索简单的字符串。

匹配模式

正则表达式的基本方面之一是检查源字符串中是否存在特定模式。 regexp软件包提供了一些方法,例如MatchMatchString方法分别从图案字符串的字节和字符串上分别在字符上。

匹配的字符串

如果图案与给定的字符串匹配,则可以执行带有正则表达式或正则表达式的基本操作以比较和匹配。

在Golang中,regexp软件包提供了一些功能,可以简单地将表达式与字符串或文本匹配。易于理解的之一包括MtachStringMatch方法。

package main

import (
    "log"
    "regexp"
)

func log_error(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {

    str := "gophers of the goland"
    is_matching, err := regexp.MatchString("go", str)
    log_error(err)
    log.Println(is_matching)
    is_matching, err := regexp.MatchString("no", str)
    log_error(err)
    log.Println(is_matching)

}
$ go run main.go

true
false

在上面的代码中,我们使用了MatchString方法,该方法将吸引两个参数字符串/模式以及源字符串。 The function returns a boolean as true or false if the pattern is present in the source string, also it might return an error if the pattern(first parameter) is parsed as an incorrect regular expression.

因此,我们可以清楚地看到,字符串go存在于字符串gophers of the goland中,而字符串no不是子字符串。

我们还具有Match方法,它是MatchString的更通用版本,除了字节片而不是字符串作为源字符串。第一个参数仍然是字符串,但第二个参数是字节的片段。

is_matching, err = regexp.Match(`.*land`, []byte("goland is a land of gophers"))
log_error(err)
log.Println(is_matching)
$ go run main.go

true

我们可以使用Match方法简单地解析一个字节片以用作源文本以检查模式。

package main

import (
    "log"
    "os"
    "regexp"
)

func log_error(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    file_content, err := os.ReadFile("temp.txt")
    log_error(err)
    is_matching, err = regexp.Match(`memory`, file_content)
    log_error(err)
    log.Println(is_matching)
    is_matching, err = regexp.Match(`text `, file_content)
    log_error(err)
    log.Println(is_matching)
}
# temp.txt

One of the gophers used a slice,
the other one used a arrays.
Some gophers were idle in the memory.
$ go run main.go

true
false

现在,我们甚至可以将文件的内容作为字节切成薄片。因此,可以快速比较并检查文件中的字符串模式真的很不错。在上面的示例中,我们已经检查了文本中是否存在memory,在第二个通话中,我们检查了文件内容中是否存在text字符串。

查找模式

我们甚至可以使用正则表达式使用Golang Regexp提供的结构/类型Regexp搜索文本或字符串。我们可以创建正则表达式并使用其他功能,例如MatchStringMatch和其他我们将探索以匹配或在文本中找到模式的其他功能。

从Regex查找字符串

我们可以从FindAll方法中获取一片字节,该方法将采用一片字节,第二个参数为所有匹配项为-1。该函数返回一片字节片,并在源文本中使用匹配字符串的字节表示。

exp, err := regexp.Compile(`\b\d{5}(?:[-\s]\d{4})?\b`)
log_error(err)
pincode_file, err := os.ReadFile("pincode.txt")
log_error(err)
match := exp.FindAll(pincode_file, -1)
log.Println(match)
# pincode.txt

Pincode: 12345-1234
City, 40084
State 123
$ go run main.go

[[49 50 51 52 53 45 49 50 51 52] [52 48 48 56 52]]

在上面的示例中,我们使用了Compile方法来创建正则表达式和FindAll,以获取文本中匹配模式的所有出现。我们再次读取文件中的内容。在此示例中,exp是邮政编码的正则表达式,该表达式可以具有5位数字或5Digits-4二元组合。我们将文件pincode.txt读取为字节片,并使用FindAll方法。 Findall方法将参数作为字节片和整数作为要搜索的出现数量。如果我们使用负数,它将包括所有匹配。

我们在文件中搜索PIN代码,而Funciton返回了一个字节列表,该字节符合提供的对象exp中的正则表达式。最后,我们将结果作为12345-123440084的结果。它与给定的正则表达式无效匹配不匹配数字123

还有一个FindAll的版本为FindAllString,它将以文本源为字符串并返回一片字符串。

matches := exp.FindAllString(string(pincode_file), -1)
log.Println(matches)
$ go run main.go

["12345-1234" "40084"]

所以,FindAllString将返回文本中的一小部分匹配项。

从Regex找到字符串的索引

我们甚至可以使用FindIndexFindAllIndex方法获得文本中匹配的字符串的开始和结尾索引,以获取匹配项的索引,或文件内容的所有匹配项。

exp, err := regexp.Compile(`\b\d{5}(?:[-\s]\d{4})?\b`)
log_error(err)
pincode_file, err := os.ReadFile("pincode.txt")
log_error(err)

match_index := exp.FindIndex(pincode_file)
log.Printf("%T\n", match_index)
log.Println(match_index)
$ go run main.go

[]int
[9 19]

上面的代码使用FindIndex方法来获取正则表达式的第一匹匹配的索引。代码的输出给出了一个整数的单个切片,其中二的长度为二次和最后一个索引,作为文本文件中匹配的字符串的开始和最后一个索引。因此,在这里,9表示字符串中第一个字符匹配的位置(索引),而19是匹配的字符串的最后一个索引。

Pincode: 12345-1234
01234567890123456789

Assume 0 as 10 after 9, 11, and so on
It starts from the 9th character as `1` and ends at the `4` character
at position 18 but it returns the end position + 1 for the ease of slicing

City, 40084
012345678901

State 123
234567890

9和18时的字符是源字符串的第一个和最后一个字符的位置/索引,因此它返回端位置 + 1作为索引。这使得切片源字符串的检索变得更加容易,因为我们不会被一个。

如果我们想在源字符串中获取文本,我们可以使用切片为:

exp, err := regexp.Compile(`\b\d{5}(?:[-\s]\d{4})?\b`)
log_error(err)
pincode_file, err := os.ReadFile("pincode.txt")
log_error(err)

match_index := exp.FindIndex(pincode_file)
if len(match_index) > 0 {

    // Get the slice of the original string from start to end index
    sliced_string := pincode_file[match_index[0]:match_index[1]]
    log.Printf("%q\n", sliced_string)
}
$ go run main.go

"12345-1234"

因此,我们可以从源文本访问字符串而不调用其他功能,简单地切片原始字符串将检索预期的结果。这是Golang Standard库的约定,以使最终索引独家。

同样,FindAllIndex方法可用于获取匹配字符串的此类索引列表。

exp, err := regexp.Compile(`\b\d{5}(?:[-\s]\d{4})?\b`)
log_error(err)
pincode_file, err := os.ReadFile("pincode.txt")
log_error(err)

match_indexes := exp.FindAllIndex(pincode_file)
log.Printf("%T\n", match_indexes)
log.Println(match_indexes)
$ go run main.go

[][]int
[[9 19] [26 31]]

上面的示例获取切片列表,如源字符串/文本中的所有搜索模式索引。我们可以在列表上迭代并获取每个匹配的字符串索引。

查找subsatch

regexp软件包还具有用于查找给定正则表达式的子匹配的实用程序功能。该方法返回包含最大匹配项和该匹配项的子匹配的字符串(或字节切片)列表。这也具有All版本,该版本没有返回单个匹配项,即它返回所有匹配项和相应的子匹配。

package main

import (
    "log"
    "regexp"
)

func main() {

    str := "abc@def.com is the mail address of 8th user with id 12"
    exp := regexp.MustCompile(
        `([a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]{2,})` + 
            `|(email|mail)|` + 
            `(\d{1,3})`)`,
    )
    match := exp.FindStringSubmatch(str)
    log.Println(match)
    matches := exp.FindAllStringSubmatch(str, -1)
    log.Println(matches)
}
$ go run main.go

[abc@def.com abc@def.com  ]
[[abc@def.com abc@def.com  ] [mail  mail ] [8   8] [12   12]]

上面的示例使用正则示例来计算一些诸如mailemail之类的电子邮件地址,也有多达3位数字。这些表达式之间的|表明这三件事的任何组合都可以匹配正则表达式。 FindStringSubmatch方法将字符串作为源,并返回匹配模式的切片。第一个元素是给定正则表达式的字符串源中的最左匹配,然后后续元素是匹配的字符串中的子匹配。

现在,我们可以向前走一些一步,以实际理解正则表达式中的子匹配。

str := "abe21@def.com is the mail address of 8th user with id 124"
exp := regexp.MustCompile(
    `([a-zA-Z]+(\d*)[a-zA-Z]*@[a-zA-Z]*(\d*)[a-zA-Z]+\.[a-z|A-Z]{2,})` +
        `|(mail|email)` +
        `|(\d{1,3})`,
)

match := exp.FindStringSubmatch(str)
log.Println(match)
matches := exp.FindAllStringSubmatch(str, -1)
log.Println(matches)
$ go run main.go

[abe21@def.com abe21@def.com 21   ]
[[abe21@def.com abe21@def.com 21   ] [mail    mail ] [8     8] [124     124]]

在上面的示例中,有几件事可以带走,让我们将其分解成小块。我们有一个以匹配邮件地址,mailemail或最多3位数字的邮件。正则与上一个示例有所不同,以了解表达式中的子匹配。我们在字符串中找到将由FindStringSubmatch处理的子匹配。此功能接收一个字符串,并返回源字符串中最左侧匹配的字符串列表。

首先,我们需要理解正则表达式,以清楚地了解代码段。第一个子匹配是用于电子邮件地址。但是,我们在选择用户名和域名时使用[a-zA-Z],我们不想直接匹配数字。这条正Gex的目的是在电子邮件地址中获取数字。因此,我们可以拥有1个或更多字符的[a-zA-z]+,其次是0或更多数字(\d*),我们可以再次拥有0个或更多字符[a-zA-Z]*+适用于1或更多,*用于0或更多,\d用于数字。此后,我们将@作为邮件中的强制性字符,然后在用户名中使用相同的序列,即1个或更多字符,0或更多数字,0或更多字符。最后,我们有一个.com和域名扩展名,作为2个或更多字符的组[a-z|A-Z]{2,}

因此,正则接受电子邮件,并在用户名或域名中的任何位置的数字匹配。

FindStringSubmatch函数列出了源字符串中正则左右匹配的子匹配。因此,它找到了字符串abc21@def.com,即电子邮件ID。此Regex ([a-zA-Z]+(\d*)[a-zA-Z]*@[a-zA-Z]+(\d*)[a-zA-Z]*\.[a-z|A-Z]{2,})在用户名部分和域部分中具有两个子匹配\d*。因此,列表将电子邮件地址作为比赛和比赛本身以及邮件地址内的子匹配中的数字返回。因此,结果是[abc21@def.com abc21@def.com 21 ],有一些空字符串子匹配,因为域名的第二个子匹配返回一个空字符串。

同样,FindAllStringSubmatch将返回源字符串中所有匹配项的列表。其他匹配项中没有任何子匹配,因此在String mail,Digit 8和Digit 124的情况下,它只是获得匹配和子匹配。

我们还可以将此示例从文件中用作字节片。这将返回一片字节片的列表,而不是字符串切片。

package main

import (
    "log"
    "regexp"
)

func main() {

    exp := regexp.MustCompile(
        `([a-zA-Z]+(\d*)[a-zA-Z]*@[a-zA-Z]*(\d*)[a-zA-Z]+\.[a-z|A-Z]{2,})` +
            `|(mail|email)` +
            `|(\d{1,3})`,
    )
    email_file, err := os.ReadFile("subtext.txt")
    log_error(err)
    mail_match := exp.FindSubmatch(email_file)
    log.Printf("%s\n", mail_match)
    mail_matches := exp.FindAllSubmatch(email_file, -1)
    //log.Println(mail_matches)
    log.Printf("%s\n", mail_matches)

}
# submatch.txt

abc21@def.com is the mail address of user id 1234
The email address abe2def.com is of user name abc
a2be.@def.com
Email address: abe@de2f.com, User id: 45
johndoe@example.com
jane.doe123@example.com
janedoe@example.co.uk
john123@example.org
janedoe456@example.net
$ go run main.go

[][]unit8
[abc21@def.com abc21@def.com 21   ]

[][][]unit8
[
    [abc21@def.com abc21@def.com 21   ] [mail    mail ] [123 123] [4     4] 
    [email    email ] [2     2] [2     2] [mail    mail ] 
    [abe@de2f.com abe@de2f.com  2  ] [45     45] 
    [johndoe@example.com johndoe@example.com    ] 
    [doe123@example.com doe123@example.com 123   ] 
    [janedoe@example.co janedoe@example.co    ] 
    [john123@example.org john123@example.org 123   ] 
    [janedoe456@example.net janedoe456@example.net 456   ]
]

从虚拟电子邮件ID和一些随机文本中可以看到,我们可以将电子邮件ID和其中的数字匹配为字符串的子匹配。对于FindSubmatch[][][]unit8的情况下,这是[][]unit8。字节与字符串相同。

查找submatch索引

我们还具有FindSubmatchIndexFindAllSubmatchIndex,并且字符串变体以获取从正则表达式中选择的模式中的子匹配的索引。

str := "abe21@def.com is the mail address of 8th user with id 124"
exp := regexp.MustCompile(
    `([a-zA-Z]+(\d*)[a-zA-Z]*@[a-zA-Z]*(\d*)[a-zA-Z]+\.[a-z|A-Z]{2,})` +
        `|(mail|email)` +
        `|(\d{1,3})`,
)

match := exp.FindStringSubmatch(str)
match_index := exp.FindStringSubmatchIndex(str)
log.Println(match)
log.Println(match_index)
$ go run main.go

[abe21@def.com abe21@def.com 21   ]
[0 13 0 13 3 5 9 9 -1 -1 -1 -1]

因此,这返回了一对列表,此处将列表视为[(0, 13) (0, 13), (3, 5), (9, 9), (-1, -1), (-1, -1)]。因为该列表代表源字符串中子匹配的开始和结尾索引。比赛是第一对,即在第一个字符0-> a,在太空结束,即13字符以一个。然后,我们具有相同索引的子匹配本身。然后35指示abc21@def.com中的数字子匹配21,它从3开始,以5结束,因此在源字符串中占3和4个字符。同样,域级别的数字在源字符串中没有任何数字,因此它已将域名端索引返回为空字符串。

我们使用了可以返回数字出现0或更多出现的(\d*),因此在域级别名称的情况下,它没有返回匹配,因此我们将9作为一个空字符串在子匹配末端的空字符串作为空字符串。它。其余的是用于emailmail,而我们在源字符串中第一匹匹配中没有的正则表达式中的唯一数字子匹配。

str := "abe21@def.com is the mail address of 8th user with id 124"
exp := regexp.MustCompile(
    `([a-zA-Z]+(\d*)[a-zA-Z]*@[a-zA-Z]*(\d*)[a-zA-Z]+\.[a-z|A-Z]{2,})` +
        `|(mail|email)` +
        `|(\d{1,3})`,
)

match := exp.FindAllStringSubmatch(str, -1)
match_indexes := exp.FindAllStringSubmatchIndex(str, -1)
log.Println(match_indexes)
$ go run main.go

[[abe21@def.com abe21@def.com 21   ] [mail    mail ] [8     8] [124     124]]
[
    [0 13 0 13 3 5 9 9 -1 -1 -1 -1]
    [21 25 -1 -1 -1 -1 -1 -1 21 25 -1 -1]
    [37 38 -1 -1 -1 -1 -1 -1 -1 -1 37 38]
    [54 56 -1 -1 -1 -1 -1 -1 -1 -1 54 57]
]

同样,我们使用了FindAllStringSubmatchIndex来获取源字符串中表达式子匹配的索引(int)的列表。

第一个元素与上一个示例相同,源字符串中的下一个匹配项是mail,该匹配项在索引21处,并以第24个索引结束,戈兰(Golang)将24+1作为约定。同样,数字8在index 37和index 54处的数字124匹配,这些匹配的其余子匹配不存在,因此其余子匹配的其余部分。

因此,这也可以用于具有FindSubmatchIndexFindAllSubmatchIndex的字节/UINT8类型的变体。

更换图案

替换方法用于替换匹配的模式。

带有某些字符串和字节切片变化的ReplaceAllReplaceAllLiteral可以帮助我们用替换字符串替换源文本,以替换针对正则表达式。

让我们从一个简单的字符串示例开始。

package main

import (
    "log"
    "regexp"
)

func main() {

    str := "Gophy gophers go in the golang grounds"
    exp := regexp.MustCompile(`(Go|go)`)
    replaced_str := exp.ReplaceAllString(str, "to")
    log.Println(replaced_str)

}

上面的代码可以用替换字符串替换字符串。 exp变量中提供的正则是Gogo的模式匹配。因此,ReplaceAllString方法在替换字符后返回字符串的两个参数。

$ go run replace.go

Original:  Gophy gophers go in the golang grounds
Replaced:  tophy tophers to in the tolang grounds

因此,用to中的Gogo代替了所有字符。

替换字符串中有一个特殊值可用,可以扩展正则表达式文字。由于正则表达式由多个文字/量词组成,因此我们可以使用它们来替换或将字符串保持在特定表达式的评估中。

str = "Gophy gophers go in the golang grounds"
exp2 := regexp.MustCompile(`(Go|go)|(phers)|(rounds)`)
log.Println(exp2.ReplaceAllString(str, "hop"))
log.Println(exp2.ReplaceAllString(str, "$1"))
log.Println(exp2.ReplaceAllString(str, "$2"))
log.Println(exp2.ReplaceAllString(str, "$3"))
$ go run replace.go

hopphy hophop hop in the hoplang ghop
Gophy go go in the golang g
phy phers  in the lang g
phy   in the lang grounds

上述代码使用的正则表达式匹配字符串已被文字替换。因此,Regex (Go|go)|(phers)|(rounds)具有三个部分Go|gophersrounds。每个文字都由操作员分开,表明应该考虑匹配或所有匹配项。

在第一个语句中,我们用hop替换了正则表达式,因为您可以看到所有匹配都用替换字符串替换。例如,gophers一词被hophop替换,因为gophers分别匹配并替换。

在第二个语句中,我们将源字符串替换为$1,指示正则是正则是Go|go中的第一个文字。这些陈述将扩展$1,并仅在删除字面匹配和休息的地方保留比赛。因此,GophyGo|go匹配,因此将其按照替换中的方式保留。但是,对于grounds$1的字面匹配即Go|go不匹配,因此不保留和删除,因此生成的字符串变为g rets用空字符串代替。

在第三个打印语句中,源字符串被第二个字面的$2替换,即phers。因此,如果任何字符串与文字匹配,则只保留了这些字符串,其余的则由一个空字符串代替。因此,GophyGophers不匹配,因此被替换,但是,gophers匹配了phers零件并保持原样,但是go零件被替换。

同样,对于第四个打印语句,源字符串被第三个字面的即rounds替换。因此,如果仅保留第三个字面的文字,则将正则匹配的静止匹配的字符串用空字符串代替。因此,grounds保持原样,因为它与替换文字中的rounds匹配。

简而言之,我们替换了源字符串中的正则表达式后,我们更换了文字。这可以用于微调或访问正则表达式中的特定文字或表达式。

str = "Gophy gophers go in the golang grounds"
exp2 := regexp.MustCompile(`(Go|go)|(phers)|(rounds)`)
log.Println(exp2.ReplaceAllString(str, "$1$2"))

str = "Gophy gophers go in the golang cophers grounds"
log.Println(exp2.ReplaceAllString(str, "$1$3"))
$ go run replace.go

Gophy gophers go in the golang g
Gophy go go in the golang co grounds

我们甚至可以对替换字符串进行连接并进行一些小字符串的字面调整。正如我们在示例中所做的那样,两个$1$2都用作替换字符串。这两个文字结合在一起,用Go|gophers制作一根字符串。因此,我们可以在第一个语句(Gophy gophers go in the golang g)中看到结果,所有带有Go|gophers的字符都被保留为(替换为同一字符串),但是grounds,但是不匹配,因此将其替换为空字符串(因为捕获组的$1$2rounds匹配)。

同样,对于第三个示例,$1$3Go|gorounds与源字符串匹配。因此,我们在gophers中的phers,而cophers与捕获组的$1$3不匹配,因此被一个空的组(字符串)替换。但是,Gophygolanggrounds与捕获组匹配,并由该匹配字符串替换(这是同一字符串)。

如果我们想像在上一个使用$1和其他参数的示例中避免扩展字符串,我们可以使用ReplaceAllLiteralReplaceAllLiteralString来解析字符串。

str := "Gophy gophers go in the golang cophers grounds"
exp2 := regexp.MustCompile(`(Go|go)|(phers)|(rounds)`)
log.Println(exp2.ReplaceAllLiteralString(str, "$1"))
$ go run replace.go

$1phy $1$1 $1 in the $1lang co$1 g$1

我们可以看到,$1没有扩展和解析,因为它是为了替换正则表达式中的模式。 Go$1替换为看起来像$1phy,并且在其余模式中类似。

就是这一系列的第26部分,示例的所有源代码都链接在100 days of Golang存储库的GitHub中。

结论

本文介绍了使用regexp软件包在Golang中使用正则表达式的基本原理。我们探索了包装中的方法和Regexp类型,并使用类型接口可用的各种方法。通过探索示例和简单片段,进行了各种模式匹配,查找和更换的方式。

因此,希望这篇文章可能对您有用,如果您在文章中有任何疑问,问题,反馈或错误,您可以在讨论或社交手柄中告诉我。 Happy Coding :)系列:“ [''100天Golang']