[go入門]Pythonと比べてgoに親しむ[スクレイピング]

# goでスクレイピング

、初見の`go`でスクレイピングプログラムを書いてみます。

## 環境
- MacBook Pro 2021 14inc
- macOS Montrey 12.0
- go version 1.182

## インストール
今回はバージョン管理のことは考えず、`homebrew`でインストールします。

```bash
$ brew install go
```

インストール確認

```bash
$ go version
go version go1.18.2 darwin/arm64
```

作業ディレクトリを用意したら

```bash
go mod init ディレクトリ名
```

を実行しておきましょう。モジュールを使用する際に必要になります。


## 通信部分
スクレイピングの最初の工程はurlからhtmlを獲得することです。
goでは組み込みモジュールの`net/http`が用意されています。


```go
res, err := http.Get(url)
```

Pythonなら
```python
requests.get(url)
```

１つ目の返り値である`res`にはstatus_codeやresponse_bodyなどが含まれています。
response_bodyから任意の要素を取り出すためにdomへ変換します。
この操作には`goquery`を使います。

```bash
$ go get github.com/PuerkitoBio/goquery
```

githubのリポジトリを直接指定しているのがなんだか新鮮ですね。
Pythonでは

```bash
$ pip install bs4
```

goqueryのドキュメントは[こちら](https://pkg.go.dev/github.com/PuerkitoBio/goquery)
```go
doc, err = goquery.NewDocumentFromReader(res.Body)
```

Pythonなら
```python
soup = BeautifulSoup(res.text, 'html')
```

一連の流れを関数にしました。urlを渡すとdomを返す関数です。

```go
func fetch_doc(url string) *goquery.Document {
	time.Sleep(1 * time.Second) // 連続アクセス回避のために1秒待ちます
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close() // 必須.
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		panic(err)
	}
	return doc
}
```

## struct(構造体)について
`go`には`class`という概念はなく、代わりにstruct(構造体)を使います。

```go
type 構造体名 struct{}
```

のように宣言します。

```go
type BaseCrawler struct{
    Base_url string
    Url string
    Detail_selector string
    Next_selector string
   Category_url string
}
```

ここで宣言した変数はインスタンス変数のように使うことができます。

構造体に紐付いた関数(メソッド)を定義するためにはレシーバ(ここではself)とstruct名を`func`の後に続けます。

```
func(self BaseCrawler) fetch_doc *goquery.Document {
     return
}
```

説明を省略した部分もありますが、以上を踏まえ基底構造体を定義しました。
qiitaのgolangの記事一覧ページから記事URLを取得しています。


```go
package main

import (
	"fmt"
	"time"
	"net/http"
	"github.com/PuerkitoBio/goquery"
)

// 構造体を宣言します
type BaseCrawler struct{
	Site_id int // １文字目を大文字にしているのは外部から読み込むためです
	Base_url string
	Url string
	Detail_selector string
	Next_selector string
	Category_url string
}

func(self BaseCrawler) Main() {
	self.crawl(self.Category_url)
}

func(self BaseCrawler) crawl(category_url string) {
    // 無限ループ
	for i := 0; ; i++ {
		doc := self.fetch_doc(category_url)
		doc.Find(self.Detail_selector).Each(func(_ int, s *goquery.Selection) {
			detail_url, _ := s.Attr("href") // href属性を取得
			fmt.Println(self.Base_url+detail_url)
		})
		next_btn, _:= doc.Find(self.Next_selector).Attr("href")
		if next_btn == "" {
　　　　　　  // 次ページへのボタンがなくなったら終了
			break
		}
		category_url = self.Base_url+self.parse_next_btn(next_btn)
	}
}

func(self BaseCrawler) fetch_doc(url string) *goquery.Document {
	time.Sleep(1 * time.Second) // 1秒待機
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		panic(err)
	}
	return doc
}

// qiitaから記事のURLを取得できるよう各種変数を用意
func main() {
	base_crawler := new(BaseCrawler)
	base_crawler.Category_url = "https://qiita.com/search?q=go"
	base_crawler.Next_selector = ".js-next-page-link"
	base_crawler.Base_url = "https://qiita.com"
	base_crawler.Detail_selector = ".searchResult_itemTitle > a"
	base_crawler.Main()
}

```

比較的単純な横移動式のサイトならこのままでも使用できますが、スクレイピングしたい対象のサイトに合わせて適宜structを継承し機能を変更・追加していくことで様々なサイトに対応できるようになります。