ポンコツエンジニアのごじゃっぺ開発日記。

いろいろポンコツだけど、気にするな。エンジニアの日々の開発などの記録を残していきます。 自動で収入を得られるサービスやシステムを作ることが目標!!

【GASの起動時間の制限を回避せよ】分割実行や非同期処理を使って高速実行を実現してみた!

この記事は GAS道場 Advent Calendar 2019 の25日目の記事です。 Google Apps Script(GAS)をこれから使おうという方向けのアドベントカレンダーになります。

とうとう最終日です! 今回は、GASを使いまくっている方だったら一度は見たことがあるかもしれない「起動時間の最大値を超えました」という文字、それを回避する方法を考えて実施してみたので紹介したいと思います。

f:id:ponkotsu0605:20191224231702p:plain

起動時間の最大値とはなにか

GASにはいろいろな制限があります。その中でも、1つのタスクの実行時間が制限は6分(360秒)となっていて、それ以上処理を行おうとすると、上のようなエラーが出てしまいます。

developers.google.com

いろんな処理をしたい、だけどもこの制限にかかってしまう。そんなときの回避策を自分はこうやりましたというのを紹介したいと思います。

例:スプレッドシートの一覧に書いてある名前のTwitterアカウントの検索

今回の例題として、スプレッドシートに入力しているワードのリストに書いてあるものをYahoo検索(Google検索)を行って、Twitterアカウントを探し、それを別のシートに記録してくというものです。

f:id:ponkotsu0605:20191224232354p:plain
検索対象の単語一覧

この一覧から検索したものが以下になります。

f:id:ponkotsu0605:20191224232422p:plain
検索結果のTwitterアカウント一覧

これを実現するために、以下のようなプログラムを書いてみました。

function main() {
  var words = getList();
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('result').clear();
  for (var i = 0; i < words.length; i++) {
    var word = words[i][0];
    var result = search(word);
    Logger.log(result);
    for (var j = 0; j < result.length; j++) {
      add(word, result[j])
    }
  }
}

function add(word, url) {
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('result').appendRow([word, url]);
}


function getList() {
  return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('words').getDataRange().getValues();
}

function search(word) {
  console.log('search:' + word);
  for (var n = 1; n <= 10; n++) {
    try {
      var searchUrl = "https://search.yahoo.co.jp/search?p=" + encodeURI(word)
      var response = UrlFetchApp.fetch(searchUrl);
      
      var myRegexp = /<a href=\"(.*?)\">/g;
      
      var elems = response.getContentText().match(myRegexp);
      
      var result = []
      for(var i in elems) {
        var title = elems[i]
        myRegexp = /href=\"(.*?)\"/;
        var url = title.match(myRegexp)[1];
        
        if (url.match(/https:\/\/twitter.com\/.*/)) {
          if(url.match(/\/status\//)) continue;
          if(url.match(/\/moments\//)) continue;
          if(url.match(/\/statuses\//)) continue;
          if(url.match(/\/hashtag\//)) continue;
          
          console.log('push: ' + url)
          
          result.push(url)
        }
      }
      return result;
    } catch(e) {
      console.log(e)
      console.log('NG count: ' + n);
    }
  }
}

これのmain()を実行して、スプレッドシートの一覧を取得し、Yahoo検索を行い、見つかったTwitterアカウントを別のシートにい記録していくというものになります。

これを行うことで、Twitterのアカウントの一覧の作成ができるのです。ここでの例は、売上が高いスマホゲームの一覧をシートに入力しておいて、それのTwitterアカウントを書き出してみたという内容になります。

ただ、Yahoo検索を毎回してしまっています。外部APIを叩くということは、とても時間がかかるということになります。また、Yahooの場合はエラーになることが多いので、再実行を10回までするようにスクリプトを書いています。

これの結果、単語の一覧をものすごい多くすると6分では処理しきれなくなってしまいます。

対処法

1実行あたりの実行時間が6分を超えてしまう、ということで、以下の対処法を考えました。

  • 処理を分割して1実行あたりの実行時間を減らす
  • 処理を同期処理ではなく非同期処理に変えて、並行処理を行う

これを実現するためにプログラミングをしたいと思います。

プログラム

ということで、以下のように書きました。 スプレッドシートから、スクリプトを開きます。

コード.gs は以下のようになります。

function onOpen() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var entries = [{
    name:"実行",
    functionName:"run"
  }];
  spreadsheet.addMenu("オリジナルメニュー", entries);
};

function run() {
  var html = HtmlService.createTemplateFromFile("index.html").evaluate();
  SpreadsheetApp.getUi().showModalDialog(html, "スクレイピング中!!");
}

function addRow(word, url, num) {
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('result').getRange(num, 1, 1, 2).setValues([[word,url]])
}

function getList() {
  return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('words').getDataRange().getValues();
}

function search(word) {
  console.log('search:' + word);
  for (var n = 1; n <= 10; n++) {
    try {
      var searchUrl = "https://search.yahoo.co.jp/search?p=" + encodeURI(word)
      var response = UrlFetchApp.fetch(searchUrl);
      
      var myRegexp = /<a href=\"(.*?)\">/g;
      
      var elems = response.getContentText().match(myRegexp);
      
      var result = []
      for(var i in elems) {
        var title = elems[i]
        myRegexp = /href=\"(.*?)\"/;
        var url = title.match(myRegexp)[1];
        
        if (url.match(/https:\/\/twitter.com\/.*/)) {
          if(url.match(/\/status\//)) continue;
          if(url.match(/\/moments\//)) continue;
          if(url.match(/\/statuses\//)) continue;
          if(url.match(/\/hashtag\//)) continue;
          
          console.log('push: ' + url)
          
          result.push(url)
        }
      }
      return {
        word:word,
        result:result
      };
    } catch(e) {
      console.log(e)
      console.log('NG count: ' + n);
    }
  }
}

index.html は以下のようになります。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script type='text/javascript'>    
      window.onload = function(){
      console.log('start')
        google.script.run.withSuccessHandler(search).getList();
      }
      var num = 1;
      function search(words) {
        console.log(words)
        for (var i = 0; i < words.length; i++) {
          word = words[i][0];
          
          google.script.run.withSuccessHandler(add).search(word);

        }
//        google.script.host.close();
      }
      function add(data) {
        var result = data.result;
        var word = data.word;
        console.log(data)
          for (var j = 0; j < result.length; j++) {
            google.script.run.addRow(word, result[j], num++)
          }
      }
  </script>
  </head>
</html>

この2つのファイルを作成して、スプレッドシートをリロードすると、以下のようなメニューが作成されます。

f:id:ponkotsu0605:20191224233534p:plain

オリジナルメニュー実行 を押すと、ポップアップが表示されます。

f:id:ponkotsu0605:20191224233750p:plain

今回はポップアップを閉じる処理を省略しているので、StackDriver Loggingを見て、処理が終わっていることを確認してからポップアップを閉じてください。

f:id:ponkotsu0605:20191224235940p:plain

ちなみに、全タスクが完了したら以下のスクリプトを呼ぶようにプログラムを書くことで、自動で閉じることを実現できます。

google.script.host.close();

処理の解説

では、この処理はどういうことをしているかというのを紹介したいと思います。

処理の分割

まずは、処理の分割ということで、ポップアップ上のHTMLに書いてあるJavaScriptからGASの処理を実行しています。JavaScriptからGASのスクリプトを呼ぶ方法については、以下のドキュメントをご覧ください。

developers.google.com

スプレッドシートから単語のリストを取得した後に、単語ごとに1実行となるようにGASのスクリプトを実行します。そうすることで、1つの単語の検索だけで最大6分かけることができるのです。もちろん1つの単語の検索にかかっても10秒もあれば終わるので全く問題ありません。

非同期処理による同時実行

そして、非同期処理をGASで実現します。 これも、ポップアップ上のJavaScriptから、1単語ごとに1タスクを呼ぶようにしましたが、同時にここで非同期に実行できるように呼んでいます。 GASの裏側はきっとたくさんのコンテナが並んでくれるはずなので、実行者の自分たちからしたら考える必要はありません。とりあえず呼んだらその数だけ実行してくれると思っていただいてもよいかもです。ただ、実行数の制限もあるのでご注意ください。

developers.google.com

この2つの工夫による高速化

さて、これを実行するとものすごい高速に実行してくれます。 自分も一発目の実行のときはめちゃくちゃ驚きました。 200単語を全部検索するのに、30秒で完了しました。合計91個のTwitterアカウントを見つけてくれました。

f:id:ponkotsu0605:20191224235227p:plain

もちろん非同期処理で実行しているので、早く終わったタスク、すなわち単語からTwitterアカウントを記録していくため、実行するたびに順序は変わっていきます。

さいごに

今回は、GASで分割実行と非同期処理について紹介しました。 HTML上のJavaScriptはいくら時間をかけても問題ないという特性を利用したものになりますね。

そして、無事25日間のアドベントカレンダーを書き終えることができました!とても大変な一ヶ月弱でしたが、GASをより詳しく知ることができた一ヶ月になったのは間違いありません。 落ち着いたら振り返りのブログを書きたいなと思っております。 これからも頑張って更新するので、引き続きよろしくお願いいたしいます!!