Goでスクレイピングをやってみる

みなさんスクレイピングを知っていますか？

スクレイピングとは、webサイトのHTML要素をプログラムを使って取得することです。

今回は、以下のスクレイピング練習サイトでGoの`colly`というライブラリを使って、スクレイピングを行ってみました。

https://books.toscrape.com/


# スクレイピングを行う際の注意事項
スクレイピングは、注意して行わないと違法になる可能性があります。
今回、自分が調べられる範囲で注意しなければならないことを調べてみました。

### まず利用規約を確認する
利用規約には、スクレイピングを禁止しているサイトがいくつかあるそうです。
例えば、`Amazon`の利用規約には以下のような文言があります。
>この利用許可には、アマゾンサービスまたはそのコンテンツの転売および商業目的での利用、製品リスト、解説、価格などの収集と利用、アマゾンサービスまたはそのコンテンツの二次的利用、第三者のために行うアカウント情報のダウンロードとコピーやその他の利用、データマイニング、ロボットなどのデータ収集・抽出ツールの使用は、一切含まれません。

`ロボットなどのデータ収集・抽出ツールの使用は、一切含まれません。`という部分がスクレイピングを禁止していることを表しています。

このようなスクレイピングを禁止している背景としては、スクレイピングをプログラムで行うことで短期間のうちに多くのリクエストを行うことになり、`Dos攻撃`に値してしまう可能性があるからだそうです。

またQiitaでも、https://help.qiita.com/ja/articles/qiita-api に以下のように記述されています。
>スクレイピングはサーバーの負荷上昇への懸念があるため、Qiitaへのスクレイピングは許可しておりません。
と記述されています。

気をつけましょう。

### robots.txtを確認する
robots.txtには、ロボット（いわゆるbot)にクロールされたくないコンテンツが指定されています.

Qiitaだと
```
https://qiita.com/robots.txt
```
で確認することができます。

確認してみると、、、

```
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
User-agent: *
Disallow: /*/edit$
Disallow: /api/*
Disallow: /graphql$
Disallow: /policies/td-optout$
Disallow: /search
Disallow: *.md
Disallow: */items/*/revisions
Disallow: */private/*/revisions
Allow:    /api/*/docs$

Sitemap: https://cdn.qiita.com/sitemap-https/sitemap.xml.gz
```
と書かれていました。

`User-Agent`とは、一般的にユーザーが使用しているOSやブラウザを指します。(クライアントって認識で良いと思います。
`User-agent: *`は、すべてのクライアントを指定しています。

`Diallow`では、参照できないコンテンツを指しています。
逆に`Allow`では、参照できるコンテンツを指定しています。

このrobots.txtで許可されていないコンテンツをスクレイピング行わないようにしましよう。

# collyを使ってみる

### インストール
インストールをまず行います。
`go mod init`でモジュールを初期化します。
```
go mod init "github.com/maooz4426/go_scraping"
```
>モジュール名はあくまで一例です

その後、`go get`コマンドで`colly`をインストールします。
```
go get -u github.com/gocolly/colly/...
```
### Titleタグの内容を取得するコードを書いてみる。
`main.go`にqiitaのHPのタイトルを取得するコードを書いてみます.

```go:main.go
package main

import (
	"fmt"
	"github.com/gocolly/colly"
	"log"
)

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

	url := "https://books.toscrape.com/"

	c.OnError(func(_ *colly.Response, err error) {
		log.Println("Something went wrong:", err)
	})

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

	c.Visit(url)

}

```

`go run main.go`で実行してみます。
```
go run main.go
```

結果は下のようになりました。
```
go run main.go

    All products | Books to Scrape - Sandbox
```

デベロッパーツールでは、HTMLは以下の通りになっていたので、コンテンツが取得できていることがわかります。
```html
<title>
    All products | Books to Scrape - Sandbox
</title>
```

### タグを指定して取得する
productのタイトルを指定して取得してみようと思います。
サイトで言うと本の表紙と思われる画像の下にある`A Light in the ...`とかがtitleに当たると思うので、それを指定して取得してみようと思います。
![スクリーンショット 2024-08-13 0.09.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/bd7e2852-c0c6-7a2e-f2c3-20436226244b.png)

該当部分のhtmlは下のようになっていました。
```html
<article class="product_pod">
    <div class="image_container">
        <a href="catalogue/a-light-in-the-attic_1000/index.html">
            <img src="media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg" alt="A Light in the Attic" class="thumbnail">
        </a>
    </div>

    <p class="star-rating Three">
        <i class="icon-star"></i>
        <i class="icon-star"></i>
        <i class="icon-star"></i>
        <i class="icon-star"></i>
        <i class="icon-star"></i>
    </p>

    <h3>
        <a href="catalogue/a-light-in-the-attic_1000/index.html" title="A Light in the Attic">A Light in the ...</a>
    </h3>

    <div class="product_price">
        <p class="price_color">£51.77</p>
        <p class="instock availability">
            <i class="icon-ok"></i>
            In stock
        </p>

        <form>
            <button type="submit" class="btn btn-primary btn-block" data-loading-text="Adding...">Add to basket</button>
        </form>
    </div>
</article>
```
articleタグ内のaタグを取得できれば良さそうですね。

それを取得するコードは以下のようになります。

```go:main.go
package main

import (
	"fmt"
	"github.com/gocolly/colly"
	"log"
)

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

	url := "https://books.toscrape.com/"

	c.OnError(func(_ *colly.Response, err error) {
		log.Println("Something went wrong:", err)
	})

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

	c.Visit(url)

}

```
`c := colly.NewCollector()`でインスタンスを作成し、それにメソッドを指定することでスクレイピングを行えます。

OnHTMLメソッドの第一引数に`article a[title]`のような感じで書くことで、articleタグ内のaタグというように取得できるので、そのメソッド内でe.Textによりスクレイピングの結果となるコンテンツを取得できます。


これを実行してみると
```
go run main.go
```
下のようにtitleが取得できました。
```
A Light in the ...
Tipping the Velvet
Soumission
Sharp Objects
Sapiens: A Brief History ...
The Requiem Red
The Dirty Little Secrets ...
The Coming Woman: A ...
The Boys in the ...
The Black Maria
Starving Hearts (Triangular Trade ...
Shakespeare's Sonnets
Set Me Free
Scott Pilgrim's Precious Little ...
Rip it Up and ...
Our Band Could Be ...
Olio
Mesaerion: The Best Science ...
Libertarianism for Beginners
It's Only the Himalayas
```

### classを指定して取得する
先ほどのHTML要素の`price_color`クラスに値段らしきものが書いてあったのでそれを取得してみようと思います。

それを取得するコードは以下のようになります。

```go:main.go
package main

import (
	"fmt"
	"github.com/gocolly/colly"
	"log"
)

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

	url := "https://books.toscrape.com/"

	c.OnError(func(_ *colly.Response, err error) {
		log.Println("Something went wrong:", err)
	})

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

	c.Visit(url)

}

```

結果は下のように取得できました！
```
£51.77
£53.74
£50.10
£47.82
£54.23
£22.65
£33.34
£17.93
£22.60
£52.15
£13.99
£20.66
£17.46
£52.29
£35.02
£57.25
£23.88
£37.59
£51.33
£45.17
```

### 複数の要素を指定して取得してみる
今までは本のタイトルと値段をそれぞれ取得していたので、それを同時に取得してリストのように取得するようにしてみました。

```go:main.go
package main

import (
	"fmt"
	"github.com/gocolly/colly"
	"log"
)

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

	url := "https://books.toscrape.com/"

	c.OnError(func(_ *colly.Response, err error) {
		log.Println("Something went wrong:", err)
	})

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

	c.OnHTML("article", func(e *colly.HTMLElement) {
		title := e.DOM.Find("a").Text()
		price := e.DOM.Find(".price_color").Text()
		fmt.Println("Title:", title)
		fmt.Println("Price:", price)
	})

	c.Visit(url)

}
```
`DOM.Find()`を使うことで複数の要素を指定できるようになります。

これを実行してみると
```
Title: A Light in the ...
Price: £51.77
Title: Tipping the Velvet
Price: £53.74
Title: Soumission
Price: £50.10
Title: Sharp Objects
Price: £47.82
Title: Sapiens: A Brief History ...
Price: £54.23
Title: The Requiem Red
Price: £22.65
Title: The Dirty Little Secrets ...
Price: £33.34
Title: The Coming Woman: A ...
Price: £17.93
Title: The Boys in the ...
Price: £22.60
Title: The Black Maria
Price: £52.15
Title: Starving Hearts (Triangular Trade ...
Price: £13.99
Title: Shakespeare's Sonnets
Price: £20.66
Title: Set Me Free
Price: £17.46
Title: Scott Pilgrim's Precious Little ...
Price: £52.29
Title: Rip it Up and ...
Price: £35.02
Title: Our Band Could Be ...
Price: £57.25
Title: Olio
Price: £23.88
Title: Mesaerion: The Best Science ...
Price: £37.59
Title: Libertarianism for Beginners
Price: £51.33
Title: It's Only the Himalayas
Price: £45.17
```

このように本のタイトルと値段を同時に取得できます。

# 最後に
スクレイピングは、色々な開発に使えると思いますが、注意しなければならない技術でもあります。
わかる範囲でいいので注意して使っていきましょう。