Google Apps Script で FetchAllとRedirctURL の組み合わせは悪い

Google Apps Script (以下、GAS)で、困ったことがあったので備忘録として残しておこうと思います。

やろうとしたこと

特定ハッシュタグにおける、ツイートに書いてあるリンクを集めようとしていました。 そのリンクは、特定のドメインのみでフィルタリングしたいとも思っていました。 これらをRESTful APIとして提供したかったので、手軽に作れるGASで作ろうと考えていました。

取り組んでみたこと

Twitterに書くリンクは、全て短縮URLになります。 そのため、短縮URLにアクセスし、リダイレクト先のURLを取りに行く必要がありました。 GASでは、リクエストメソッドであるfetchがあります。そのfetchのfollowRedirectsというオプションをfalseにし、responseHeaderのlocationを取ることで、解決(リダイレクト先のURL取得が)できます。

developers.google.com

また、1リクエストだけをするfetchでは、直列処理になってしまうため、大変遅いです。 複数リクエストが同時にできるfeatchAllを使うことで、並列処理ができ、パフォーマンスが良いです。 要するに次のようなコードで解決しようと考えていました。

f:id:silverbirder180:20200224084938p:plain
FetchAllとRedirectURL

let urlList: Array<string> = ['https://t.co/XXXX', 'https://t.co/YYYY'];
const locationList: Array<string> = [];
while (true) {
    const requestList: Array<URLFetchRequest> = urlList.map((url: string) => {
        return {
            url: url,
            method: 'get',
            followRedirects: false,
            muteHttpExceptions: true,
        }
    });
    const responseList: Array<HTTPResponse> = UrlFetchApp.fetchAll(requestList);
    urlList = [];
    responseList.forEach((response: HTTPResponse) => {
        const allHeaders: any = response.getAllHeaders();
        const location: string = allHeaders['Location'];
        if (location) {
            locationList.push(location);
            urlList.push(location);
        }
    });
    if (urlList.length === 0) {
        break;
    }
}
return locationList;
追記 (20200228)

developer.twitter.com

TwitterAPIレスポンスに urls がありました。説明はありませんでしたが、Tweetに貼られたリンク(短縮URLと、オリジナルURL)の情報が入るそうです。

"urls": [
          {
            "url": "https://t.co/Rbc9TF2s5X",
            "expanded_url": "https://twitter.com/i/web/status/1125490788736032770",
            "display_url": "twitter.com/i/web/status/1…",
            "indices": [
              117,
              140
            ]
          }
 ]

困ったこと

この手段だと、Locationを1つ1つ辿っていくことになります。 そのため、リダイレクトを自動的に追う( followRedirects: true )よりも、処理コストが大きいです。まあ、そこは目を瞑ります。

次です。

www.monotalk.xyz

fetchやfetchAllは、muteHttpExceptions: true としたとしても、ExceptionErrorが発生してしまいます。 そうすると、例えば1000件のURLをfetchAllした場合、どれが成功で、どれが失敗で、どれが未実施か がわからないというところです。

f:id:silverbirder180:20200224090136p:plain
FetchAllとRedirectURL (Error)

Promise.allSettled が使えれば、解決できるのかなと思いますが、現状Promiseは使えません。

私が思う解決策としては、

  • fetchAllではなく、fetchを使う
  • fetchAllでリクエストする件数をいくつかの塊に分ける。(一気にではなく、分ける)

最後に

そもそもなのですが、今回やろうとしたことってGASの良さがないですよね。 GASは、GSuites連携を簡単にできるという良さがあります。

しかし、今回はちょっとしたクローラーを作りたいだけでした。もちろん、GASでも作れると思いますが、いくつかを妥協しないといけなくなります。

もし、そこが妥協できないのであれば、別の手段を検討する必要があります。

教訓

  • 表面的
    • fetchAllするときは、リダイレクト先URLを取得しない
  • 根本的
    • 目的に適したツールを選択する

ちなみに、このツールは、並列処理をシンプルにコーティングできるgolangで書き直そうと考えています。