こんにちは。KOUKIです。
今日は、ログ解析ツールをハンズオン形式で解説したいと思います。
ツールといっても簡単な奴ですが^^;
<目次>
完成系イメージ
以下のようなログがあるとします。
1 2 3 4 5 6 7 |
// log.txt learngoprogramming.com 10 learngoprogramming.com 10 golang.org 4 golang.org 6 blog.golang.org 2 blog.golang.org 10 |
これを読み込んで、次の形式で出力します。
1 2 3 4 5 6 |
DOMAIN VISITS --------------------------------------------- blog.golang.org 12 golang.org 10 learngoprogramming.com 20 TOTAL 42 |
作ってみよう!
データの読み込み
最初にログデータを読み込む機能を作ります。
読み込む方法はいくつかあると思いますが、今回は、bufioパッケージのNewScannerを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import ( "bufio" "fmt" "os" ) func main() { // terminalからデータを読み込む in := bufio.NewScanner(os.Stdin) // 読み込み for in.Scan() { fmt.Println(in.Text()) } } |
bufio.NewScannerでterminalから送られたデータをインスタンス化しています。
実行結果は、以下の通りです。
1 2 3 4 5 6 7 |
$ go run main.go < log.txt learngoprogramming.com 10 learngoprogramming.com 10 golang.org 4 golang.org 6 blog.golang.org 2 blog.golang.org 10 |
取得データを加工する
次に取得データを加工してみましょう。
1 2 3 4 5 6 7 |
// 読み込み for in.Scan() { // 取得したデータを[]stringに格納する fields := strings.Fields(in.Text()) fmt.Println(fields[0]) fmt.Println(fields[1]) } |
取得データをstrings.Fieldsに渡すと、stringのスライスに格納してくれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ go run main.go < log.txt learngoprogramming.com 10 learngoprogramming.com 10 golang.org 4 golang.org 6 blog.golang.org 2 blog.golang.org 10 |
いい感じですね。
この読み込み処理はツールの肝なので、データが渡されなかった場合は、エラーを返すようにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package main import ( "bufio" "fmt" "os" "strings" ) func main() { var ( // ライン番号 lines int ) in := bufio.NewScanner(os.Stdin) // 読み込み for in.Scan() { lines++ fields := strings.Fields(in.Text()) if len(fields) != 2 { fmt.Printf("wrong input: %v (line #%d)\n", fields, lines) } } } |
読み込みデータ数が2未満の場合は、エラーを返すようにしました。
1 2 3 |
$ go run main.go test wrong input: [test] (line #1) |
いい感じでデータが取得できたので、domainとvisit数に分けてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func main() { var ( lines int ) in := bufio.NewScanner(os.Stdin) for in.Scan() { ... domain := fields[0] // 取得したデータはstring型なので数値にする visits, _ := strconv.Atoi(fields[1]) fmt.Println("domain: ", domain) fmt.Println("visits: ", visits) } } |
スライスには、[“earngoprogramming.com 10”]の形式でデータが格納されるのでインデックスアクセス(fields[0])ができるようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ go run main.go < log.txt domain: learngoprogramming.com visits: 10 domain: learngoprogramming.com visits: 10 domain: golang.org visits: 4 domain: golang.org visits: 6 domain: blog.golang.org visits: 2 domain: blog.golang.org visits: 10 |
ドメインの統合
ログ上のドメインは重複している場合があります。
これを一つのドメインにするために、重複したドメインは統合しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package main import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { var ( // 合計数カウント用の変数 sum map[string]int // mapは順番を保持しないのでスライスにドメインを格納する domains []string // visitsカウント用 total int lines int ) sum = make(map[string]int) in := bufio.NewScanner(os.Stdin) for in.Scan() { lines++ fields := strings.Fields(in.Text()) if len(fields) != 2 { fmt.Printf("wrong input: %v (line #%d)\n", fields, lines) } domain := fields[0] visits, _ := strconv.Atoi(fields[1]) if _, ok := sum[domain]; !ok { // 表示順番を保持するため、スライスにドメイン名を格納する domains = append(domains, domain) } total += visits sum[domain] += visits } fmt.Println(total) fmt.Println(sum) } |
sum変数は、ドメイン名とvisit数を保持するための変数です。これはMapでできています。
Mapは格納したデータの順番を保持しないので、順番を保持できるdomainsスライスを用意しています。(terminal上に出力する際、結果がバラバラになる)
実行してみましょう。
1 2 3 |
$ go run main.go < log.txt 42 map[blog.golang.org:12 golang.org:10 learngoprogramming.com:20] |
ログ解析結果の表示
次にログ解析結果を表示させましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "bufio" "fmt" "os" "sort" "strconv" "strings" ) func main() { var ( ... for in.Scan() { ... } fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS") fmt.Println(strings.Repeat("-", 45)) // domainを昇順にする sort.Strings(domains) for _, domain := range domains { visits := sum[domain] fmt.Printf("%-30s %10d\n", domain, visits) } fmt.Printf("%-30s %10d\n", "TOTAL", total) } |
プログラムを実行してみましょう。
1 2 3 4 5 6 7 |
$ go run main.go < log.txt DOMAIN VISITS --------------------------------------------- blog.golang.org 12 golang.org 10 learngoprogramming.com 20 TOTAL 42 |
OKですね。
エラーハンドリング
ツール自体の作成は終わったのですが、最後にエラーハンドリングをしたものを載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package main import ( "bufio" "fmt" "os" "sort" "strconv" "strings" ) func main() { var ( sum map[string]int domains []string total int lines int ) sum = make(map[string]int) in := bufio.NewScanner(os.Stdin) for in.Scan() { lines++ fields := strings.Fields(in.Text()) if len(fields) != 2 { fmt.Printf("wrong input: %v (line #%d)\n", fields, lines) return } domain := fields[0] visits, err := strconv.Atoi(fields[1]) if visits < 0 || err != nil { fmt.Printf("wrong input: %q (line #%d)\n", fields[1], lines) return } if _, ok := sum[domain]; !ok { domains = append(domains, domain) } total += visits sum[domain] += visits } fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS") fmt.Println(strings.Repeat("-", 45)) sort.Strings(domains) for _, domain := range domains { visits := sum[domain] fmt.Printf("%-30s %10d\n", domain, visits) } fmt.Printf("%-30s %10d\n", "TOTAL", total) if err := in.Err(); err != nil { fmt.Println("> Err:", err) } } |
おわりに
いかがだったでしょうか。
ログの解析といっても簡単なツールでしたが、ところどころ難しく感じる場面もあったかと思います。
しかし、一つ一つ処理を分解していけば、プログラムの流れは簡単に把握できると思います。
それでは、また!
最近のコメント