サンプルを動かして理解するchromedp (GoでChromeを操作する)
動機
-
Puppeteer
でJavaScriptの実行込みで簡単なWebスクレイピングをしたいと思ったが、Nodeよくわからん。 -
chromedp
ってのがPuppeteer
と同じことができるGo言語のライブラリなので触ってみたいが、「なんでGoでJavaScriptが動くの??」ってくらいに理解がない(※この理解は間違っている)。 - とりあえず、動作イメージを掴む。
※WebScrapingは用法用量を守って正しくお使いください。Webサイトの利用規約を遵守してください。
chromedpとは
- chromedp/chromedp: A faster, simpler way to drive browsers supporting the Chrome DevTools Protocol.
-
chromedp
は Chromeを操作する ためのGo言語のパッケージです。-
chromedp
そのものがChromeを再現しているわけではなく、既存のChromeに対してGo言語で指示を出せるということです。
-
- デフォルトでheadlessモード(UIのウィンドウなし)のChromeを用います。
-
Chrome DevTools Protocol(CDP)
というプロトコルを用いてChromeを操作します。 -
chromedp
と同様にCDPをサポートするライブラリにNode(JavaScript)製のPuppeteer
があり、こちらはChromeチームが公式にサポートしているライブラリです。
なにができるの?
- Webページのスクレイピング
- Webページのスクリーンショットを取る
など。
なにが嬉しいの?
最近のWebサイトは、JavaScriptを用いてクライアント側でゴリゴリ処理してその結果がレンダリングされてページ内容を形作ることが多いです。Chromeそのものをレンダリングエンジンとして用いることで、普段のブラウジングと同様にレンダリングをしてくれて、当然JavaScriptも実行してくれて、その結果のDOMに対してスクレイピングを行うことができます。
一方で、JavaScriptの結果に左右されないようなWebページのスクレイピングの場合は、レンダリングが不要なことが多いですので、DOMパーサーを使ったほうが動作が速いかもしれません。
この記事の実行環境
- macOS Catalina 10.15.7
- go 1.15.7
- github.com/chromedp/chromedp v0.6.5
- Google Chrome 87.0.4280.141(Official Build) (x86_64)
- 普段使っているGoogle Chrome
サンプル1 GoDocのExecAllocator example
chromedpのGoDocの中にはいくつかのExampleコードがあります。
まずはExecAllocatorのExampleの内容を実行してみます。
package main
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/chromedp/chromedp"
)
func main() {
dir, err := ioutil.TempDir("", "chromedp-example")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.DisableGPU,
chromedp.UserDataDir(dir),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
// also set up a custom logger
taskCtx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
defer cancel()
// ensure that the browser process is started
if err := chromedp.Run(taskCtx); err != nil {
panic(err)
}
path := filepath.Join(dir, "DevToolsActivePort")
bs, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
lines := bytes.Split(bs, []byte("\n"))
fmt.Printf("DevToolsActivePort has %d lines\n", len(lines))
}
実行してみましたが、処理しているのかどうかさっぱりわかりません。
$ go run 01_exec_allocator.go
DevToolsActivePort has 2 lines
が、実は1個だけ変化がありました。実行時に、macOSのDockでChromeアイコンが飛び跳ねていました。
とりあえず、 起動には成功した と見て良さそうです。
サンプル2 Webスクレイピングする
起動しただけだとつまらないので、Webページの内容を読み取ってみたいと思います。今回は個人的に分かりやすかったScrape the Web Faster, in Go with Chromedp | by Lewis Fairweather | ITNEXTの内容に従って github.com のH1タグを読み取ってみます。
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/chromedp"
)
func main() {
// create chrome instance
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()
// create a timeout
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()
start := time.Now()
// navigate to a page, wait for an element, click
var res string
err := chromedp.Run(ctx,
emulation.SetUserAgentOverride("WebScraper 1.0"),
chromedp.Navigate(`https://github.com`),
// wait for footer element is visible (ie, page is loaded)
chromedp.ScrollIntoView(`footer`),
// chromedp.WaitVisible(`footer < div`),
chromedp.Text(`h1`, &res, chromedp.NodeVisible, chromedp.ByQuery),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("h1 contains: '%s'\n", res)
fmt.Printf("\n\nTook: %f secs\n", time.Since(start).Seconds())
}
※ 元の記事では chromedp.WaitVisible(
footer < div)
はコメントアウトされていませんが、これがあると先に進まなかったのでコメントアウトしています。
実行してみます。
$ go run 02_web_scraper.go
h1 contains: 'Where the world
builds software'
Took: 2.758275 secs
参考までに、以下が実行時点でのgithub.comの様子です。(今回のサンプルでスクリーンショットを撮ったものではなく、別途Google Chromeで開いて画面切り抜きキャプチャしたものです。)
一番大きな文字列「Where the world builds software」の部分がH1タグのテキスト内容なのですが、確かに読み取っていますね。
それにしても 2.7秒、、、ちょっと重いかも?
サンプル3 現在のHTMLソースコードを取得する
さきほどの記事のサンプルには続きがあり、実行するアクションを変更してDOMを取得しています。
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/cdproto/dom"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/chromedp"
)
func main() {
// create chrome instance
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()
// create a timeout
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()
start := time.Now()
// navigate to a page, wait for an element, click
var res string
err := chromedp.Run(ctx,
emulation.SetUserAgentOverride("WebScraper 1.0"),
chromedp.Navigate(`https://github.com`),
// wait for footer element is visible (ie, page is loaded)
chromedp.ScrollIntoView(`footer`),
chromedp.Text(`h1`, &res, chromedp.NodeVisible, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
node, err := dom.GetDocument().Do(ctx)
if err != nil {
return err
}
res, er := dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx)
fmt.Print(res) // print HTML source code
return er
}),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\n\nTook: %f secs\n", time.Since(start).Seconds())
}
実行してみます。
$ go run 03_print_source_code.go
<!DOCTYPE html><html lang="en" class="html-fluid"><head>
<meta charset="utf-8">
<link rel="dns-prefetch" href="https://github.githubassets.com">
〜〜〜省略〜〜〜
<div aria-live="polite" class="sr-only"></div></body></html>
Took: 2.581220 secs
長すぎたので省略しましたが、取得できているようです。
サンプル4 スクリーンショットを取る
さきほどの記事には更に更に続きがあり、驚くほど簡単にスクリーンショットを撮る事ができるようです。
すこしだけコードを修正してスクリーンショットを撮ってみました。
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/chromedp"
)
func main() {
// create chrome instance
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()
// create a timeout
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()
start := time.Now()
// navigate to a page, wait for an element, click
var res string
var imageBuf []byte
err := chromedp.Run(ctx,
emulation.SetUserAgentOverride("WebScraper 1.0"),
chromedp.Navigate(`https://github.com`),
chromedp.Text(`h1`, &res, chromedp.NodeVisible, chromedp.ByQuery),
// wait for footer element is visible (ie, page is loaded)
chromedp.ScrollIntoView(`h1`),
chromedp.WaitVisible(`h1`, chromedp.ByQuery),
chromedp.Screenshot(`h1`, &imageBuf, chromedp.NodeVisible, chromedp.ByQuery),
)
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("h1-screenshot.png", imageBuf, 0644); err != nil {
log.Fatal(err)
}
fmt.Printf("h1 contains: '%s'\n", res)
fmt.Printf("\nTook: %f secs\n", time.Since(start).Seconds())
}
実行してみます。
$ go run 04_screenshot.go
h1 contains: 'Where the world
builds software'
Took: 13.923399 secs
約14秒?ちょっと重すぎやしないか、、、
とはいえ正常終了したので、フォルダを見に行くと、、、
h1-screenshot.png
に画像が保存されていました!
こんなに!簡単に!!スクリーンショットが撮れるなんて!!!
しかも指定した要素(h1)にフォーカスした画像ができるんですね。
後述の参考資料の中には、この特定要素のスクリーンショット機能を活用してSlackアイコンを生成している人もいました。
参考になる資料
今回は扱いませでしたが、ググって見つけた資料のリンクを貼っておきます。
-
chromedp/examples: chromedp code examples.
- 公式のexample集。要素をクリックしたり、Cookieを扱ったり、フォームをsubmitしたり、いろいろなユースケースのサンプルが揃っています。
-
chromedp/headless-shell - Docker Hub
-
containing a pre-built version of Chrome's headless-shell
だそうです。 コンテナ上でchromedpしたいときに便利そう。
-
-
bmf-tech - Golang×chromedp×slack botでslackの絵文字自動生成ボットをつくってみた
- 特定のDOMだけスクリーンショットを撮っていますね。
-
Goでheadless browserを用いた動的画像生成|Seiji Takahashi - timakin|note
- こちらも同様に、特定のDOMだけスクリーンショットを撮っていますね。
-
Chrome as a service in Go - Speaker Deck
- chromedpを業務で利用して、フォーム入力してPDFをダウンロードしてくる機能を実現したそうです。
- ただし、さらなる改善の結果、Chromeをやめてheadzoo/surf: Stateful programmatic web browsing in Go.を使うようにした模様。
-
chromedp で Chrome を見える状態(not headless)で起動して、かつ終了しないようにする - Qiita
- タイトルの通り、chromedpをheadlessではなく、UIを表示するモードで操作する例。
-
chromedpでGopherのための自動化を目指す - Retty Tech Blog
- chromedpを使ってみた系の記事。chromedp経由でブログにログインして記事を書くところまでやってみた上での知見が詰まっています。Cookie使ってますね。
-
Go - Headless Chrome を使って SPA をスクレイピングする - Qiita
- こちらもchromedpを使ってみた系の記事。
Discussion
こんな書籍もあるようです
uta_mory さん、情報提供ありがとうございます!
niboshi さん https://scrapbox.io/stream/niboshi/ の書籍のようです。
書籍の説明文をこちらに引用しておきます。
可愛い