Go 正規表示式學習

琴水玉發表於2024-04-06

正則是用於處理文字的利器之一。

關於正則的基礎知識及應用,之前寫過幾篇文章,讀者可以閱讀文後的相關資料作一基本瞭解。本文主要學習 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(多行匹配)**

在多行模式下,正規表示式中的 ^$ 除了匹配字串的開始和結束外,還可以匹配每一行的開始和結束。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(單行匹配)

在單行模式下,. 元字元可以匹配包括換行符在內的所有字元,而在普通模式下,. 不匹配換行符。Go 不支援單行模式匹配。

func TestSingleLineMatch3(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
}  

Backtracking(回溯匹配)

在處理複雜的正規表示式時,引擎可能採用回溯演算法,嘗試不同的路徑來找到匹配。當正規表示式包含分支結構(如 (|))和重複結構時,引擎會嘗試所有可能的匹配路徑,直至找到一個匹配或確定無匹配。
Go 採用 RE2 (DFA)實現,不支援回溯匹配。

Atomic Grouping and Possessive Quantifiers(原子組和佔有量詞)
一些正規表示式引擎支援原子組 (?>...) 和佔有量詞,這些機制有助於控制回溯行為,以提高匹配效率和準確性。Go 也不支援原子組和佔有量詞。

相關文章

  • 正規表示式基礎知識
  • 使用正規表示式抽取所需文字
  • 使用Python從Markdown文件中自動生成標題導航
  • Python正則處理多行日誌一例
  • Scala正則和抽取器:解析方法引數

相關文章