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

#はじめに

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

https://qiita.com/yuki_2020/items/40362aec281f8fa0a8ab

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

https://monoblog.jp/archives/2349



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

https://qiita.com/s_maeda_fukui/items/d194c6408803229fe1b9

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



#仕組み

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


#プロパティとは

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/beea2cec-0d4c-7984-5eaa-7bbc79e0026f.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/4bcb8e1b-cc4e-18d1-a436-f458908f4467.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/f6b96c80-e6c7-c8e2-8927-8e81c2e68477.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/5acae981-9161-2fd3-1f5f-c43860c5298b.png)


#具体的な流れ

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

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




今回の関数では、このような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

```

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


https://tonari-it.com/gas-trigger-delete/













##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というものを使用しました。

https://hirachin.com/post-4322/

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/09025431-d4c3-adf1-914a-bc396565bfd0.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/c1e26cef-9836-27b5-19bd-0ae2f6977e52.png)



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

https://monoblog.jp/archives/2349

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



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/8e8a8ee1-b3ed-5f8d-3613-648749707572.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/09025431-d4c3-adf1-914a-bc396565bfd0.png)


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

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

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

#まとめ

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