Goでは、regexpパッケージが正規表現を扱うための機能を提供しています。
受け付ける正規表現の構文は、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*`)
この関数を使えば、エラーを処理する必要はありません。
パターンが確実にコンパイルできると分かっている場合にのみMustCompileを使うべきです。そうでなければ、プログラムはパニックを起こします。
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ではなく、先頭と末尾に空白がないことが保証されています。
アーカイブ内のログ行のうち、現在の基準に準拠していないものがどれくらいあるのか、おおよその見当をつける必要があります。 簡単なテストで、そのログ行が有効かどうかがわかると考えています。 有効とみなされるには、行が次のいずれかの文字列で始まっている必要があります。
文字列が有効でなければfalse、そうでなければtrueを返すIsValidLine関数を実装しましょう。
IsValidLine("[ERR] A good error here")
// => true
IsValidLine("Any old [ERR] text")
// => false
IsValidLine("[BOB] Any old text")
// => false
新しいチームが組織に加わり、そのチームのログファイルでは「フィールド」の区切りに変わった区切り文字が使われていることに気づきます。 コロン「:」のような常識的なものではなく、"<--->"や"<=>"のような文字列を使っているのです(そのほうがきれいだからです)。実際、最初の文字が"<"で最後の文字が">"であり、その間に"~"、"*"、"="、"-"の任意の組み合わせが入る文字列なら何でも構いません。
行を受け取り、それぞれがフィールドを含む文字列の配列を返すSplitLogLine関数を実装しましょう。
SplitLogLine("section 1<*>section 2<~~~>section 3")
// => []string{"section 1", "section 2", "section 3"},
passwordを含む行の数を数えるチームは、引用符で囲まれたテキスト内のパスワードへの言及を把握して、手作業で確認できるようにする必要があります。
手作業がどれくらいの規模になりそうかの目安を示すために、CountQuotedPasswords関数を実装しましょう。
"password"という文字列が、大文字と小文字の任意の組み合わせで、引用符で囲まれているログ行を特定します。 "password"の前後で、引用符の内側に追加の内容がある可能性も考慮する必要があります。 各行に含まれる引用符は最大2つです。
この処理に渡される行は、タスク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
ログの上流処理の一部が、"end-of-line"というテキストとそれに続く行番号(間に空白は入りません)をログ全体に散りばめていることに気づきました。
文字列を受け取り、end-of-lineテキストを取り除いて「きれいな」文字列を返すRemoveEndOfLineText関数を実装しましょう。
end-of-lineテキストを含まない行は、そのまま返す必要があります。
end-of-line文字列だけを取り除きます。 空白の調整は行わないでください。
RemoveEndOfLineText("[INF] end-of-line23033 Network Failure end-of-line27")
// => "[INF] Network Failure "
ログ行の中には、ユーザーに言及した文が含まれているものがあることに気づきました。
これらの文には、必ず"User"という文字列のあとに1つ以上の空白文字、そしてユーザー名が続きます。
そこで、そのような行にタグを付けることにします。
ログ行を処理する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 "という文字列は最大1回しか現れません。