Goとスクレイピングに同時に入門する

# この記事は？

この記事は [ウィルゲート Advent Calendar 2022](https://adventar.org/calendars/7899) 4 日目の記事です。

Go言語でHello Worldをしたことがなく、Webページのスクレイピングもやったことがない人が、Go言語でWebページのスクレイピングしてみる入門用の記事です。
「コピペでひとまず動かしてみる」ことが目的なので、詳しい関数の解説などはありませんがご了承ください。

この記事では「WILLGATE TECH BLOG」のトップ画面に表示されている、記事の投稿日とタイトルをスクレイピングで取得する例を紹介します。
https://tech.willgate.co.jp/

## 参考文献

『Go初心者でも作れるスクレイピングツール』という本を読んで勉強しました。
初めてGoとスクレイピングをやる私でも分かりやすい内容で書かれており、技術書展の同人誌でページ数も少ないのでとても入門しやすい本でした。

https://techbookfest.org/product/5133603998531584?productVariantID=2Kv9fJeuCaEfSswAT07ydv

# Go言語にHello Worldしよう

まず、Goのファイルを実行する練習をします。
任意のディレクトリを作成します。

```
$ mkdir go-scraper-blog
```

`main.go`ファイルを作成します。

```
$ cd go-scraper-blog
$ touch main.go
```

`main.go`ファイルの中身を記述します。

```go:main.go
package main

import "fmt"

func main() {
	fmt.Println("Hello World! Go!")
}
```

モジュール管理ツールを導入するため、以下のコマンドを実行します。
（モジュール名の例: `github.com/kotomi1338/go-scraper-blog`）

```
$ go mod init [任意のモジュール名]
```

実行すると、`go.mod`ファイルが生成されます。

ここまでで Hello World が実行できます。動作確認してみましょう。

```
$ go run .
Hello World! Go!
```

ここまででGoのファイルを実行する練習は終わりです。
ここから先ではいよいよスクレイピング入門していきます。

# スクレイピング入門しよう

## スクレイピングしたデータを保持するための型を定義

まずはスクレイピングしたデータを保持し、扱いやすくするために型を定義します。

```go:main.go
package main

// 省略

type Item struct {
	Date  string
	Title string
}
```

## スクレイピング用の関数を作成

GETリクエストを投げて帰ってきたレスポンスのメッセージボディを取得する関数`getResponse`と、データをスクレイピングする関数`getList`を作成します。

```go:main.go
package main

// 追加
import (
	"fmt"
	"net/http"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

// 省略

func getResponse(url string) (*http.Response, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("http get request error: %w", err)
	}
	return response, nil
}

func getList(response *http.Response) ([]Item, error) {
	// レスポンスボディを取得
	body := response.Body

	var items []Item

	doc, err := goquery.NewDocumentFromReader(body)
	if err != nil {
		return nil, fmt.Errorf("get document error: %w", err)
	}

	// Find関数を使用して記事の要素を取得
	entry := doc.Find("section.archive-entry.test-archive-entry.autopagerize_page_element")

	// 記事の要素ごとに必要な情報を変数に格納する
	entry.Each(func(_ int, s *goquery.Selection) {
		item := Item{}

		// 記事の投稿日を格納
		item.Date = strings.TrimSpace(s.Find("div.date.archive-date").Text())
		// 記事のタイトルを格納
		item.Title = strings.TrimSpace(s.Find("h1.entry-title").Text())

		if item.Date != "" {
			items = append(items, item)
		}
	})

	return items, nil
}
```

関数を作成した後、以下のコマンドを実行してください。

```
$ go mod tidy
```
import文をもとに依存性解決が行われます。
`go.mod`が修正され、新たに`go.sum`が追加されます。

## main関数を変更し、スクレイピングする

スクレイピングしたデータをコンソールに出力するように`main`関数を書き換えます。
先程追加した`getResponse`関数と`getList`関数を呼び出します。

```go:main.go
package main

// 省略

func main() {
	// スクレイピングするページのURL
	baseUrl := "https://tech.willgate.co.jp/"
	response, err := getResponse(baseUrl)
	if err != nil {
		panic(err)
	}

	items, err := getList(response)
	if err != nil {
		panic(err)
	}

	for _, item := range items {
		fmt.Println(item)
	}
}

// 省略
```

ここまで出来たらスクレイピングが出来ていることの動作確認です。

```
$ go run .
{2022-11-30 5%ルールを利用してプロダクトマネージャーカンファレンス2022 に参加しました!}
{2022-11-28 PhpStormを使って半年の2年目エンジニアが紹介する便利な機能･ショートカット}
{2022-11-24 社内ハッカソンを開催しました！}
{2022-10-27 ざっくり理解するPrometheusを使った監視システム #1}
{2022-09-22 2年目エンジニアが初めてのPhpStormを使いこなせるように工夫したこと}
{2022-09-16 他社との交流LT会を開催しました}
{2022-09-15 PHP Conference Japan 2022 に協賛および所属エンジニアが登壇します #phpcon}
{2022-08-09 io-ts を使った実行時型チェック（TypeScript）}
```

お疲れさまでした。
見事に記事の投稿日とタイトルを取得して出力出来ました。

# おわりに

[ウィルゲート Advent Calendar 2022](https://adventar.org/calendars/7899) 明日は「ファシリテーションや司会進行で心掛けていること」です！乞うご期待！