Go 正則表達式學習

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

關於正則的基礎知識及應用,之前寫過幾篇文章,讀者可以閱讀文後的相關資料作一基本瞭解。本文主要學習 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("[email protected]")) // 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 也不支持原子組和佔有量詞。

相關文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章