こんにちは。KOUKIです。
とあるWeb系企業で、Goエンジニアをやっています。
今日は、Go言語で文字数カウントツールを作りましょう。
<目次>
シチュエーション
「https://www.rfc-editor.org/rfc/rfc1050.txt」のようにインターネットで公開されているテキストをGo言語で読み込んで、a,b,cなどの文字がその文章の中でどのくらい使われているのかチェックするツールを作成します。
実装1: 逐次処理
早速、実装しましょう。
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 |
package main import ( "fmt" "io/ioutil" "net/http" "strings" "time" ) const allLetters = "abcdefghijklmnopqrstuvwxyz" func countLetters(url string, frequency *[26]int32) { resp, _ := http.Get(url) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) for _, b := range body { c := strings.ToLower(string(b)) index := strings.Index(allLetters, c) if index >= 0 { frequency[index] += 1 } } } func main() { start := time.Now() var frequency [26]int32 for i := 1000; i <= 1200; i++ { countLetters( fmt.Sprintf("https://www.rfc-editor.org/rfc/rfc%d.txt", i), &frequency, ) } elapsed := time.Since(start) fmt.Println("Done") fmt.Printf("Processing took %s\n", elapsed) for i, f := range frequency { fmt.Printf("%s -> %d\n", string(allLetters[i]), f) } } |
countLetters関数は、Web上に公開されているTextを読み込んで、frequency [26]int32変数に格納する処理を実装しています。この26文字は「abcdefghijklmnopqrstuvwxyz」を指しています。int32はカウント数ですね。
読み込んだ文字がこの26文字内に含まれるものかチェックし、含まれた場合は該当の文字のカウント数をインクリメントします。
main関数では、1000~1200番のWeb Textをリクエストできるようにfor文で繰り返し処理を実装しています。
プログラムを実行します。
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 |
$ go run main.go Done Processing took 59.376127991s a -> 533257 b -> 112398 c -> 302791 d -> 272584 e -> 913683 f -> 165911 g -> 130183 h -> 249034 i -> 539690 j -> 16454 k -> 45356 l -> 257378 m -> 216497 n -> 533842 o -> 518718 p -> 206754 q -> 14651 r -> 531012 s -> 514864 t -> 695963 u -> 187718 v -> 68991 w -> 83089 x -> 27718 y -> 95829 z -> 7647 |
いい感じですね。イメージは掴めたでしょうか?
1分くらい処理に時間がかかってしまうのが難点ですね。
実装2: Goroutine + Mutex
GoroutineとMutexを使って、平行処理を取り入れてみましょう。
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 62 |
package main import ( "fmt" "io/ioutil" "net/http" "strings" "sync" "time" ) const allLetters = "abcdefghijklmnopqrstuvwxyz" // メモリセーフのためにMutexを用意 var lock = sync.Mutex{} // WaitGroup設定 func countLetters(url string, frequency *[26]int32, wg *sync.WaitGroup) { resp, _ := http.Get(url) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) for _, b := range body { c := strings.ToLower(string(b)) // Mutex設定 lock.Lock() index := strings.Index(allLetters, c) if index >= 0 { frequency[index] += 1 } // Mutex設定 lock.Unlock() } // WaitGroup設定 wg.Done() } func main() { start := time.Now() // WaitGroup設定 wg := sync.WaitGroup{} var frequency [26]int32 for i := 1000; i <= 1200; i++ { // WaitGroup設定 wg.Add(1) // goroutineで起動 go countLetters( fmt.Sprintf("https://www.rfc-editor.org/rfc/rfc%d.txt", i), &frequency, &wg, // WaitGroup設定 ) } // WaitGroup設定 wg.Wait() elapsed := time.Since(start) fmt.Println("Done") fmt.Printf("Processing took %s\n", elapsed) for i, f := range frequency { fmt.Printf("%s -> %d\n", string(allLetters[i]), f) } } |
変更した箇所は、コメントにしてあります。
Mutexはメモリ書き込み読み込みの競合を防ぐ便利機能なので覚えておきましょう。今回は、カウント数のインクリメントで競合が起きそうなので、そこをロックしています。
プログラムを実行してみましょう。
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 |
$ go run main.go Done Processing took 1.749314879s a -> 533257 b -> 112398 c -> 302791 d -> 272584 e -> 913683 f -> 165911 g -> 130183 h -> 249034 i -> 539690 j -> 16454 k -> 45356 l -> 257378 m -> 216497 n -> 533842 o -> 518718 p -> 206754 q -> 14651 r -> 531012 s -> 514864 t -> 695963 u -> 187718 v -> 68991 w -> 83089 x -> 27718 y -> 95829 z -> 7647 |
今度は、約2秒です。めちゃくちゃ早くなりましたね。
実装3: atomic
次は、atomicパッケージのAddInt32を使ってみましょう。
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 |
package main import ( "fmt" "io/ioutil" "net/http" "strings" "sync" "sync/atomic" "time" ) const allLetters = "abcdefghijklmnopqrstuvwxyz" // var lock = sync.Mutex{} func countLetters(url string, frequency *[26]int32, wg *sync.WaitGroup) { resp, _ := http.Get(url) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) for _, b := range body { c := strings.ToLower(string(b)) // lock.Lock() index := strings.Index(allLetters, c) if index >= 0 { // frequency[index] += 1 // atomic導入 atomic.AddInt32(&frequency[index], 1) } // lock.Unlock() } wg.Done() } func main() { start := time.Now() wg := sync.WaitGroup{} var frequency [26]int32 for i := 1000; i <= 1200; i++ { wg.Add(1) go countLetters( fmt.Sprintf("https://www.rfc-editor.org/rfc/rfc%d.txt", i), &frequency, &wg, ) } wg.Wait() elapsed := time.Since(start) fmt.Println("Done") fmt.Printf("Processing took %s\n", elapsed) for i, f := range frequency { fmt.Printf("%s -> %d\n", string(allLetters[i]), f) } } |
atomicを導入するとMutexが不要になるので、コメントアウトしておきました。
プログラムを実行してみます。
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 |
$ go run main.go Done Processing took 3.097172242s a -> 533257 b -> 112398 c -> 302791 d -> 272584 e -> 913683 f -> 165911 g -> 130183 h -> 249034 i -> 539690 j -> 16454 k -> 45356 l -> 257378 m -> 216497 n -> 533842 o -> 518718 p -> 206754 q -> 14651 r -> 531012 s -> 514864 t -> 695963 u -> 187718 v -> 68991 w -> 83089 x -> 27718 y -> 95829 z -> 7647 |
約3秒ですか。。若干、遅くなりましたね。
おわりに
いかがだったでしょうか。
文字数のカウント自体はあまり実務では使わないと思います。
これが、「単語」単位のカウントであれば、使いそうな気がしますね。
一文章の中にどれだけ重複したキーワードが使われているか、それを分析するみたいな。
色々改良して楽しんでください^^
それでは、また!
コメントを残す
コメントを投稿するにはログインしてください。