Goのtext/scannerを使うと、トークナイズ処理ができるらしい。
やりたいことは単純で
- スペースに区切られたログをタブ区切りにしたい。
- 一回の読み込みでスペース区切りしてみたい。(→なのでトークナイザを使う)
ただ少し難点があって
- "aaa bbb ccc" [2018-04-10 10:32]といったダブルクオーテーションや[]で囲まれた場合はタブ区切りにしてほしくない。
ほんとやりたいことは単純だ。
これまでに正規表現を使ってみたり、単純にスペースでsplitして各itemを"で判断してまたコンカチ、でもいいんだけど、
Goのscannerを使ってみたかったのでやってみることにした。
ちなみにだけど正規表現処理はpythonでもGoでもめちゃ遅いしCPUをけっこう消費することが分かっている。
text/scanner では解決できなかった
結論から言うと、text/scannerでは解決できないことが分かった。
scanner - The Go Programming Language
※なのでタイトルみたくトークナイズ処理はできてないけど
似た処理はできた。
基本的にScan()関数は一文字ごとに判断している。
その一文字が数値、文字や記号であれば、その文字以降が数値、コメント、識別子・・・といった
意味のあるカテゴリーの文字の羅列になっているときに意味のある単語(トークン)のかたまりとして判断される。
なのでスペースで区切った文字列全部をトークンとして判断させようとしても、
下記のカテゴリに当てはまってなければ1文字づつ区切られてしまうのでダメだった。
src/text/scanner/scanner.go - The Go Programming Language
GoTokens = ScanIdents | ScanFloats | ScanChars | ScanStrings | ScanRawStrings | ScanComments | SkipComments
思い通りのトークナイザはできないがサンプルコードを載せておく。
ここを参考にした。
https://socketloop.com/tutorials/golang-how-to-tokenize-source-code-with-text-scanner-package
package main import ( "fmt" "strings" "text/scanner" ) func main() { text_log := `2018-04-10T10:32:00.123456Z hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1"` codeReader := strings.NewReader(text_log) fmt.Println(text_log) fmt.Println("---------------------------") var scn scanner.Scanner scn.Init(codeReader) // * Whitespaceは区切り文字として削除する文字。デフォルトは下記 // GoWhitespace = 1<<'\t' | 1<<'\n' | 1<<'\r' | 1<<' ' // 下記のようにして変更できる。 //scn.Whitespace = 1<<' ' // * Mode:トークンとして判断するカテゴリ。デフォルトは下記 // Mode : ScanIdents | ScanFloats | ScanChars | ScanStrings | ScanRawStrings | ScanComments | SkipComments // 下記のようにして変更できる。 //scn.Mode = scanner.ScanIdents | scanner.ScanComments tok := scn.Scan() fmt.Println(scn.TokenText()) for tok != scanner.EOF { tok = scn.Scan() //fmt.Println(tok) fmt.Println(scn.TokenText()) } }
これを実行するとこんな結果になる。
2018-04-10T10:32:00.123456Z hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1" --------------------------- 2018 # <--- タイムスタンプがぶつ切りになってしまってる - 04 - 10 T10 : 32 : 00.123456 Z hoge1234hage aaa 10.20 # <--- IPアドレスも数値と判断されてしまってる .30 .40 : 80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1"
bufio の scanner で解決できた
そこでどうしようかとググってみたらよさげな
サンプルを見つけたので試してみたところ、うまくいった。
baubaubau.hatenablog.com
参考にさせていただきます!
package main import ( "bufio" "fmt" "strings" ) func main() { fmt.Println("------Sample Text-------") text_log := `2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1"` codeReader := strings.NewReader(text_log) fmt.Println(text_log) fmt.Println("---------------------------") fmt.Println("Normal splitter") fmt.Println() scn := bufio.NewScanner(codeReader) scn.Split(bufio.ScanWords) for scn.Scan(){ fmt.Println(scn.Text()) } fmt.Println("---------------------------") fmt.Println("Customized splitter") fmt.Println() codeReader = strings.NewReader(text_log) scn_sp := bufio.NewScanner(codeReader) dbl_q_on := false splitSpace := func(data []byte, atEOF bool) (advance int, token []byte, err error){ for i := 0 ; i < len(data) ; i++ { if data[i] == '"' || data[i] == '[' || data[i] == ']' { dbl_q_on = ! dbl_q_on } if data[i] == ' ' { if ! dbl_q_on { return i + 1, data[:i], nil } } } return 0, data, bufio.ErrFinalToken } scn_sp.Split(splitSpace) for scn_sp.Scan(){ fmt.Println(scn_sp.Text()) } }
------Sample Text------- 2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1" --------------------------- Normal splitter 2018-04-10T10:32:00 [2018-04-10 10:32] # <--- こういうのだめ hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa # <--- こういうのだめ HTTP/1.1" --------------------------- Customized splitter 2018-04-10T10:32:00 [2018-04-10 10:32] # <--- うまくいった hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1" # <--- うまくいった
さらにもうちょっと改善
上述のコードをもとにもうちょっと、一回のreadでパースできないかと
Scanを使わないで一文字づつの判定でパースするようにしてみた。
それとメモリ消費も抑えるようにもやってみた。
package main import ( "fmt" "unsafe" ) func parseLog(data *[]byte) *string { dbl_q_on := false output := "" cur_pos := 0 for i := 0 ; i < len(*data) ; i++ { if (*data)[i] == '"' || (*data)[i] == '[' || (*data)[i] == ']' { dbl_q_on = ! dbl_q_on } if (*data)[i] == ' ' { if ! dbl_q_on { fmt.Println(string((*data)[cur_pos:i])) output += string((*data)[cur_pos:i]) + "\t" cur_pos = i + 1 } } } output += string((*data)[cur_pos:]) return &output } func main() { text_log := `2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1"` fmt.Println(text_log) fmt.Println("-------------------------") text_log_b := *(*[]byte)(unsafe.Pointer(&(text_log))) //fmt.Println(text_log_b) output_str := parseLog(&text_log_b) fmt.Println("-------------------------") fmt.Println(*output_str) }
unsafeはどこでも言われてるが使う時に注意が必要なので、むやみやたらとunsafeを使うことはおすすめしない。
unsafeはログを読み込んで1行をbyteに変換することも想定にいれてて
メモリ消費を減らせないかと考えたので使ってる。
2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1" ------------------------- 2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 ------------------------- 2018-04-10T10:32:00 [2018-04-10 10:32] hoge1234hage aaa 10.20.30.40:80 0.000 0.003 200 401 "GET http://hoge.fuga.com/uaaa HTTP/1.1"
CPUの負荷も下がった
ログをぶったぎるのにトークナイザを使おうとしたのは
ログをreadする処理を減らしたかったからだ。
スペースでsplitして各itemの先頭をさらに判断してパースする場合
推測なんだけど、スペースを探す処理でログを一回readして、
さらに各itemをforループするときにもう一回readしてる。
2回のループ。
でも先頭から一文字づつ読み込んで判定すれば、
適切な場所に来たら1トークンとなるので、readは一回で済む(と思う)。
そのせいか大量ログで試した場合、splitでやるよりも
readを減らした方がCPU負荷がかなり下がった。
勉強になりました。
Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る impress top gearシリーズ
- 作者: Sau Sheong Chang,武舎広幸,阿部和也,上西昌弘
- 出版社/メーカー: インプレス
- 発売日: 2017/03/17
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
- 作者: 渋川よしき
- 出版社/メーカー: Lambda Note
- 発売日: 2017/10/19
- メディア: テキスト
- この商品を含むブログを見る