Google Apps Scriptで動的サイトのHTMLを取得する方法

## はじめに
Google Apps Script（GAS）は、スプレッドシートと連携した自動化処理に非常に便利です。中でも「Webページの情報を自動で取得し、スプレッドシートに転記したい」というニーズは非常に多いです。

一方でGAS単体ではクライアント側でレンダリングされる動的なサイトのHTML要素を取得することができません。
そこでこの記事では、クライアント側でレンダリングされる動的なWebサイトのスクレイピング方法を解説します。

## この記事で触れないこと
* GASの操作
* Cheerioの使い方
* スクレイピングの実装方法

## GASでのスクレイピングにCheerioを使う
GAS単体ではDOMの解析ができませんが、Cheerioというライブラリを使うことで、Node.js風にjQueryライクなDOM操作が可能になります。
GASとCheerioを組み合わせると、取得したデータをスプレッドシートに転記する処理も簡単に書けます。業務の自動化や定期的なデータ収集に最適です。

## しかし、Cheerioには限界がある
便利なCheerioですが、 **JavaScriptで描画されるコンテンツ（いわゆる動的サイト）** には対応できません。これは次のような理由によるものです：

- UrlFetchAppで取得できるHTMLは、　**JavaScriptの実行前の状態**
- つまり、　**画面上に表示されている内容と取得できるHTMLが異なる**
- Cheerioはあくまで「静的なHTML」に対する処理を行うライブラリ

### 具体的な例
たとえば、SPA（Single Page Application）で作られたECサイトの商品リストや、チャートライブラリで動的に描画されるグラフ情報などは、Cheerioでは取得できません。

## 解決方法
このような「JavaScriptで描画されたあとのHTML」を取得するには、ヘッドレスブラウザを使った描画処理が必要になります。そこで登場するのが[PhantomJSCloud]()です。

## PhantomJSCloudとは？
PhantomJSCloudは、クラウド上でヘッドレスブラウザを実行し、描画済みのHTMLを取得できるAPIサービスです。
PhantomJSCloudを使い以下の流れでスクレイピングが実行可能になります。
1. PhantomJSを呼び出しHTMLを描画
1. 描画されたHTMLをGASに返す
1. Cheerioでスクレイピング実行

## 実装編

### PhantomJSCloudのAPIキーを取得する
[PhantomJSCloud](https://www.phantomjscloud.com/)のページにアクセスし、APIキーを取得します。
`Sign up now`のボタンから登録します。
![Screenshot 2025-04-26 at 11.47.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1010273/e0d43563-644c-4373-a088-8930a1705969.png)

登録が完了するとAPIキーが表示され、以降PhantomJSCloudのエンドポイントへリクエストを送るために必要なためコピーして控えておきます。

![Screenshot 2025-04-26 at 13.25.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1010273/490623e5-68ae-4d2c-8321-78c3e7f83c3e.png)

### エンドポイントへのリクエストの書き方

#### GET
http(s)://PhantomJsCloud.com/api/browser/v2/`[先ほど発行したAPIキー]`/?request=`リクエストのJSONオブジェクト`


#### POST
http(s)://PhantomJsCloud.com/api/browser/v2/`[先ほど発行したAPIキー]`/

### リクエストのJSONオブジェクトの書き方
プロパティはたくさんありますが、とりあえず最低限必要なもののみ紹介します。

* url（必須）
    * 操作したい対象のページURL
* render
    * レスポンスのレンダリングタイプ。HTMLとすればHTMLが戻される
* outPutAsJson
    * 
* overseerScript
    * 対象のページでクリックやフォーム入力などブラウザ操作するスクリプトを渡す
    * 詳細は[こちら](https://phantomjscloud.com/docs/http-api/automation/)を参考
    * Puppeteerのスクリプトの80%をサポートしている

参考

https://phantomjscloud.com/docs/http-api/interfaces/ipagerequest.html

### GETリクエストを送ってページを取得してみる
サクッとQiitaのトップページのHTMLを取得してみます。
HTMLが返されるので結果をCheerioに渡してあげればスクレイピングが可能です。

```javascript
function myFunction() {
  const apikey = "apiキー";
  const options = {
    url: "https://qiita.com/",
    renderType: "HTML",
    outputAsJson: true,
  }
  // encodeURIComponentでエンコードする必要があるためエンコード
  const requestJson = encodeURIComponent(JSON.stringify(options));
  const endpoint = `https://phantomJsCloud.com/api/browser/v2/${apikey}/?request=${requestJson}`
  const response = UrlFetchApp.fetch(endpoint).getContentText();
  const html = JSON.parse(response)['content']['data'];

  console.log(html);
}
```

### ログイン処理が必要な場合
ログインが必要な場合にはPOSTリクエストを送り、`overseeerScript`にログインフォームを操作するスクリプトを渡すことで実装が可能になります。


今回はPhantomJSCloudにある[サンプルのログインページ](https://phantomjscloud.com/examples/corpus/automation.html)にユーザー名・パスワードを入力してログインしてみます。


```javascript
function myFunction() {
  const apikey = "apiキー";
  const endpoint = `https://phantomJsCloud.com/api/browser/v2/${apikey}/`
  const payload = {
    url: "https://phantomjscloud.com/examples/corpus/automation.html",
    renderType: "HTML",
    outputAsJson: true,
    overseerScript: 'await page.waitForSelector("input[name=id]"); \
      await page.type("input[name=id]", user, {delay:100}); \
      await page.type("input[name=pass]", pass, {delay:100}); \
      page.click("input[type=button][value=Login]"); \
      await waitForNavigation();',
  }

  const options = {
    method: "POST",
    payload: JSON.stringify(payload)
  }
  
  const response = UrlFetchApp.fetch(endpoint, options);
  const html = JSON.parse(response)["content"]["data"];

  console.log(html)
}
```
`overseerScript`内で使っているメソッドのドキュメントは[こちら](https://phantomjscloud.com/docs/http-api/automation/classes/page.html)になります。
このスクリプトを実行するとログイン後の画面のHTMLが返されるのでスクレイピングが可能になります。
またこれ以外にログイン後の画面でボタン操作がしたいなどあれば`overseerScript`内に追加のコードを書くことで実現可能です。

## 最後に
GASでのスクレイピング方法の情報は多くありますが、動的サイトにログイン処理をしてスクレイピングする情報が少なかったので苦労しました。
この情報がどなたかの参考になれば幸いです。