Goでのスクレイピングに使っていたgoqueryをcollyに置き換えてみた

# はじめに
以前，goqueryを用いて東京ドームのイベント情報を1日1回取得するLambda関数を実装してみたのですが，どうやら[colly](http://go-colly.org/)というライブラリが割といいという話を後になって知りました．
深夜テンションでライブラリを置き換えてみたのでその記録です．
日本語での情報が少ないため，go言語でスクレイピングをしたい方のお役に立てれば幸いです．

goqueryで実装したときの記録：https://qiita.com/a_uchida/items/1cdd7a1a6003d8a93651

# colly vs goquery
## スター数
[![Star History Chart](https://api.star-history.com/svg?repos=gocolly/colly,PuerkitoBio/goquery&type=Date)](https://star-history.com/#gocolly/colly&PuerkitoBio/goquery&Date)

collyのほうが急激に伸びていますね．というのも，collyはgoqueryをもとにして作られたライブラリなので当然っちゃ当然なのかも知れません．
実際に[collyのgo.mod](https://github.com/gocolly/colly/blob/master/go.mod)を見てみると，`github.com/PuerkitoBio/goquery`が含まれているのがわかります．

## 簡単な実装例
Googleのトップページにアクセスし，タイトルを出力する例を見てみましょう．
### goquery
```go
package tmp

import (
	"fmt"
	"net/http"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	url := "https://www.google.com"

	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}

	doc, err := goquery.NewDocumentFromReader(res.Body)

	if err != nil {
		panic(err)
	}

	title := doc.Find("title").Text()
	fmt.Println(title)
}
```

goqueryでも，`NewDocument(url string)`関数を使って指定したURLからHTMLを持ってくることも可能ですが，deprecatedになっているので，現在は`net/http`を使ってHTMLを取得した上で，`NewDocumentFromReader(r io.Reader)`関数を用いて読み込むことが望ましいです．

### colly
```go
package tmp

import (
	"fmt"

	"github.com/gocolly/colly"
)

func main() {
	c := colly.NewCollector()

	url := "https://www.google.com"

    c.OnError(func(_ *colly.Response, err error) {
		panic(err)
	}

	c.OnHTML("title", func(e *colly.HTMLElement) {
		fmt.Println(e.Text)
	})

	c.Visit(url)
}
```
collyで直接エラーハンドリングが可能であり，可読性が大幅に向上します．
これに加え，並列処理，スレッド数の上限の設定，実行遅延なども可能です．

# 実際の実装
## 旧実装（goquery）
```go
package scraper

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/PuerkitoBio/goquery"
)

func FetchTodayEvent() string {
	jst, _ := time.LoadLocation("Asia/Tokyo")

    url := "https://www.tokyo-dome.co.jp/dome/event/schedule.html"
	res, err := http.Get(url)

	if err != nil {
		fmt.Println("Failed to scrape")
		panic(err)
	}
	defer res.Body.Close()

	doc, _ := goquery.NewDocumentFromReader(res.Body)

	selector := "div.c-mod-tab__body:nth-child(2) > table > tbody"
	innerSelector := "tr.c-mod-calender__item"
	dateSelector := "th > span:nth-child(1)"
	categorySelector := "td:nth-child(2) > div > div:nth-child(1) > p > span"
	titleSelector := "td > div > div:nth-child(2) > p.c-mod-calender__links"
	timeSelector := "td > div > div:nth-child(2) > p:nth-child(2)"

	selection := doc.Find(selector)

	var event string
	selection.Find(innerSelector).Each(func(index int, s *goquery.Selection) {
		date, _ := strconv.Atoi(s.Find(dateSelector).Text())
		category := s.Find(categorySelector).Text()
		title := s.Find(titleSelector).Text()
		info := s.Find(timeSelector).Text()

		if date == time.Now().In(jst).Day() {
			if title == "" {
				event = "イベントなし"
			} else {
				event = title + "（" + category + "）" + "\n" + info
			}
		}
	})
	return event
}
```

## 新実装（colly）
```go
package scraper

import (
	"fmt"
	"time"

	"github.com/gocolly/colly"
)

func FetchTodayEvent() string {
	jst, _ := time.LoadLocation("Asia/Tokyo")

	url := "https://www.tokyo-dome.co.jp/dome/event/schedule.html"

	c := colly.NewCollector()

	selector := "div.c-mod-tab__body:nth-child(2) > table > tbody"
	innerSelector := "tr.c-mod-calender__item"
	dateSelector := "th > span:nth-child(1)"
	categorySelector := "td:nth-child(2) > div > div:nth-child(1) > p > span"
	titleSelector := "td > div > div:nth-child(2) > p.c-mod-calender__links"
	timeSelector := "td > div > div:nth-child(2) > p:nth-child(2)"

	var event string
	c.OnHTML(selector, func(e *colly.HTMLElement) {
		e.ForEach(innerSelector, func(_ int, s *colly.HTMLElement) {
			date := s.ChildText(dateSelector)
			category := s.ChildText(categorySelector)
			title := s.ChildText(titleSelector)
			info := s.ChildText(timeSelector)
			today := time.Now().In(jst).Format("02")

			if date == today {
				if title == "" {
					event = "イベントなし"
				} else {
					event = title + "（" + category + "）" + "\n" + info
				}
			}
		})
	})

	c.Visit(url)

	return event
}
```

# 所感
新実装の方が依存ライブラリが減ったこともあり，何をやっているかが直感的に幾分かわかりやすくなったように思えます．
goqueryよりも様々な機能がcollyに備わっており，スター数もcollyの方が急劇に伸びていることから，goでスクレイピングをしたいときはcollyが良さそうです．

# リポジトリURL
https://github.com/Eagle-Konbu/tokyo-dome-event-notifier