trifle

技術メモ

Go で Web サイト監視ツールを作るときのメモ

最近 Web サイトのコンテンツ更新を自動で検知したいという欲が発生したので、監視ツールを作成しました。
そこで初めて Go を使ったのですが、ライブラリが充実しており、並行処理が書きやすく、使いやすかったです。今後また Go で何か作りたい時のためにメモ書きを残しておきます。

作ったもの

github.com

含まれる要素としては、

  • PostgreSQL で既に検知しているコンテンツの情報を保存
  • goquery で t7s.jp の色々なページをスクレイピング、新しいコンテンツが含まれていないかチェック
  • go-githubGitHub API を操作し、新しいコンテンツがあったらその名前で Issue を立てる
  • Heroku で定期実行

PostgreSQL

Heroku のアドオンとして入れます。

$ heroku addons:create heroku-postgresql:hobby-dev

そうしたら

$ heroku config
DATABASE_URL: ZZZZZZZZZZZZZZZZ

というように DATABASE_URL が取れます。ローカルで試すときはこの URL を使うといいです。本番では os.Getenv("DATABASE_URL") とします。
あらかじめテーブルを作成するのはコマンドラインでできます。

$ heroku pg:psql

でデータベースに接続して SQL を打てます。これは普通に history が残っていて昔の SQL とかもさかのぼれるのは意外と便利です。
今回は CREATE TABLE issues (title TEXT); したものとします。すなわち、title という列だけを持つ issues というテーブルを作り、これにコンテンツ情報を一行ずつ入れていきます。

Go でデータベースに接続するときは

db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
    panic(err)
}
defer db.Close()

でできます。
defer は関数終了時まで実行されないという go 特有のシンタックスで、初めて defer を知ったときは何のために必要なのかよくわかりませんでしたが、今回ようやくメリットが分かりました。

db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))

// クソ長い処理

db.Close()

という状況で close を忘れそうになるのを防止できます。Pythonwith に似たものを感じますが、defer はインデントが下がらないのでさらに良いですね。

テーブルの中身を Go の配列にするのは

row, err := db.Query("SELECT title FROM issues")
if err != nil {
    panic(err)
}

var issueTitles []string
var issueTitle string
for row.Next() {
    err := row.Scan(&issueTitle)
    if err != nil {
        panic(err)
    }
    issueTitles = append(issueTitles, issueTitle)
}

こんな感じで書けます。Next() で行ごとのイテレータScan() で Go のデータに落とし込み、です。今回は ORM ライブラリを使うまでもないですが、ORM を使いたいときは別にライブラリを入れるのを検討した方が良さそうです。ライブラリの選定記事↓

qiita.com

スクレイピング

goquery でおkです。

qiita.com

GitHub API

G社が提供している go-github が網羅的です。

developer.github.com

godoc.org

とにかくこの godoc とニラメッコしていればよく、godoc を誤読しないようにしましょう(これが言いたかっただけ)。

あらかじめ GitHubaccess_token を Heroku の環境変数として登録しておきます。

heroku config:set GITHUB_TOKEN=YYYYYYYYYYYYYYYYY

まず OAuth 認証は

token := os.Getenv("GITHUB_TOKEN")
ts := oauth2.StaticTokenSource(
    &oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(oauth2.NoContext, ts)
client := github.NewClient(tc)

でできます。Context というのがよく分かりませんが(https://blog.golang.org/context に書いてあるっぽい?)今回の用途では明らかに不要なので NoContext でよい気がします。
今回は Issue 作成の API を使いました。

opt := &github.IssueRequest{
    Title: github.String(title),
    Body:  github.String(link),
}

_, _, err := client.Issues.Create(oauth2.NoContext, "t7s-2034", "info", opt)
if err != nil {
    panic(err)
}

これで、t7s-2034 というアカウントの info というリポジトリに、title 変数の文字列をタイトル、link 変数の文字列を本体のコメントとして、 Issue を新規作成できます。

並行処理

Web ページをスクレイピングして、その情報をもとに GitHub API を操作するというのは、時間のかかる行為です。複数のページで順番にそれをやっていると大変です。なので、これを並行処理できるとうれしいです。
Go の並行処理はめちゃくちゃ学ぶことがありそうで、全然何もわかっていないですが、とりあえず今回の用途としては sync.WaitGroup を使うとよさそうでした。

qiita.com

今回は、ニュースページをスクレイピングして、新しい情報があったら Issue を立てる関数 checkNews() を用意しました。また別にCD発売ページをスクレイピングする checkCD()、他にも checkRelease()checkUnit() を作ってみました。これらを並行して走らせるのは

var wg sync.WaitGroup
wg.Add(4)
go checkNews(client, db, issueTitles, &wg)
go checkRelease(client, db, issueTitles, &wg)
go checkCD(client, db, issueTitles, &wg)
go checkUnit(client, db, issueTitles, &wg)
wg.Wait()

という感じでできます。
Node.js で近いのは Promise.all でしょうか。
これはかなりC言語とかでやるようなシステムプログラミングっぽいです。やってることは、まず wg というカウンターを用意していて、最初にカウンターに4を入れます。それぞれのスクレイピング関数の最後に wg.Done() を書いておきます。これはカウンターを1減らす関数です。あとは、wg.Wait() という、カウンターが0になるまで待つ関数を置くだけです。
なお、今回は使いませんでしたが、もし結果を checkNews(), checkRelease()... の順を保ったまま取り出したいときは Go の Channel とかを使うことになるはずです、多分。

qiita.com


初めて今回のツールを動かしたとき(すなわち、データベースに何も情報がなく、スクレイピングしたコンテンツが全て新しい情報で、その結果毎回 Issue を立てることになる)は、実行画面は、こんな感じでした。

f:id:HelloRusk:20190831022111p:plain

それぞれの関数が並行して動いた結果、Issue 作成もバラバラにできていることが分かります。

Heroku でのデプロイ

意外とよくわからなかったのがデプロイの仕方でした。npm で普通にできるようなことが、go ではできません。

qiita.com

govendor というのを使ってみましたが、本当に良いツールなのかはよくわかりません。
Go Modules というのが今後デファクト・スタンダードになっていくとすると、この辺りは変わるかもしれません。