背景
java Scriptなどを用い、クライアント側でレンダリングするサイトがあります。ブログでクライアント・サイト・レンダリングする代表的なのはアメブロです。
アメブロの記事をスクレイピングするには、curlなどでサーバーから取得することができないため、クライアントでレンダリングされたhtmlを使用する必要があります。
golangでクライアント・サイド・レンダリングするページをスクレイピング
参考にした記事
以下の記事を参考にしました。先人に感謝。
GoでHeadless browserを使いClient Side Renderingを Cloud Run で動かす go-rod/rodヘッドレスブラウジングします。
参考にしたコード
go-rod/rod というライブラリが良さそうで、これを使います。
以下がサンプルコードで、これを見るとヘッドレスブラウザを作って、jqueryスタイルでタグを読み込むような動くんだろうなーというのがなんとなくわかります。
import (
  "fmt"
  "github.com/go-rod/rod"
  "github.com/go-rod/rod/lib/input"
)
func main() {
  // Launch a new browser with default options, and connect to it.
  browser := rod.New().MustConnect()
  // Even you forget to close, rod will close it after main process ends.
  defer browser.MustClose()
  // Create a new page
  page := browser.MustPage("https://github.com")
  // We use css selector to get the search input element and input "git"
  page.MustElement("input").MustInput("git").MustType(input.Enter)
  // Wait until css selector get the element then get the text content of it.
  text := page.MustElement(".codesearch-results p").MustText()
  fmt.Println(text)
  // Get all input elements. Rod supports query elements by css selector, xpath, and regex.
  // For more detailed usage, check the query_test.go file.
  fmt.Println("Found", len(page.MustElements("input")), "input elements")
  // Eval js on the page
  page.MustEval(`() => console.log("hello world")`)
  // Pass parameters as json objects to the js function. This MustEval will result 3
  fmt.Println("1 + 2 =", page.MustEval(`(a, b) => a + b`, 1, 2).Int())
  // When eval on an element, "this" in the js is the current DOM element.
  fmt.Println(page.MustElement("title").MustEval(`() => this.innerText`).String())
}
ちなみに、上のコードを初めて動かした時は以下のような[launcher.Browser]を伴うメッセージが出力されました。chromiumというワードが見えます。
$ go run main.go 
[launcher.Browser]2022/10/22 18:18:47 try to find the fastest host to download the browser binary
[launcher.Browser]2022/10/22 18:18:47 check https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1033860/chrome-linux.zip
[launcher.Browser]2022/10/22 18:18:47 check https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/Linux_x64/1033860/chrome-linux.zip
[launcher.Browser]2022/10/22 18:18:47 check https://playwright.azureedge.net/builds/chromium/1033860/chromium-linux-arm64.zip
[launcher.Browser]2022/10/22 18:18:48 check result: Get "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/Linux_x64/1033860/chrome-linux.zip": context canceled
[launcher.Browser]2022/10/22 18:18:48 check result: Get "https://playwright.azureedge.net/builds/chromium/1033860/chromium-linux-arm64.zip": context canceled
[launcher.Browser]2022/10/22 18:18:48 Download: https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1033860/chrome-linux.zip
[launcher.Browser]2022/10/22 18:18:48 Progress:
[launcher.Browser]2022/10/22 18:18:48 00%
[launcher.Browser]2022/10/22 18:18:49 23%
[launcher.Browser]2022/10/22 18:18:50 46%
[launcher.Browser]2022/10/22 18:18:51 71%
[launcher.Browser]2022/10/22 18:18:52 95%
[launcher.Browser]2022/10/22 18:18:52 100%
[launcher.Browser]2022/10/22 18:18:52 Unzip to: /home/yamadatt/.cache/rod/browser/chromium-1033860
[launcher.Browser]2022/10/22 18:18:52 Progress:
[launcher.Browser]2022/10/22 18:18:52 00%
[launcher.Browser]2022/10/22 18:18:53 23%
[launcher.Browser]2022/10/22 18:18:54 45%
[launcher.Browser]2022/10/22 18:18:55 83%
[launcher.Browser]2022/10/22 18:18:55 100%
Git is the most widely used version control system.
Found 5 input elements
1 + 2 = 3
Search · git · GitHub
自分が書いたコード
自分が実際に書いたコードは以下です。
この関数の引数にurlを渡すと、hugoのページに必要なものを返却するという、簡単なものです。
最初のヘッドレスブラウザの初期化処理等はそのまま拝借しました。
ちょっと悩んだのは、取得したhtmlをそのまま取得する方法です。.MustHTML()を書けばhtmlで取得することができました。関数の戻り値をスライスなどにすればいいのですが、わかりやすいし、動けばいいんです。
func parse(url string) (title, date, tag, filename, body string) {
  // Launch a new browser with default options, and connect to it.
  browser := rod.New().MustConnect()
  // Even you forget to close, rod will close it after main process ends.
  defer browser.MustClose()
  // Create a new page
  page := browser.MustPage(url)
  // When eval on an element, "this" in the js is the current DOM element.
  title = page.MustElement("title").MustEval(`() => this.innerText`).String()
  title = title[:strings.Index(title, " | ")]
  // 本文
  body = page.MustElement("div.articleText").MustHTML()
  // 投稿時刻
  date = GetHugoDate(page.MustElement("time").MustEval(`() => this.innerText`).String())
  // ファイル名として、entry-xxxxxxxxxxx.htmlを取得する
  _, filename = filepath.Split(url)
  // テーマをtagとしてセットする
  tag = page.MustElement("span.articleTheme").MustEval(`() => this.innerText`).String()
  tag = tag[strings.Index(tag, "テーマ:")+12:]
  return
}