學習軌道
/
Go
Go
/
練習
/
解析日誌檔
解析日誌檔

解析日誌檔

學習練習

簡介

regexp 套件提供 Go 中正規表達式的支援。

語法

正規表達式所接受的語法與 Perl、Python 及其他語言所用的一般語法相同。

搜尋模式與輸入文字都會以 UTF-8 解讀。

使用反引號(`)建立字串時,反斜線(\)沒有任何特殊意義,也不會標示定位字元\t或換行字元\n這類特殊字元的開頭:

"\t\n" // regular string literal with 2 characters: a tab and a newline
`\t\n`// raw string literal with 4 characters: two backslashes, a 't', and an 'n'

因此,用反引號來寫正規表達式比較理想,因為這樣就不需要跳脫反斜線:

"\\" // string with a single backslash
`\\` // string with 2 backslashes

編譯模式:RegExp型別

要使用正規表達式,我們必須先編譯字串模式。這裡的編譯是指把正規表達式的字串模式轉換成更容易操作的內部表示法。每個模式只需要編譯一次,之後就能重複使用編譯後的正規表達式。regexp.Regexp型別代表編譯後的正規表達式。我們可以用regexp.Compile函式把字串模式編譯成regexp.Regexp。如果編譯失敗,這個函式會回傳nil和一個錯誤:

re, err := regexp.Compile(`(a|b)+`)
fmt.Println(re, err) // => (a|b)+ <nil>
re, err = regexp.Compile(`a|b)+`)
fmt.Println(re, err) // => <nil> error parsing regexp: unexpected ): `a|b)+`

MustCompile函式是Compile的便利替代方案:

re = regexp.MustCompile(`[a-z]+\d*`)

使用這個函式時,不需要處理錯誤。

Caution

MustCompile只該用在我們確定模式能夠編譯的時候,否則程式會 panic。

正規表達式的方法

Regexp有 16 個方法可以比對正規表達式並找出符合的文字。這些方法的名稱都符合這個正規表達式:

Find(All)?(String)?(Submatch)?(Index)?
  • 如果有All,這個方法會比對整個表達式一連串不重疊的符合項。
  • 如果有String,引數會是字串;否則會是位元組切片;回傳值也會隨情況調整。
  • 如果有Submatch,回傳值會是一個切片,指出表達式一連串的子符合項。
  • 如果有Index,符合項與子符合項會以輸入字串中的位元組索引對來表示。

另外還有一些方法可以:

  • 用替代字串取代正規表達式的符合項,以及
  • 拆分以正規表達式分隔的字串。

整體來說,regexp套件定義了 40 多個函式與方法。下面我們會示範幾個方法的用法。這些方法和其他函式的詳細資訊,請參閱 API 文件。

MatchString範例

MatchString方法會回報字串是否包含正規表達式的任何符合項。

re = regexp.MustCompile(`[a-z]+\d*`)
b = re.MatchString("[a12]")       // => true
b = re.MatchString("12abc34(ef)") // => true
b = re.MatchString(" abc!")       // => true
b = re.MatchString("123 456")     // => false

FindString範例

FindString方法會回傳一個字串,內容是正規表達式最左邊符合項的文字。

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.FindString("[a12]")       // => "a12"
s = re.FindString("12abc34(ef)") // => "abc34"
s = re.FindString(" abc!")       // => "abc"
s = re.FindString("123 456")     // => ""

FindStringSubmatch範例

FindStringSubmatch方法會回傳一個字串切片,內容是正規表達式最左邊符合項的文字,以及它的子表達式(如果有的話)的符合項。這可以用來找出符合捕獲群組的字串。回傳值為nil代表沒有符合項。

re = regexp.MustCompile(`[a-z]+(\d*)`)
sl = re.FindStringSubmatch("[a12]")       // => []string{"a12","12"}
sl = re.FindStringSubmatch("12abc34(ef)") // => []string{"abc34","34"}
sl = re.FindStringSubmatch(" abc!")       // => []string{"abc",""}
sl = re.FindStringSubmatch("123 456")     // => <nil>

ReplaceAllString範例

re.ReplaceAllString(src,repl)方法會回傳src的副本,並把正規表達式re的符合項取代為替代字串repl。

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.ReplaceAllString("[a12]", "X")       // => "[X]"
s = re.ReplaceAllString("12abc34(ef)", "X") // => "12X(X)"
s = re.ReplaceAllString(" abc!", "X")       // => " X!"
s = re.ReplaceAllString("123 456", "X")     // => "123 456"

Split範例

re.Split(s,n)方法會把文字s切成以該表達式分隔的子字串,並回傳這些表達式符合項之間子字串的切片。數量n決定最多要回傳幾個子字串。如果n<0,這個方法會回傳所有子字串。

re = regexp.MustCompile(`[a-z]+\d*`)
sl = re.Split("[a12]", -1)      // => []string{"[","]"}
sl = re.Split("12abc34(ef)", 2) // => []string{"12","(ef)"}
sl = re.Split(" abc!", -1)      // => []string{" ","!"}
sl = re.Split("123 456", -1)    // => []string{"123 456"}

說明

這個練習會帶你解析日誌檔。

在最近一次安全審查之後,你被要求清理組織中封存的日誌檔。

所有傳入函式的字串都保證不是 null,而且沒有開頭與結尾的空白。

1. 辨識格式混亂的日誌行

你需要大致了解你的封存檔中有多少日誌行不符合現行標準。 你相信只要簡單測試一下,就能看出某一行日誌是否有效。 若要視為有效,一行必須以下列其中一個字串開頭:

  • [TRC]
  • [DBG]
  • [INF]
  • [WRN]
  • [ERR]
  • [FTL]

實作IsValidLine函式,如果字串無效就回傳false,否則回傳true。

IsValidLine("[ERR] A good error here")
// => true
IsValidLine("Any old [ERR] text")
// => false
IsValidLine("[BOB] Any old text")
// => false

2. 分割日誌行

有一個新團隊加入了組織,而你發現他們的日誌檔使用了奇怪的「欄位」分隔符。 他們不用像冒號「:」這樣合理的字元,而是使用像「<--->」或「<=>」這樣的字串(因為比較好看)。事實上,任何字串只要第一個字元是「<」、最後一個字元是「>」,且中間是「~」、「*」、「=」和「-」的任意組合都可以。

實作SplitLogLine函式,它接收一行並回傳一個字串陣列,其中每個字串各包含一個欄位。

SplitLogLine("section 1<*>section 2<~~~>section 3")
// => []string{"section 1", "section 2", "section 3"},

3. 計算引號文字中包含password的行數

團隊需要知道引號文字中提及密碼的地方,以便手動檢查。

實作CountQuotedPasswords函式,以指出手動檢查可能的規模。

找出字串「password」(可能為任意大小寫組合)被引號包住的日誌行。 你應該考慮引號內在「password」前後可能有其他內容。 每一行最多只會有兩個引號。

傳入此函式的行,可能符合或不符合任務 1 所定義的有效性。 無論它們是否有效,我們都會以相同方式處理。

lines := []string{
    `[INF] passWord`, // contains 'password' but not surrounded by quotation marks
    `"passWord"`,  // count this one
    `[INF] User saw error message "Unexpected Error" on page load.`, // does not contain 'password'
    `[INF] The message "Please reset your password" was ignored by the user`, // count this one
}
// => 2

4. 移除日誌中的殘留文字

你發現某些日誌的上游處理程序會在日誌中四處散布「end-of-line」文字,後面接著行號(中間沒有空格)。

實作RemoveEndOfLineText函式,接收一個字串,移除 end-of-line 文字,並回傳一個「乾淨」的字串。

不包含 end-of-line 文字的行應保持原樣回傳。

只需移除 end-of-line 字串。 不要嘗試調整空白字元。

RemoveEndOfLineText("[INF] end-of-line23033 Network Failure end-of-line27")
// => "[INF]  Network Failure "

5. 為日誌行標註使用者名稱

你注意到有些日誌行包含提及使用者的句子。 這些句子總是包含字串"User",後面接著一個或多個空白字元,然後是使用者名稱。 你決定為這類行加上標籤。

實作一個函式TagWithUserName,用來處理日誌行:

  • 不包含字串"User "的行保持不變。
  • 對於包含字串"User "的行,在該行前面加上[USR],後面接著使用者名稱。

例如:

result := TagWithUserName([]string{
    "[WRN] User James123 has exceeded storage space.",
	"[WRN] Host down. User   Michelle4 lost connection.",
	"[INF] Users can login again after 23:00.",
	"[DBG] We need to check that user names are at least 6 chars long.",
})
// => []string {
//  "[USR] James123 [WRN] User James123 has exceeded storage space.",
//  "[USR] Michelle4 [WRN] Host down. User   Michelle4 lost connection.",
//  "[INF] Users can login again after 23:00.",
//  "[DBG] We need to check that user names are at least 6 chars long."
// }

你可以假設:

  • 在日誌中,使用者名稱後面至少會有一個空白字元。
  • 每一行中,字串"User "最多只會出現一次。
  • 使用者名稱是不含空白字元的非空字串。
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Go Exercism

準備好開始 解析日誌檔 了嗎?

註冊 Exercism,透過 34 個概念165 個練習 和真人引導來學習並精通 Go,全部免費。