#はじめに
前回はGoogle Apps Scriptを使用してこのようなプログラムを作りました。
しかし、このサイトにもあるように、無料の範囲ではgoogleでは実行時間は6分で自動的に打ち切られてしまいます。
そこで、対策方法が何かないか調べたところ、以下のような記事が見つかりました。
今回は@s_maeda_fukuiさんの不死鳥関数をベースに少し私のスクリプトに合わせたものを作成しました。
#仕組み
この不死鳥関数とは簡単に説明すると、
ある時間(6分未満)経過すると、ループ処理をどこまで実行したかをプロパティに保存した後、すぐに同じ関数を実行するトリガーを作成し、スクリプトの実行を終了するという方法です。
#プロパティとは
ここで、プロパティとは、Google Apps Scriptのプロジェクトごとに読み書きできる値です。
このような現在のエディタでは見ることができないため、(スクリプト実行時に読み書きはできる)右上の以前のエディタを使用を押します。
その後ポップアップが出てきたりするが無視して進むと、以下のように古いエディタに移動できます。
その後「ファイル」→「プロジェクトのプロパティ」を押すと
以下のようなポップアップが出てくるので、「スクリプトのプロパティ」を押すと名前と値を登録することができます。
なお今回は
なまえ:number
値:0
と最初に設定しておきます。
#具体的な流れ
元の不死鳥関数はこのような流れです。
- スクリプトのプロパティから必要な値を引っ張ってくる
- 関数実行開始時間を取得
- ループ
- 現在時刻の取得
- difference = { 現在時刻 - 実行開始時刻 }
- ◯分経過しているか
- している
スクリプトプロパティの更新を行う
次回実行のためのトリガーの設定 - していない
処理を行う
- している
- ループ後処理(必要なら)
- スクリプトプロパティの初期化
- トリガーの削除(しないと永遠に回り続けるぞ?)
今回の関数では、このようなmain関数を設定します。
1.スクリプトのプロパティから必要な値を引っ張ってくる
2.関数実行開始時間を取得
new!! 2.5.forで回す内容をとってくる
3.ループ new!! プロパティの値からスタート
1.現在時刻の取得
2.difference = { 現在時刻 - 実行開始時刻 }
new!! 2.5. プロパティの更新(ifの前に移動)
3.◯分経過しているか
- している
次回実行のためのトリガーの設定
(new!! ミラー関数を指定する,afterを使用する)
- していない
処理を行う
4.ループ後処理(必要なら)
5.スクリプトプロパティの初期化
そしてこのmain関数を実行するための関数を二つ用意します。
1.たまっているトリガーの削除
2.main関数の実行
1.たまっているトリガーの削除
2.main関数の実行
「決まった時間に実行する関数」は定期的に実行するためのトリガーとして指定。「連続して実行するためのミラー関数」は6分回避用のトリガーとして指定します。
改善点として、元の不死鳥関数では6分回避用のトリガーと定期的に実行する用のトリガーが分けられていなかったという問題点がありました。トリガーで起動してこのスクリプトを実行すると、最後にトリガーの削除があるため、次回以降自動で起動できないという問題点があったのでトリガーを分けることで対策しました。
また、プロパティについて、forで何番目から実行するかを保存するようにしました。
トリガーの作成については、afterを使用することで任意時間後に一度だけ自動実行可能にしました。
#具体的なスクリプト
以下が今回のスクリプト全体です。
//なろう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
//なろう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
//保存したい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
// 特定関数のトリガーを全て削除
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
//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
//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関数です。
大きな変更として、規定時間を超えたときにもう一度実行するためにトリガーを設定するのですが、その方法が変わっています。
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)は初めてなので、変数のスコープが適当になっていますが、動きます。
これが実行されると、以下のようにトリガーが追加されます。
##dayとday_mirror
//決まった時間に実行する関数
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関数を実行する関数として選択します。
このday関数ではまず後述する残っているday_mirror関数のトリガーを削除します。というのも、トリガーの数は以下のサイトによると、20個までに制限されているからです。
その後main関数であるnarou_wayback関数を実行します。
この中である指定時間を過ぎると、day_mirrorという関数のトリガーが作成され、実行されます。
そしてこのday_mirror関数の中ではday関数と全く同じことが行われます。
すなわちまずトリガーを削除し、実行後ある時間が経過したらまたトリガーを作成します。
つまり、まずday関数が定期的なトリガーによって実行され、一周目が実行されます。その後、day_mirror関数がforループが終わるまで何度もトリガーが作成されて実行されます。
なぜday関数とday_mirror関数に分けたかというと、トリガーの削除が問題となってくるからです。
元の不死鳥関数では、手動で実行した関数をずっと続けるには問題がありませんでした。
しかしトリガーで起動した場合、そのトリガー自身が削除されてしまうため次回以降自動で実行できません。
それを回避するために定期実行用のdayと削除用のday_mirrorに分けました。
#まとめ
今回はgasで6分以上疑似的に実行する不死鳥関数を実際に実装しました。
非常に有用で、@s_maeda_fukuiさんには感謝です。