GitHub Actionsでスクレイピングを定期実行して結果をSlackに通知する

# はじめに
GitHub Actionsで処理を自動化する話です。
Go言語でスクレイピングして結果をSlackに通知します。

# 参考にさせていただいた記事
- Go で Slack Bot を作る (2020年3月版)
 - https://qiita.com/frozenbonito/items/cf75dadce12ef9a048e9
- Goとgoqueryでスクレイピング
 - https://qiita.com/Yaruki00/items/b50e346551690b158a79

# 準備
GitHubとSlackのアカウントが必要です。
Slackはフリープランでスペースを作成し、Botから通知を送るためのチャンネルを作成しておきます。GitHubにはプライベートのリポジトリを作成しておきます（DDoS攻撃などに悪用されないため非公開にする）

# 流れ

以下の順番で作成を進めます。

1. 対象ページから必要な情報を取得する処理の作成
1. Slackアプリの作成とトークンの取得
1. SlackにPOSTする処理の作成
1. GitHub Actionsで処理を定時実行できるようにする

# 対象ページから必要な情報を取得する処理の作成

Go言語でスクレイピングする際の定番パッケージである（ように思われる）`PuerkitoBio/goquery`を使用します。スクレイピングするからには有用でなおかつ毎日更新される情報が欲しいので、Yahoo!ショッピングの日替わりクーポンを取得してみます。https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html このURLがスクレイピングの対象です。

サンプル https://github.com/PuerkitoBio/goquery#examples を参考にしつつtitleタグの中身だけ取得する処理を作成します。


```golang:main.go
package main

import (
        "fmt"
        "log"
        "net/http"

        "github.com/PuerkitoBio/goquery"
)

func main() {
        // Request the HTML page.
        res, err := http.Get("https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html")
        if err != nil {
                log.Fatal(err)
        }
        defer res.Body.Close()

        if res.StatusCode != 200 {
                log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
        }

        // Load the HTML document
        doc, err := goquery.NewDocumentFromReader(res.Body)
        if err != nil {
                log.Fatal(err)
        }

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

ビルドして実行するとタイトルの取得結果を確認することができます。

```bash
$ go mod init
$ go build
$ ./goquery_test
【今日のクーポン】メガネ、くもり止めカテゴリで使える30%OFFクーポン
```

# Slackアプリの作成とトークンの取得

SlackにメッセージをPOSTする処理を作成する前にSlackアプリ(Bot)を作成し、トークンを取得しておく必要があります。

1. https://api.slack.com/apps にアクセスして`Create New App`ボタンを押下
2. `App Name`と`Development Slack Workspace`を入力して`Create App`ボタンを押下
![Screenshot 2021-02-12 at 20.54.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/0c62e946-a11f-5c45-e862-0e19b295438d.png)

3. 左カラムのFeatures配下にある`OAuth & Permissions`をクリック
![Screenshot 2021-02-20 at 11.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/ea7faf2c-00df-ea23-903b-792720414acb.png)

4. `Bot Token Scopes`に`chat:write`を追加（Add an OAuth Scopeをクリックしてリストから選択）
![Screenshot 2021-02-20 at 10.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/99faca03-fccc-da7d-dc5d-241eb7c70bfb.png)

5. 左カラムのSettings配下にある`Install App`をクリック
6. `Install to Workspace`ボタンを押下
![Screenshot 2021-02-20 at 11.13.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/dc845deb-885c-5ded-e77a-1295ca6f6d3a.png)

7.ワークスペースにアプリのインストールが完了するとトークン(OAuth Access Token)が表示されるのでメモしておく。
![Screenshot 2021-02-20 at 11.15.56 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/781c9195-efe2-770b-6e58-98585050fe9e.png)

8.メッセージを受信したいチャンネルに作成したアプリを追加しておく。ショートカットを開いて`invite`と入力、表示されたリストから`このチャンネルにアプリを追加する`を選択する。
![Screenshot 2021-02-20 at 13.38.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/e543fceb-2638-331e-4ae5-ab381f761d49.png)

# SlackにPOSTする処理の作成

続いてSlackにPOSTする処理を作成します。Qiitaの多くの記事にも書かれている`slack-go/slack`パッケージを使います。examples以下を探したところ単純にメッセージを送信するサンプルを見つけることができました。https://github.com/slack-go/slack/tree/master/examples/messages こちらをベースに作成します。

2箇所書き換えが必要です。
`YOUR_TOKEN_HERE` : メモしておいた`OAuth Access Token`に書き換え
`CHANNEL_ID` : メッセージを受信したいチャンネル名かチャンネルIDを指定


```golang:main.go
package main

import (
	"fmt"

	"github.com/slack-go/slack"
)

func main() {
	// YOUR_TOKEN_HEREをメモしておいたトークンに置き換える
	api := slack.New("YOUR_TOKEN_HERE")
	attachment := slack.Attachment{
		Pretext: "some pretext",
		Text:    "some text",
		// Uncomment the following part to send a field too
		/*
			Fields: []slack.AttachmentField{
				slack.AttachmentField{
					Title: "a",
					Value: "no",
				},
			},
		*/
	}

	channelID, timestamp, err := api.PostMessage(
		"CHANNEL_ID", // メッセージを送信したいチャンネルを指定する
		slack.MsgOptionText("Some text", false),
		slack.MsgOptionAttachments(attachment),
		slack.MsgOptionAsUser(true), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot
	)
	if err != nil {
		fmt.Printf("%s\n", err)
		return
	}
	fmt.Printf("Message successfully sent to channel %s at %s\n", channelID, timestamp)
}
```

ビルドして実行するとSlackにメッセージが送信されます。

```bash
$ go mod init
$ go build
$ ./slack_bot
Message successfully sent to channel XXXXXXXXXXXXX at 1613796836.000500
```
![Screenshot 2021-02-20 at 13.55.29 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/2a1c18f4-1757-8f83-fd2b-804af43f1c46.png)
`not_in_channel`とエラーが出力された場合にはチャンネルにアプリを追加して下さい。

# GitHub Actionsで処理を定時実行できるようにする

まずは動作確認用のプライベートリポジトリを作成しておく。`Hello, World`を出力するだけの処理を作り`go.mod`も含めてリポジトリに追加しておく。

```golang:main.go
package main

import "fmt"

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

Actionsタブをクリックするとそのリポジトリに適したワークフローテンプレートがサジェストされます。サジェストされたGo言語用のテンプレートをベースにしつつワークフローを作成します。cronによる定期実行だけではなく手動で実行するため`workflow_dispatch`を登録しておくと確認が楽になります。`schedule`イベントはcron形式で記述できますがUTC時間なので注意が必要です。
詳しくはコチラ https://docs.github.com/ja/actions/reference/events-that-trigger-workflows

作成したワークフローファイルは、リポジトリの`.github/workflows`ディレクトリに登録します。ワークフローを手動実行するか設定した時刻になれば結果を確認できるはずです。ワークフローの手動実行方法 https://docs.github.com/ja/actions/managing-workflow-runs/manually-running-a-workflow

```yaml:cron.yml
name: Go

on:
  workflow_dispatch:
  schedule:
    - cron: '5 15 * * *'

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.15

    - name: Build
      run: |
        mkdir /home/runner/work/dist
        go build -v -o /home/runner/work/dist/myapp ./...
    - name: RunMyApp
      run: /home/runner/work/dist/myapp
```

# まとめ+α

仕上げとしてスクレイピング＋Slack送信の処理を作成します。さらにトークンはシークレットから取得するようにしておきます。シークレットはリポジトリの`Settings`タブの`secrets`から登録する。下に掲載したサンプル(main.go)ではトークンを`SLACK_TOKEN`にしています。

```golang:main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/PuerkitoBio/goquery"
    "github.com/slack-go/slack"
)

func mustGetenv(k string) string {
    v := os.Getenv(k)
    if v == "" {
        log.Panic("env not set.")
    }
    return v
}

var token string

func init() {
    token = mustGetenv("SLACK_TOKEN")
}

func main() {
    // Request the HTML page.
    res, err := http.Get("https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/index.html")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    if res.StatusCode != 200 {
        log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
    }

    // Load the HTML document
    doc, err := goquery.NewDocumentFromReader(res.Body)
    if err != nil {
        log.Fatal(err)
    }

    title := doc.Find("title").Text()

    api := slack.New(token)

    attachment := slack.Attachment{
        Text: "https://topics.shopping.yahoo.co.jp/campaign/cate_coupon/",
    }

    channelID, timestamp, err := api.PostMessage(
        "CHANNEL_ID",
        slack.MsgOptionText(title, false),
        slack.MsgOptionAttachments(attachment),
        slack.MsgOptionAsUser(true),
    )
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Message successfully sent to channel %s at %s\n", channelID, timestamp)
}
```

ワークフローにもシークレットを取得してセットする処理を追加しておきます。（追加した前後だけ抜粋）

```yaml:cron.yml
name: Go

env:
  SLACK_TOKEN: ${{secrets.SLACK_TOKEN}}

on:
  workflow_dispatch:
  schedule:
    - cron: '5 15 * * *'
```

処理や設定に問題がなければ以下のように毎日通知が届くようになります。
![Screenshot 2021-02-20 at 22.41.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129243/0fb78a68-b457-9a6d-b2ea-9058164f1580.png)

# 最後に

GitHub Actionsで何か自動化してみたかったのでスクレイピングしてSlackに通知をしてみましたが、スクレイピングする場合にはルールとマナーを守りましょう。