※ Retty Advent Calendar 2019 - Qiita 7日目の記事です。
昨日は渡邉さんの記事で、あなたのエリアは何処から? ~地理空間クラスタリングとの差分検証~ - Retty Tech Blog でした。
はじめに
技術部の李です。Gopherです。Goが大好きです。
日々の業務でWebページから何かしらの数字とグラフを取得することはよくあることです。
その中で定期的に取得する必要があるようなことも少なくはないでしょう。
そんなこと、エンジニアだったら誰でも自動化したくなるはずです。
プログラムでブラウザを起動し、指定したWebページにアクセスして情報を取得する方法はたくさんあります。
一番知られているのは SeleniumHQ Browser Automation や Puppeteer を利用することでしょう。
でも、Goが大好きの私はもちろんGoを使わなければいけないのです。
ここで本題に入ります。
chromedp
chromedpは Chrome DevTools Protocol をサポートしているブラウザをコントロールできるGoのパッケージです。
Chrome DevTools は Chrome DevTools Protocol をサポートしているため、chromedpを使えば、GoでChromeを完全にコントロールすることは可能です。
chromedpをはじめて使う場合は examples をまず見てみることをおすすめします。そちらでよく使うActionの例は大体あげらています。
なぜchromedp
- 開発において
- Goが使える(Goの良いところは全部持ってくるので、Goが使えるだけで十分なんですね。)
- 純粋なGoの実装なので、Cなど他のライブラリーへの依存がなく、インストールが楽
- chromedpを利用して開発したツールとして
chromedpの使い方
まず、contextを理解する
chromedpはGoのcontextを活用した実装になっています。
func main() { ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() if err := chromedp.Run(ctx); err != nil { log.Fatal(err) } }
上記は一番簡単な例です。
chromedp.NewContext
でcontextを初期化する。chromedp.Run
にcontextを渡してChromeのプロセスを起動する。- contextの
cancel
が呼ばれてChromeのプロセスを終了する。
chromedpのcontextはGoの標準のcontextなので、timeout, cancelなどの制御も普通のGoのプラグラムと同じように簡単にできます。
そして、headlessモードを理解する
chromedpはデフォルトで headlessモード で起動するので、上の例だと、何も見えずに、プログラムが終了してしまいます。
(headlessモード:名前の通り、UIが起動せずに、バックグラウンドでChromeのプロセスを起動するということです。)
ChomeをUI付きで起動する方法もあります。
func newChromedpContext(ctx context.Context, headless bool) (context.Context, context.CancelFunc) { var opts []chromedp.ExecAllocatorOption for _, opt := range chromedp.DefaultExecAllocatorOptions { opts = append(opts, opt) } if !headless { opts = append(opts, chromedp.Flag("headless", false), chromedp.Flag("hide-scrollbars", false), chromedp.Flag("mute-audio", false), ) } allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...) ctx, cancel := chromedp.NewContext(allocCtx) return ctx, func() { cancel() allocCancel() } }
DefaultExecAllocatorOptions
(デフォルトでheadlessモードになってる起動オプション)を取ってくる。- headlessモードオプションを全部オフにして追加する。
NewExecAllocator
/NewContext
で新しいheadlessモードがオフになっているcontextを初期化する。
最初の例でこちらのnewChromedpContext
を使ってcontextを初期化すれば、一瞬Chromeが立ち上がることが見れると思います。
最後に、Actionを理解する
func main() { ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() if err := chromedp.Run(ctx, chromedp.Navigate("https://retty.me/"), chromedp.Sleep(time.Second), ); err != nil { log.Fatal(err) } }
chromedp.Run
はcontext以外、複数のchromedp.Action
も渡せます。
渡されたActionは順番にChrome上で実行されます。
上記の例だと、「https://retty.me/ を開いて、1秒待つ」のように実行されます。
type Action interface { Do(context.Context) error }
chromedp.Action
は上記のように定義されたinterfaceなので、必要に応じてカスタマイズのActionも簡単に作れます。
すでに用意してくれているのはfunctionで便利に定義できるchromedp.ActionFunc
と複数のActionを集合として定義できるchromedp.Tasks
です。
よく使うAction
基本なActionと実装例は examples を確認してください。
その中、いくつか紹介したいと思います。
ページの指定したエリアのキャプチャーを取ってファイルに保存する
var CaptureAndSaveAction = chromedp.ActionFunc(func(ctxt context.Context) error { var buf []byte tasks := chromedp.Tasks{ emulation.SetDeviceMetricsOverride(1680, 2048, 0, false), chromedp.CaptureScreenshot(&buf), } if err := tasks.Do(ctxt); err != nil { return err } if err := ioutil.WriteFile("capture.png", buf, 0644); err != nil { return err } return nil })
* emulation - "github.com/chromedp/cdproto/emulation"
(SetDeviceMetricsOverride
以外もいくつか便利なfunctionが用意されているので、詳細は Go Doc を確認してください。)
Cookieを取得/設定する
サイトによってログインなどのActionを短時間でやり過ぎるとBANされることがあるので、ログインした後のCookieを保存しておいて、再度アクセス際にCookieを設定することをよくやります。
ここで"github.com/chromedp/cdproto/network"
を利用します。
- 取得する
var GetCookiesAction = chromedp.ActionFunc(func(ctx context.Context) error { cookies, err := network.GetAllCookies().Do(ctx) if err != nil { return err } log.Printf("cookies got: %v", cookies) // e.g. save to file return nil })
- 設定する
var SetCookiesAction = func(cookies []*network.Cookie) chromedp.Action { return chromedp.ActionFunc(func(ctx context.Context) error { cc := make([]*network.CookieParam, 0, len(cookies)) for _, c := range cookies { cc = append(cc, &network.CookieParam{ Name: c.Name, Value: c.Value, Domain: c.Domain, }) } return network.SetCookies(cc).Do(ctx) }) }
ページの中の指定したリクエストのURLを取得する
Chrome DevToolsを使う際に、Networkパネルを開いて、ある文字列が含まれているのリクエストのURLを取得することはよくあるでしょう。
例えば、ページ内の.cssファイルのURLを集めるとしましょう。
ここでnetwork.Enable
Actionを使います。
func main() { ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() chromedp.ListenTarget(ctx, func(ev interface{}) { switch ev := ev.(type) { case *network.EventRequestWillBeSent: url := ev.Request.URL if strings.HasSuffix(url, ".css") { log.Printf("css got: %v", url) } } }) if err := chromedp.Run(ctx, network.Enable(), chromedp.Navigate("https://retty.me/"), chromedp.Sleep(time.Second), ); err != nil { log.Fatal(err) } }
* network.Enable
Actionはnetworkパネルの機能を有効にしてくれるので、chomedp.Navigate
など他のActionの前に渡す必要がある、そうでなければ、networkパネルが無効のままなので、chromedp.ListenTarget
は何も受け取れません。
おわりに
早速chromedpを使ってみたくなりましたでしょうか?
Webページで情報集める時の自動化、chromedpの存在はGopherとして本当嬉しいことです。
こうしてみんなでたくさんの自動化ツールを作って共有し合うのも非常に楽しいです。
最後に、今RettyでgPRCのサービスを日々Goで楽しく開発しています。Goが好きな仲間が増えるととても嬉しいので、興味ある方はぜひ!!
おまけ
動画の通り、この記事はchromedpで自動的に書かれたものでした。