GASを使用してログインが必要なページからデータを取ってきてみた


## はじめに

はじめまして、エンジニア歴半年のGopherくんLoverなペーペーエンジニアです。
今回は、仕事の関係でスクレイピングをしたい場面があったので、その内容を備忘録として記録しようと思います。

#### やりたいこと
・仕事の都合で数値を扱うことが多いのですが、管理画面上だと何かと不便……
・独自に数値を編集するために、スプレッドシートに落としたいが、エクスポートできない(なんでやねん)
・全部手動で取ったら時間的大赤字なので、自動でとりたい

という経緯で作成してみることにしました。
Pythonはやったことがなかったので、一旦触ったことがあるGASを使用してやってみることにしました。
(本当はPythonでやってみたい……)

実装しながら並行で書いたので、処理は少し独自性が強いかもしれません。
参考程度でお読みいただければ幸いです。

#### ～流れ～
>**⓪事前準備(ライブラリのインストール)**
>**①ログイン画面へのリクエスト**
>**②ログインに必要なヘッダー情報を取得**
>**③必要な情報を用いて、ログイン画面にてログインのリクエスト**
>**④目的のページからデータを取得できるかを確認**
>**⑤目的のページからのデータの取得**
>**⑥スプレッドシートへの書き出し**

## ⓪事前準備(Parser・Cheerioライブラリのインストール)

今回はParser・Cheerioを使い分けたいと思っておりますので、どちらも入れておきます。

> Parserライブラリ スクリプトID(2022年11月時点)
> 1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw

> Cheerioライブラリ スクリプトID(2022年11月時点)
> 1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711450/91cd11d9-7b5e-ce8e-534e-4e58a8c366d4.png" width="350">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711450/9e4003e7-4c23-5f1f-0684-fc51b872e9d9.png" width="350">

## ①ログイン画面へのリクエスト

今回、データ取得先へのアクセスにログインが必要なので、まずはログインを突破するところからです。
まずはログイン画面にアクセスしていきます。
やることはシンプルで、ログイン画面のURLをリクエストするだけです。

```js
    // loginURL
    const loginURL = "ログイン先のURL"
    
    // リクエスト送信
    let response = UrlFetchApp.fetch(loginURL)
```

## ②ログインに必要なヘッダー情報を取得

ログイン画面へのリクエストが問題なく完了したら、ログインのリクエストに必要なヘッダー情報を取得します。
（ログインフォームに埋め込まれているトークンなどです。必要なければスキップしてください。）

今回必要なのは、リクエストヘッダー情報内のCookieに保存さている``session_id``と、
ログインフォームに組み込まれた``authenticity_token``でした。

※ログインしようとしているページによって必須な情報は異なるので、ログインしたいページに実際にログインした上で、リクエストヘッダーとCookieを分析してみてください。

先程のリクエストで返却された``response``からsession_idとtokenを取得します。

```js
    // レスポンスヘッダーからCookieを取得し、そこからsession_idを取得
    let cookies = response.getHeaders()["Set-Cookie"];
    
    // cookiesからsession_idを取得
    let sessionId = CookieUtil.getValue(cookies, 'session_id');
    
    // コンテンツ(ログインページのHTML)を取得
    let content = response.getContentText("UTF-8");
    
    // Cheerioライブラリを使用し、ログインページのHTML情報からtokenを取得
    $ = Cheerio.load(content)
    const token = $('[name="authenticity_token"]').val()
```

問題なく取得できていれば、``console.log(session_id)``などで出力したときに表示されるはずです。

## ③必要な情報を用いて、ログイン画面にてログインのリクエスト

先程取得した情報を設定し、ログインを行います。
リクエストヘッダー・ログイン時に送信するリクエストデータを配列としてまとめ、リクエスト時に送信します。

```js
    // リクエストヘッダー(cookieとuser-agent)
    let headers = {
        'cookie': `session_id=` + sessionId + ';',    // 先程取得したsession_id
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'
    }

    // リクエストデータ(token＋ログイン情報)
    let payload = {
        "authenticity_token": token,    // 先程取得したauthenticity_token
        "supplier[email]":"ログインID/メールアドレス",
        "supplier[password]":"パスワード"
    }
    
    // ヘッダー・リクエストデータをセット
    let options = {
        'method': "post",          // ポストメソッドで送信
        'headers': headers,        // リクエストヘッダー
        'payload': payload,        // リクエストデータ
        'followRedirects': false   // リダイレクト処理の抑止
    };
    
    // リクエスト送信
    response = UrlFetchApp.fetch(loginURL, options);

    // 結果の確認
    console.log("初回のアクセス：" + response.getResponseCode())
    console.log(content)
```

取得したレスポンスを出力して、ログイン後のHTMLが出力されていれば成功です。

今回はリダイレクトを止めているので、リダイレクトに飛ばすリンクが記載されたHTMLが出力されました。
ステータスコードが302であり、さらに``https://リダイレクト先のURL``へのリダイレクトがかかっています。
ログイン画面に戻されていなかったので、ログイン処理の突破は完了です。

```言語:出力結果
    // リダイレクトがかかっている
    初回のアクセス：302
    <html><body>You are being <a href="https://リダイレクト先のURL">redirected</a>.</body></html>
```

## ④目的のページからデータを取得できるかを確認

ログイン処理は突破できたので、最後にデータの取得を行います。
データを取得したいページURLを用いて、データを取得を試みました。

今回は数値がテーブル状に表示されているページから、そのテーブル要素のデータを取りたいと考えていたので、まずは取れているかを確認しました。

```js
    // データを取得したいページのURL
    let targetURL = "https://データを取得したいページのURL"
    
    // 先ほどと同じoptionsを使用しURLからデータを取得
    response = UrlFetchApp.fetch(apiURL, options)
    content = response.getContentText("UTF-8")

    console.log("目的ページへのアクセス：" + response.getResponseCode())
    
    $ = Cheerio.load(content);
    
    // trの中身が取れているかを確認
    if ($('tbody tr').length > 0) {
        // 要素が取れている：成功
        console.log("取得：" + '成功')
    } else {
        // 要素が取れていない:失敗
        console.log("取得：" + '失敗')
    }
```

とれていませんでした。なぜでしょうか……？

```言語:出力結果
    目的ページへのアクセス：200
    取得：失敗
```

いろいろ調べたところ、テーブル内のデータはページ描画後に別のAPIが叩かれているため、アクセスしたURLからの情報の取得ができていなかったようでした。
なので、ページ描画後に実行されているAPIのリクエストURLを直接叩くことにしました。

なお、リクエストAPIを叩いたときの返却値が配列だったので、それをParserで切り出します。
とりあえずテーブル内のタイトルを抜き出してみました。

```js
    // データを取得したいページのURL→叩かれているAPIのリクエストURLに変更
    let apiURL = "https://実行されているAPIのリクエストURL"
    
    // 先ほどと同じoptionsを使用しURLからデータを取得
    response = UrlFetchApp.fetch(apiURL, options)
    content = response.getContentText("UTF-8")
    
    // apiの返却値が配列での返却だったので、CheerioではなくParserを使用  
    var $titles = Parser.data(content).from('"display_name":"').to('","').iterate();
    
    // とりあえず、上位3つを出力
    for (var index = 0; index < 3; index++) {
    console.log("タイトル：" + $titles[index]);
    }
```

という形でタイトルが出力されたので、問題なさそうです。
これで、データが取得できることが確認できました。


```言語:出力結果
    タイトル：テーブル内に記載されたタイトルその１
    タイトル：テーブル内に記載されたタイトルその２
    タイトル：テーブル内に記載されたタイトルその３
```

## ⑤目的のページからのデータの取得

ここからは実際に必要な情報を抜き出す処理を書いていきます。
なお、ここからはとりたいデータや目的のページによっても異なるので、省略した形で記載します。

加えて、今回は目的ページのテーブルの各行への埋め込まれたリンクから詳細ページへアクセスし、データを取りたいと考えていました。
そのため、まずは詳細ページへのIDを取得して詳細ページのURLを作成し、中身のデータを取得しています。

```js
    // データを取得しているAPI
    let apiURL = "https://実行されているAPIのリクエストURL"

    response = UrlFetchApp.fetch(apiURL, options)
    content = response.getContentText("UTF-8")

    // 返却された配列から詳細ページの個別IDとそのページの形式を取得
    var $links = Parser.data(content).from('"encoded_id":"').to('","').iterate();
    var $isGroup = Parser.data(content).from('"is_group":').to(',"').iterate();

    // 返却用の配列作成
    let list = new Array();

    // 各リンクから詳細なデータを取得
    for (var index = 0; index < $links.length; index++) {
        // 各ページのデータを取得
        let detailURL;

        // ページの種類によってURLの形式が少し違うので、if文で分岐
        if($isGroup[index] == "false") {
            // ダッシュボードのURL＋詳細ページの個別IDでURLを作成
            detailURL = "https://ダッシュボードのURLその１" + $links[index];
        } else if($isGroup[index] == "true") {
            detailURL = "https://ダッシュボードのURLその２" + $links[index];
        }
    
        // レスポンスの格納先
        let pushResponse;

        // 詳細ページの情報を取得
        try {
          pushResponse = UrlFetchApp.fetch(detailURL, options);
        } catch(e) {
          Logger.log('Error:');　// エラーの場合、ログを出力するようにしています。
          Logger.log(e);
        }

        let pushContent = pushResponse.getContentText("UTF-8")

        // Parserを使用して要素を抜き出し
        let pageTitles = Parser.data(pushContent).from('page-title').to('</div>').iterate();
        let subTitles = Parser.data(pushContent).from('sub_title').to('</div>').iterate();
        let messages = Parser.data(pushContent).from('message').to('</div>').iterate();

        // 格納ようの変数に代入(各要素、1つずつしか存在しないので配列の0番を指定)
        let pageTitle = pageTitles[0]
        let subTitle = subTitles[0]
        let message = messages[0]
    
        // プッシュ通知のデータ(1組)を作成
        let pageDetail = [pageTitle, subTitle, message];

        // 返却用のプッシュリストに格納
        list.push(pageDetail);
    
        // アクセス負荷の低減のため、10回アクセスごとに10秒のスリープ
        if (index%10 == 0) {
          Utilities.sleep(10000);
        }
    }

    // 検証のため上位5件だけ出力
    for (var i = 0; i < 5; i++) {
    for(listContent of list) {
        console.log(list[i]);
    }

```

```言語:出力結果
    [[タイトルその１,サブタイトルその１,メッセージその１],
     [タイトルその２,サブタイトルその２,メッセージその２],
     [タイトルその３,サブタイトルその３,メッセージその３],
     [タイトルその４,サブタイトルその４,メッセージその４],
     [タイトルその５,サブタイトルその５,メッセージその５]]
```
※見づらいので改行を入れております。

問題なく出力されていれば、データの取得は完了です。

## ⑥スプレッドシートへの書き出し

書き出し用のスプレッドシートを用意し、そのIDに書き出すよう処理を書きます。
先程までで作成した処理を``getPageData();``として保存したので、それを呼びだしたのち、返却値をスプレッドシートに書き出します。

```js
    function myFunction() {
        // 先程作成した取得用のメソッドを呼び出し
        var list = getPageData();
        
        // 書き出し先のスプレッドシート
        var id = "スプレッドシートのID"; 
        var ss = SpreadsheetApp.openById(id);

        // スプレッドシート内のシートタブの名前を指定
        var sheet = ss.getSheetByName("page-list");

        // 最終行の抽出
        var lastRow = sheet.getLastRow();

        // 書き出し
        for(var i=0; i < pushList.length; i++) {
            for(var j=0; j < pushList[j].length; j++) {
                sheet.getRange(lastRow+1+i,1+j).setValue(pushList[i][j]);
            }
        }
    }
```

このような形で出力できていれば成功です。

![コメント 2022-11-30 150119.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711450/982dff61-841c-cc5b-020b-eb1c24a28376.png)


少し長々と書きましたが、以上、GASを使ったスクレイピングの方法のまとめでした。

本当はトリガー設定して自動化したかったのですが、ログインで思った以上にてこずり……
力尽きたので次回に回したいと思います！
最終的にはもろもろ全部自動で取得できるよう回収していきたいです。(秘伝のタレ化はしないように…）

ここまでお読みいただけた方、本当にありがとうございました。
少しでも参考になっていれば幸いです。

## 参考にさせていただいたサイト

https://qiita.com/standard-software/items/16214dc4e64d28952c2d

https://qiita.com/tanabee/items/2c51681396fe12b6a0e4

https://qiita.com/takaito0423/items/259097b55b026800c875