4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Google Apps Scriptで6分の壁を超える不死鳥関数を実装してみた

Last updated at Posted at 2021-05-20

#はじめに

前回はGoogle Apps Scriptを使用してこのようなプログラムを作りました。

しかし、このサイトにもあるように、無料の範囲ではgoogleでは実行時間は6分で自動的に打ち切られてしまいます。

そこで、対策方法が何かないか調べたところ、以下のような記事が見つかりました。

今回は@s_maeda_fukuiさんの不死鳥関数をベースに少し私のスクリプトに合わせたものを作成しました。

#仕組み

この不死鳥関数とは簡単に説明すると、
ある時間(6分未満)経過すると、ループ処理をどこまで実行したかをプロパティに保存した後、すぐに同じ関数を実行するトリガーを作成し、スクリプトの実行を終了するという方法です。

#プロパティとは

ここで、プロパティとは、Google Apps Scriptのプロジェクトごとに読み書きできる値です。

このような現在のエディタでは見ることができないため、(スクリプト実行時に読み書きはできる)右上の以前のエディタを使用を押します。

image.png

その後ポップアップが出てきたりするが無視して進むと、以下のように古いエディタに移動できます。
その後「ファイル」→「プロジェクトのプロパティ」を押すと

image.png

以下のようなポップアップが出てくるので、「スクリプトのプロパティ」を押すと名前と値を登録することができます。

image.png

なお今回は
なまえ:number
値:0
と最初に設定しておきます。

image.png

#具体的な流れ

元の不死鳥関数はこのような流れです。

  1. スクリプトのプロパティから必要な値を引っ張ってくる
  1. 関数実行開始時間を取得
  2. ループ
    1. 現在時刻の取得
    2. difference = { 現在時刻 - 実行開始時刻 }
    3. ◯分経過しているか
      • している
        スクリプトプロパティの更新を行う
        次回実行のためのトリガーの設定
      • していない
        処理を行う
  3. ループ後処理(必要なら)
  4. スクリプトプロパティの初期化
  5. トリガーの削除(しないと永遠に回り続けるぞ?)

今回の関数では、このようなmain関数を設定します。

main_funk.txt
1.スクリプトのプロパティから必要な値を引っ張ってくる
2.関数実行開始時間を取得

new!! 2.5.forで回す内容をとってくる

3.ループ  new!! プロパティの値からスタート
    1.現在時刻の取得
    2.difference = { 現在時刻 - 実行開始時刻 }
    
  new!! 2.5. プロパティの更新(ifの前に移動)

    3.◯分経過しているか
        - している  
           次回実行のためのトリガーの設定 
      (new!! ミラー関数を指定する,afterを使用する)  

        -  していない
           処理を行う  

4.ループ後処理(必要なら)
5.スクリプトプロパティの初期化

そしてこのmain関数を実行するための関数を二つ用意します。

決まった時間に実行する関数.txt
1.たまっているトリガーの削除
2.main関数の実行
連続して実行するためのミラー関数.txt
1.たまっているトリガーの削除
2.main関数の実行

「決まった時間に実行する関数」は定期的に実行するためのトリガーとして指定。「連続して実行するためのミラー関数」は6分回避用のトリガーとして指定します。

改善点として、元の不死鳥関数では6分回避用のトリガーと定期的に実行する用のトリガーが分けられていなかったという問題点がありました。トリガーで起動してこのスクリプトを実行すると、最後にトリガーの削除があるため、次回以降自動で起動できないという問題点があったのでトリガーを分けることで対策しました。

また、プロパティについて、forで何番目から実行するかを保存するようにしました。

トリガーの作成については、afterを使用することで任意時間後に一度だけ自動実行可能にしました。

#具体的なスクリプト

以下が今回のスクリプト全体です。

.js

//なろうapiにアクセスして新着順500取得する。
function get_narou(mode){

  var now = new Date();  
  var date = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyyMMdd');
  var day = date + '-' + mode;
  Logger.log(day);
  var payload = "?" + 'out=json'+ "&" + 'gzip=0'  + "&" + "of=n&lim=500&order=new";
  Logger.log(payload);

  var url = "https://api.syosetu.com/novelapi/api/" + payload;
  Logger.log(url);

  var responseDataGET = UrlFetchApp.fetch(url).getContentText('UTF-8');
  Logger.log(responseDataGET);

  
  var contentJson = JSON.parse(responseDataGET);
  return contentJson;
}





//保存したいurlを入力するとwaybackに変換して登録
function wayback(url){
  var pdf_url = 'https://web.archive.org/save/'+url;
  Logger.log(pdf_url);
  var options = {
    "muteHttpExceptions": true,     // 404エラーでも処理を継続する
  };
  var responseDataGET2 = UrlFetchApp.fetch(pdf_url, options);
  var code = responseDataGET2.getResponseCode();
  Logger.log(code);
  //Logger.log(responseDataGET2.getContentText('UTF-8'));
  Logger.log("--------------------------------------------")

}




// 特定関数のトリガーを全て削除
function delete_specific_triggers( name_function ){
  var all_triggers = ScriptApp.getProjectTriggers();

  for( var i = 0; i < all_triggers.length; ++i ){
    if( all_triggers[i].getHandlerFunction() == name_function )
      ScriptApp.deleteTrigger(all_triggers[i]);
  }//for_i
}//func_deleteSpecificTriggers










//ver1 不死鳥関数未実装
function narou_wayback1(mode){
  contentJson=get_narou(mode);
  for (let i=0; i<contentJson.length; i++){
    var url = "https://pdfnovels.net/" + contentJson[i].ncode +"/main.pdf";
    Logger.log(i+ "番目 "+ contentJson[i].ncode);
    wayback(url);
  }
}





//ver2
//6分で自動的にできなくなってしまうので、5分経ったら途中から実行しなおすようにした。
function narou_wayback(mode){

  var properties = PropertiesService.getScriptProperties();
  var number = parseInt(properties.getProperty("number"));
  var start_time = new Date();
  
  contentJson=get_narou(mode);

  for (let i=number; i<contentJson.length; i++){

    var current_time = new Date();
    var difference = parseInt((current_time.getTime() - start_time.getTime()) / (1000 * 60));
    // スクリプトプロパティの更新
    properties.setProperty("number",i);
    
    if(difference >= 4){
      //もう一度動かす
      ScriptApp
      .newTrigger(func_name)
      .timeBased()
      .after(10 * 1000)
      .create();
      return;

    }else{

      var url = "https://pdfnovels.net/" + contentJson[i].ncode +"/main.pdf";
      Logger.log(i+ "番目 "+ contentJson[i].ncode);
      wayback(url);
    }
  }

  properties.setProperty("number",1);

}








//決まった時間に実行する関数
function day(){
  func_name = "day_mirror";
  delete_specific_triggers(func_name)
  narou_wayback("d");
}

//連続して実行するためのミラー関数
function day_mirror(){
  func_name = "day_mirror";
  delete_specific_triggers(func_name)
  narou_wayback("d");
}



順番に説明をしていきます。

##get_narou

get_narou.js
//なろうapiにアクセスして新着順500取得する。
function get_narou(mode){

  var now = new Date();  
  var date = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyyMMdd');
  var day = date + '-' + mode;
  Logger.log(day);
  var payload = "?" + 'out=json'+ "&" + 'gzip=0'  + "&" + "of=n&lim=500&order=new";
  Logger.log(payload);

  var url = "https://api.syosetu.com/novelapi/api/" + payload;
  Logger.log(url);

  var responseDataGET = UrlFetchApp.fetch(url).getContentText('UTF-8');
  Logger.log(responseDataGET);

  
  var contentJson = JSON.parse(responseDataGET);
  return contentJson;
}

このget_narou関数では小説家になろうのapiを使用して今後ループで回したい500個の文字列をとってきています。
今回の不死鳥関数では内容は関係ないです。

##wayback

wayback.js
//保存したいurlを入力するとwaybackに変換して登録
function wayback(url){
  var pdf_url = 'https://web.archive.org/save/'+url;
  Logger.log(pdf_url);
  var options = {
    "muteHttpExceptions": true,     // 404エラーでも処理を継続する
  };
  var responseDataGET2 = UrlFetchApp.fetch(pdf_url, options);
  var code = responseDataGET2.getResponseCode();
  Logger.log(code);
  //Logger.log(responseDataGET2.getContentText('UTF-8'));
  Logger.log("--------------------------------------------")

}

この関数は、for内で実行する関数で、waybackmachineにurlを登録するスクリプトですが、内容は今回は関係ないです。

##delete_specific_triggers

delete_specific_triggers.js

// 特定関数のトリガーを全て削除
function delete_specific_triggers( name_function ){
  var all_triggers = ScriptApp.getProjectTriggers();

  for( var i = 0; i < all_triggers.length; ++i ){
    if( all_triggers[i].getHandlerFunction() == name_function )
      ScriptApp.deleteTrigger(all_triggers[i]);
  }//for_i
}//func_deleteSpecificTriggers

今回の重要な関数。
すなわちプロジェクト内の任意のトリガーをすべて削除する関数です。
詳しくはこちらを。

##narou_wayback1

narou_wayback1.js
//ver1 不死鳥関数未実装
function narou_wayback1(mode){
  contentJson=get_narou(mode);
  for (let i=0; i<contentJson.length; i++){
    var url = "https://pdfnovels.net/" + contentJson[i].ncode +"/main.pdf";
    Logger.log(i+ "番目 "+ contentJson[i].ncode);
    wayback(url);
  }
}

今回の不死鳥関数を実装する前に使用していたmain関数です。
get_narouでリストを取得し、for内でurlにしwaybackを実行するという内容です。
この関数ではforで回している間に6分経ってしまい、途中で終わってしまうという問題がりました。
今回行っていることが分かりやすいので載せました。

##narou_wayback

narou_wayback.js


//ver2
//6分で自動的にできなくなってしまうので、5分経ったら途中から実行しなおすようにした。
function narou_wayback(mode){

  var properties = PropertiesService.getScriptProperties();
  var number = parseInt(properties.getProperty("number"));
  var start_time = new Date();
  
  contentJson=get_narou(mode);

  for (let i=number; i<contentJson.length; i++){

    var current_time = new Date();
    var difference = parseInt((current_time.getTime() - start_time.getTime()) / (1000 * 60));
    // スクリプトプロパティの更新
    properties.setProperty("number",i);
    
    if(difference >= 4){
      //もう一度動かす
      ScriptApp
      .newTrigger(func_name)
      .timeBased()
      .after(10 * 1000)
      .create();
      return;

    }else{

      var url = "https://pdfnovels.net/" + contentJson[i].ncode +"/main.pdf";
      Logger.log(i+ "番目 "+ contentJson[i].ncode);
      wayback(url);
    }
  }

  properties.setProperty("number",1);

}

不死鳥関数を実装したmain関数です。
大きな変更として、規定時間を超えたときにもう一度実行するためにトリガーを設定するのですが、その方法が変わっています。

.js
    if(difference >= 4){
      //もう一度動かす
      ScriptApp
      .newTrigger(func_name)
      .timeBased()
      .after(10 * 1000)
      .create();
      return;

以下のサイトを参考に、afterというものを使用しました。

内容としてはafter内の時間(ms)後に実行するトリガーを作成するというものです。
個々のfunk_nameというのはのちに出てくるdayとday_mirro関数で指定する、トリガー名(今回はday_mirror)です。
gas(ほぼjavascript)は初めてなので、変数のスコープが適当になっていますが、動きます。
これが実行されると、以下のようにトリガーが追加されます。

image.png

##dayとday_mirror

day.js
//決まった時間に実行する関数
function day(){
  func_name = "day_mirror";
  delete_specific_triggers(func_name)
  narou_wayback("d");
}


//連続して実行するためのミラー関数
function day_mirror(){
  func_name = "day_mirror";
  delete_specific_triggers(func_name)
  narou_wayback("d");
}

今回私が追加した部分であり、定期実行するために一番重要であるところです。
まず、普段定期的に特定の時間に実行したい時には、day関数を実行する関数として選択します。

image.png

このday関数ではまず後述する残っているday_mirror関数のトリガーを削除します。というのも、トリガーの数は以下のサイトによると、20個までに制限されているからです。

その後main関数であるnarou_wayback関数を実行します。
この中である指定時間を過ぎると、day_mirrorという関数のトリガーが作成され、実行されます。

image.png

image.png

そしてこのday_mirror関数の中ではday関数と全く同じことが行われます。
すなわちまずトリガーを削除し、実行後ある時間が経過したらまたトリガーを作成します。

つまり、まずday関数が定期的なトリガーによって実行され、一周目が実行されます。その後、day_mirror関数がforループが終わるまで何度もトリガーが作成されて実行されます。

なぜday関数とday_mirror関数に分けたかというと、トリガーの削除が問題となってくるからです。
元の不死鳥関数では、手動で実行した関数をずっと続けるには問題がありませんでした。
しかしトリガーで起動した場合、そのトリガー自身が削除されてしまうため次回以降自動で実行できません。
それを回避するために定期実行用のdayと削除用のday_mirrorに分けました。

#まとめ

今回はgasで6分以上疑似的に実行する不死鳥関数を実際に実装しました。
非常に有用で、@s_maeda_fukuiさんには感謝です。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?