Go で簡単なクローラーを書いてみた
最近、会社で非同期処理が辛い的な話をよくします。
個人的には Go を推しているので、せっかくだから簡単なクローラーを Go で書いてみました。
作ったやつ#
起点となるURLを指定する。
取得した HTML の a タグで指定してあるURLを次に参照する。
同じURLは一度だけ参照。
Webページの取得は非同期に。
クロールする深さを指定できる
解説#
goroutine の制御#
基本的に、メインの goroutine (main 関数) と、Webページにアクセスしてスクレイピングする goroutine (Crowl 関数) の2種類の goroutine でやり取りを行います。
goroutine の間は、3種類のチャンネルを使用して連携しています。
リクエスト#
リクエストは、リクエストチャンネルに追加します。
メインの goroutine でリクエストをチャンネルから取り出し、Crawl 関数を goroutine として実行しています。
この時、クロールの深さと URL を参照したかどうかのチェックを行っています。
クロール処理#
func Crawl(url string, depth int, ch *Channels) {
defer func() { ch.quit <- 0 }()
// WebページからURLを取得
urls, err := Fetch(url)
// 結果送信
ch.res <- Result{
url: url,
err: err,
}
if err == nil {
for _, url := range urls {
// 新しいリクエスト送信
ch.req <- Request{
url: url,
depth: depth - 1,
}
}
}
}
まず、指定されたURLのWebページを取得し、その中のURLを取得します。
取得した結果(成功 or エラー)を、結果用のチャンネルに追加します。
また、Webページ内のURLが取得出来た場合は、新しいリクエストとしてそれぞれチャンネルに追加します。 ここで、クロールの深さをデクリメントします。
終了時に、処理終了の通知をチャンネルに追加します。
Web ページの取得とスクレイピング#
func Fetch(u string) (urls []string, err error) {
baseUrl, err := url.Parse(u)
if err != nil {
return
}
resp, err := http.Get(baseUrl.String())
if err != nil {
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return
}
urls = make([]string, 0)
doc.Find("a").Each(func(_ int, s *goquery.Selection) {
href, exists := s.Attr("href")
if exists {
reqUrl, err := baseUrl.Parse(href)
if err == nil {
urls = append(urls, reqUrl.String())
}
}
})
return
}
スクレイピングは、goquery を使ってみました。jQuery 使うような感覚で簡単に要素を抽出できて便利です。
チャンネルの制御#
リクエスト、結果、終了通知の3つのチャンネルは、メインの goroutine で制御しています。
// ワーカーの数
wc := 0
done := false
for !done {
select {
case res := <-chs.res:
if res.err == nil {
fmt.Printf("Success %s\n", res.url)
} else {
fmt.Fprintf(os.Stderr, "Error %s\n%v\n", res.url, res.err)
}
case req := <-chs.req:
if req.depth == 0 {
break
}
if urlMap[req.url] {
// 取得済み
break
}
urlMap[req.url] = true
wc++
go Crawl(req.url, req.depth, chs)
case <-chs.quit:
wc--
if wc == 0 {
done = true
}
}
}
また、リクエストを取得して goroutine を実行する際にワーカー数をインクリメント、結果通知を受け取った場合にワーカー数をデクリメントしています。
ワーカー数が 0 になったら、終了です。