フロントエンジニア（仮）がGO言語で簡単なスクレイピング処理を実装するまで

はじめまして。kenalog210と申します。
ある事情によりGo言語でのスクレイピング処理方法を色々調べてたんですが、無事お流れとなりました。
もったいないのでQiitaに上げさせていただきます。
今回は下記のサイトをサンプルとして使用させて頂きました。
https://wiki.hackerspaces.org/
<h1>使用したフレームワーク</h1>
Colly
http://go-colly.org/
https://github.com/gocolly/colly

スターが１万近くついてるスゴイやつです。
リポジトリにサンプルがいっぱいあるので、基本的なケースなら自分の応用したいパターンに合わせて
サンプルをちょっといじくり回せば使えそうです。
この記事もサンプルをちょっと応用しただけのものです。
<h1>やりたかったこと</h1>
・マッピング情報を渡したらうまいことインスタンス化してくれるようなものを組みたい
・インスタンス化出来たら、JSON文字列化してコンソールで出力
・構造体とのマッピング用スクリプトはめんどいので組みたくない

<h1>構造体を定義</h1>
まずは型定義を用意してやります。下記のようにフィールド定義と紐付けたい要素を簡単に定義できます。
ちなみに、Go言語は先頭文字が小文字か大文字かでPrivateかPublicかを定義するようなので
フィールド名の先頭を大文字にしてやらないとちゃんとパースしてくれません。（１敗）

```Go
/*
	パース処理対象の構造体
	型名指定の後に、JQueryと同じ要領でフィールドと紐付けたい要素や属性をセレクタで指定
*/
type hackerspacesWiki struct {
	// 注釈
	Note string `selector:"pre"`
	// 紹介文
	Description []string `selector:"p"`
	// 右側のサムネ画像一覧
	ThumbnailImageUrls []string `selector:".thumbimage" attr:"src"`
}
```

<h1>クローラー、パース対象の構造体のインスタンスを生成</h1>
下記のようにインスタンスを生成しました。クローリング

```Go
	// クローラーインスタンスを生成
	c := colly.NewCollector(
		// クローリングを許可するドメインを設定
		colly.AllowedDomains("wiki.hackerspaces.org"),
	)
	// パース対象のインスタンスを生成
	parseTargetStruct := &hackerspacesWiki{}
```

<h1>クローラーの各イベント処理を定義</h1>
今回はOnHTMLイベントとOnRequestイベントのみ使用しました。

```Go
    /*
	   セレクタで指定した要素が存在した場合の処理
	   第１引数にJQueryと同じ要領でセレクタを指定可能

	   めんどくさいのでルート要素を指定して一括パースをかける
	*/
	c.OnHTML(".mw-parser-output", func(e *colly.HTMLElement) {
		/*
			OnHTMLで指定したセレクタに適合した要素が引数で入ってくるため
			Unmarshalメソッドを使用してパース対象の型のインスタンスを渡してやれば
			構造体に指定したセレクタに従って自動的にマッピングしてくれる
		*/
		e.Unmarshal(parseTargetStruct)
	})

	// リクエスト実行時（Visitメソッド実行時）に実行される処理
	c.OnRequest(func(r *colly.Request) {
		fmt.Println("Visiting", r.URL.String())
	})
```

<h1>スクレイピング処理開始</h1>
Visitメソッドを実行すると OnRequest →　OnHTML の順で処理が流れます。

```Go

	// スクレイピング処理を開始
	c.Visit("https://wiki.hackerspaces.org/")
```

<h1>JSON文字列化　→　コンソール出力</h1>
```Go

	// マッピング後のインスタンスをJSON文字列化
	bs, err := json.Marshal(parseTargetStruct)
	if err != nil {
		// 申し訳程度のエラー処理
		fmt.Println("JSON Parse Error")
		os.Exit(1)
	}

	// コンソールに出力
	fmt.Println(string(bs))
```

<h1>実行結果</h1>
こんな形でコンソールに出力されました。
対象要素が複数存在する場合は、構造体の方で配列として定義してやらないとうまく取得してくれないです。

```Go
Visiting https://wiki.hackerspaces.org/

{
    "Note": "Note: We are currently updating the wiki. If you notice something you want to tell us, join #hackerspaces on Freenode IRC network.",
    "Description": [
        "Hackerspaces are community-operated physical places, where people can meet and work on their projects.",
        "This website is for Anyone and Everyone who wants to share their hackerspace stories and questions with the global hackerspaces community.",
        "Have some free time? Help with the Hackerspace Census 2019 and find out if your local hackerspaces listed here are still active!"
    ],
    "ThumbnailImageUrls": [
        "/images/e/ec/Cbase07.jpg",
        "/images/0/0f/NYCR2.JPG",
        "/images/7/73/Hackerspace_charlotte.jpg"
    ]
}
```

<h1>ソース全文</h1>

```Go
package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/gocolly/colly/v2"
)

/*
	パース処理対象の構造体
	型名指定の後に、JQueryと同じ要領でフィールドと紐付けたい要素や属性をセレクタで指定
*/
type hackerspacesWiki struct {
	// 注釈
	Note string `selector:"pre"`
	// 紹介文
	Description []string `selector:"p"`
	// 右側のサムネ画像一覧
	ThumbnailImageUrls []string `selector:".thumbimage" attr:"src"`
}

func main() {
	// クローラーインスタンスを生成
	c := colly.NewCollector(
		// クローリングを許可するドメインを設定
		colly.AllowedDomains("wiki.hackerspaces.org"),
	)
	// パース対象のインスタンスを生成
	parseTargetStruct := &hackerspacesWiki{}

	/*
	   セレクタで指定した要素が存在した場合の処理
	   第１引数にJQueryと同じ要領でセレクタを指定可能

	   めんどくさいのでルート要素を指定して一括パースをかける
	*/
	c.OnHTML(".mw-parser-output", func(e *colly.HTMLElement) {
		/*
			OnHTMLで指定したセレクタに適合した要素が引数で入ってくるため
			Unmarshalメソッドを使用してパース対象の型のインスタンスを渡してやれば
			構造体に指定したセレクタに従って自動的にマッピングしてくれる
		*/
		e.Unmarshal(parseTargetStruct)
	})

	// リクエスト実行時（Visitメソッド実行時）に実行される処理
	c.OnRequest(func(r *colly.Request) {
		fmt.Println("Visiting", r.URL.String())
	})

	// スクレイピング処理をスタート
	c.Visit("https://wiki.hackerspaces.org/")

	// マッピング後のインスタンスをJSON文字列化
	bs, err := json.Marshal(parseTargetStruct)
	if err != nil {
		// 申し訳程度のエラー処理
		fmt.Println("JSON Parse Error")
		os.Exit(1)
	}

	// コンソールに出力
	fmt.Println(string(bs))
}

```
<h1>感想</h1>
Collyが使いやすくてスゴイ。