【Go言語】goqueryとゴルーチンで簡単高速Webスクレイピング

WebスクレイピングといえばPythonのイメージがありますが、

Go言語でのWebスクレイピングが簡単かつ高速だったので紹介します。

今回注目するのはgoqueryとゴルーチンです。

以下の記事を組み合わせたような構成です。

【毎秒1万リクエスト!?】Go言語で始める爆速Webスクレイピング【Golang】
Goとgoqueryでスクレイピング

goqueryとは

Go言語でjQueryのようにWebスクレイピングができるライブラリです。

GitHub
https://github.com/PuerkitoBio/goquery

使う前にgo getで事前に導入しておき、importで"github.com/PuerkitoBio/goquery"を追加しておきます。

使い方の例

// Documentの形式で取得
doc, err := goquery.NewDocument(url)

// 要素をFindとTextを使ってstringとして取得
// idで取得は # をつける
tex := doc.Find("#some_id").Text()
// classで取得は . をつける
tex := doc.Find(".some_class").Text()

// あとはよしなに

NewDocumentの処理としてはhttp.GetでWebページを取ってきて、html.Parseで解析しツリー構造化、最後に扱いやすいDocumentに変換します。

Findでツリー構造からの検索を行いよしなな結果を取得します。

goqueryはツリー構造からの要素検索が簡単に行える点が良いですね。

ゴルーチンとは

Go言語において簡単に並列処理化する仕組みのことで、スレッドなどのリソースを意識せずに使えるようになっています。

使い方も簡単で、並列化したい関数にgoを付けるだけです。gogo

注意点としては戻り値を返すような処理には使えないので、go func() {}()としてクロージャにしてあげるととよいでしょう。

また、並列化した処理がすべて終わるまで次の処理を待ちたい場合が多いため、WaitGroupを使ってすべての処理が終わるまで待ちます。

// 通信のような重い処理が存在する場合
func connect() {
    // 重い処理を行う
    // 通信は重い
    res, e := http.Get("通信先")
    // 重い処理終わり
}

// 何もせずforで回すと通信結果が返ってくるまで次の通信が走らない=直列処理
func main() {
    for i := 0; i < 10; i++ {
        connect()
    } 
}

// ゴルーチンを使うと前の通信結果を待たずに次の通信を走らせる=並列処理
func main() {
    for i := 0; i < 10; i++ {
        // 並列化したい関数にgoをつけるだけ
        go connect()
        // 並列化したい関数が戻り値を持つならクロージャにする
        go func () {
            v := connectWithReturn()
        } ()
    } 
}

// 実際はWaitGroupとともに使ってすべての処理が終わるまで待つ必要がある
func main() {
    // 並列処理のカウンタとして働く
    var wg sync.WaitGroup
    // 10カウンタを追加
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func () {
            connect()
            // カウンタ消費
            wg.Done()
        }()
    }
    // 10カウンタすべて消費されるまで処理を待つ
    wg.Wait()
}

Webスクレイピングをしてみる

例として当ブログのタイトルを取得してみます。

package main

import (
    "fmt"
    "sync"

    "github.com/PuerkitoBio/goquery"
)

// 通信してスクレイピング、ブログタイトル(or はてなID)を取得
func connect(count int) {
    doc, _ := goquery.NewDocument("https://tetsujp84.hatenablog.com/")
    tex := doc.Find("#title").Text()
    fmt.Printf(tex+":%d回目\n", count)
}

func main() {
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        // クロージャーでループ変数を使う場合はキャプチャが必要
        count := i
        go func() {
            connect(count)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("終わり")
}
実行結果
鉄ブロ:3回目
鉄ブロ:7回目
鉄ブロ:5回目
鉄ブロ:0回目
鉄ブロ:4回目
鉄ブロ:2回目
鉄ブロ:6回目
鉄ブロ:8回目
鉄ブロ:1回目
鉄ブロ:9回目
終わり

並列化により実行順はバラバラになります。

go funcを除くだけで直列処理になります。楽ちん。

終わり

スクレイピングはサイトへのアクセスを繰り返すことになるのでほどほどにしましょう。
規約により禁止されていたらごめんなさい。

参考

Go言語でクロージャと goroutine を組み合わせた時に起こること