GAS

1日当たりのトリガー実行時間制限に時間主導型は含まれない【GAS】

はじめに

今回は、GASの実行制限についてです。
GASには、トリガーで実行できる1日あたりの時間制限があります。
無料版では90分、GoogleWorkspaceユーザは6時間です。

ところが、私のGWS環境で7時間以上、実行されていました。
「誰もどこにも書いてないけど、時間主導はトリガー扱いではない・・・?」
と思い、検証してみました。

結論1:時間主導型は制限対象に含まれていないと思われる
結論2:そもそもこの制限、機能してなくない?
以下でそこに辿り着くまでの検証を紹介します。

無料プランで検証その1

1分毎に実行するトリガーを設定し、どれだけ実行できるか観察します。

仮説1:実行数参照画面の時間合計到達で上限到達

AppsScriptDashboardの実行数を確認する画面で、
表示される所要時間が上限値にきたら次のスクリプトは即エラーになる。
普通に解釈したらこれで引っ掛かりそうですが・・・

トリガー実行ログが数十件並ぶ画像

はい、ご覧の通り、完璧に90分を超過しています。
この後数時間放置しましたが、特に変化なく実行されていました。
本当なら実行開始2,3秒でエラーになるべきですが、
タイムアウトまでちゃんと中身動いてます。

仮説2:トリガー実行始めてからの経過時間で上限到達

9時から始めて、常時トリガー実行のスクリプトがあった場合、
10時半になったら上限にかかる、という仮説です。
大量の実行ログ
はい、この説も破綻!m9(^Д^)プギャー

無料プランで検証その2

仮説3:Dashboardで「トリガー」と区分されているものが対象

さすがに「トリガー」って書いてあるやつは対象だろう。
ということで、フォーム送信時トリガーを使います。
C#でseleniumを使い、強引に1分ごとにフォーム送信します。
起動されたスクリプトはほぼ360秒を所要するため、計算が容易です。
以下、検証用コードと結果画面です。

    internal class Class1
    {
        internal async void main()
        {
            var chromeVersion = new ChromeConfig().GetMatchingBrowserVersion();
            new WebDrivermana.DriverManager().SetUpDriver(new ChromeConfig());

            var options = new ChromeOptions();
            var sv = ChromeDriverService.CreateDefaultService();
            sv.HideCommandPromptWindow = true;

            using(var Cd = new ChromeDriver(sv, options, TimeSpan.FromSeconds(30)))
            {
                for(var i = 0; i < 500; i++)
                {
                    Cd.Url = "フォームURLやで";
                    Thread.Sleep(1000);
                    Cd.FindElement(By.TagName("textarea")).SendKeys("textarea");
                    Thread.Sleep(1000);
                    Cd.FindElement(By.XPath("//div[@aria-label='Submit']")).Click();
                    Thread.Sleep(58000);
                }
            }
        }
    }

やっぱり上限エラーにならない

結果:仮説立証ならず

画像を参照いただいた通り、どう見ても上限突破しています。

まとめ

今回の検証では、結論が明確になりませんでした。
GASのフォーラムにも投稿したことがありますが、
誰も分からないのか回答が1つもありませんでした。

制限が無かったと考えれば嬉しい部分もありますが、
公式サイトには上限が明記(範囲定義が曖昧ですが)されているので、 急に適用されるかもしれません。
「トリガー」という定義に当てはまりそうなものは上限に注意しましょう。

【GAS】appendRow()の同時実行は競合するのか

はじめに

GASでシート末尾に値を追加するappendRow()について、
同時に実行したら同じ行を編集してしまうのか、検証しました。
結論:同時実行は競合します。
以下、検証方法の紹介です。

コードと出力結果

function myFunction() {
  const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");

  let now = dayjs.dayjs();
  while(now.format("HH:mm")!="09:30"){
    now = dayjs.dayjs();
  }

  sh.appendRow([dayjs.dayjs().format("YYYY/MM/DD HH:mm:ss")]);
  Utilities.sleep(300);
  sh.appendRow([dayjs.dayjs().format("YYYY/MM/DD HH:mm:ss")]);
  Utilities.sleep(300);
  sh.appendRow([dayjs.dayjs().format("YYYY/MM/DD HH:mm:ss")]);
  Utilities.sleep(300);
  sh.appendRow([dayjs.dayjs().format("YYYY/MM/DD HH:mm:ss")]);
  Utilities.sleep(300);
  sh.appendRow([dayjs.dayjs().format("YYYY/MM/DD HH:mm:ss")]);
}

4つのタブで同じスクリプトエディタを開き、実行します。
すると9:30になった瞬間同時に書き込みが走り、合計20行に値が入るはずです。
しかし、何度かやっても17~19行しか値が入りませんでした。
やはり20回の実行のうち、最終行を取得するタイミングが重なってしまうようでした。
気になる方はシートからエディタを開いてコピペして試してみてください。
(シートなので証拠になりませんが、イメージ図です。)
18行しか書き込まれていないイメージ図

対処方法はあるのか

appendRow()を使う限り、競合の可能性は捨てきれないでしょう。
appendRow()以外の方法で、絶対に競合を避けたい場合には、
GoogleDriveにファイルを保存する等の手段があります。

シート行に保存しようとしていたデータ1つ1つについて、
Drive上にJSONなどのファイルを設ければ、競合可能性を相当下げることができます。
ファイル名を日次+UUIDなどにしておけば、同名ファイルが生成されることもありません。
注意点として、それらのJSONを同時に変更可能な仕様にしてはいけません。

さいごに

GoogleAppsScriptを使う以上、
一部の処理ではどうしても競合が発生してしまいます。
上述のような打開策もありますが、仕様が分かりにくくなります。
外部のデータベースの利用なども視野に入れて仕様検討するのが無難かと思います。

GASで銀行くんAPIを実行して銀行コードと支店コードを全件取得

はじめに

GASで銀行くんAPIを使って、
金融機関コードと支店情報をスプレッドシートに一括取得してみました。
銀行コード等を取得したい方はぜひ参考にしてみてください!

コードと出力結果


function ginko() {  
  const baseUrl = `https://bank.teraren.com/banks.json?per=2000`;

  const response = UrlFetchApp.fetch(baseUrl);
  const json = JSON.parse(response.getContentText());

  const pasteArr = json.map(x => [x.code, x.normalize.name]);
  const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("銀行くん");
  sh.getRange("A2:B").clear();
  sh.getRange(2, 1, pasteArr.length, pasteArr[0].length).setValues(pasteArr);
}


function shiten(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const bankSh = ss.getSheetByName("銀行くん");
  const bankJson = bankSh.getRange("A2:B").getValues().filter(x => x[0])
                    .map(x => ({code:x[0], name:x[1]}));

  bankJson.forEach(x => {
    const url = `https://bank.teraren.com/banks/${x.code}/branches.json?per=1000`;
    const response = UrlFetchApp.fetch(url);
    const responseJson = JSON.parse(response.getContentText());
    x.brunchJson = responseJson;
  });

  const maxColumns = Math.max(...bankJson.map(x => x.brunchJson.length + 2));
  const pasteArr = bankJson.map(x => {
    const rowData = [x.code, x.name];
    x.brunchJson.forEach(y => rowData.push(`${y.code}_${y.name}`));
    while (rowData.length < maxColumns) {
      rowData.push("");
    }
    return rowData;
  });

  const brunchSh = ss.getSheetByName("銀行くん支店");
  brunchSh.getRange("2:1500").clear();
  brunchSh.getRange(2, 1, pasteArr.length, maxColumns).setValues(pasteArr);
}

関数名やシート名が適当すぎるので、必要に応じて変更してください。1
スプレッドシートにコンテナバインドした前提で、こんな感じです。
銀行くんAPIの結果として金融機関コードや支店コードのリストがスプレッドシートに出力されている画像

さいごに

以前紹介した記事では、
無料版では上限問題で全支店情報まで日次取得できませんでした。
今回の銀行くんの場合、支店コードを毎日取得しても制限に引っ掛かりません!
毎日実行のトリガー設定を行うなど、便利に活用してみてください。

BankcodeJP APIで銀行コードと銀行名を一括取得【GAS】

はじめに

GASでBankCodeJP APIを使って、
金融機関コードをスプレッドシートに一括取得してみました。
銀行コード等を取得したい方はぜひ参考にしてみてください!

コードと出力結果

function getAllBanks(){
  
  const apikey = "あなたのAPIキー";
  const baseUrl = `https://apis.bankcode-jp.com/v3/banks?apikey=${apikey}&limit=2000`;
  let nextCursor = "";
  const banks = [];

  do{
    const response = UrlFetchApp.fetch(baseUrl+(nextCursor ? `&nextCursor=${nextCursor}` : ""));
    const json = JSON.parse(response.getContentText());
    banks.push(...json.banks);
    if(json.hasNext) nextCursor = json.nextCursor;
    else nextCursor = "";
  }while(nextCursor);

  const pasteArr = banks.map(x => [x.code, x.name, x.halfWidthKana, x.fullWidthKana, x.hiragana, x.businessTypeCode, x.businessType]);
  const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("bank");
  sh.getRange("A2:G").clear();
  sh.getRange(2, 1, pasteArr.length, pasteArr[0].length).setValues(pasteArr);

}

スプレッドシートにコンテナバインドした前提で、こんな感じです。
銀行コードなど銀行情報がシート出力されているイメージ

さいごに

尚、このAPIは無料版だと制限があります。
支店コードも概ね同様の書き方で全て取得できますが、
日次で全金融機関の支店コードを取得しようとすると、上限に引っ掛かります。
別の記事で、他のAPIの使用例も紹介しようと思いますので、
ぜひそちらの記事も参考にしてみてください!

⇒(追記)銀行くんAPIに関する記事を公開しました。

GoogleAppsScriptGitHubアシスタント undefinedの解決方法

はじめに

GASとgithubを連携するのに使うChrome拡張機能、
GoogleAppsScriptGitHubアシスタント。便利ですよね。
しかし、そこそこの頻度で以下のようなエラーに遭遇します。
[GitHub assistant] undefined
今回は、このエラーの解決方法について解説していきます。

再認証する

まず1つ目は、再認証する方法です。
Chrome上部ツールバーから拡張機能のボタンを選択、
GitHubアシスタントの・・・からオプションを選択します。
ツールバーのchrome拡張ボタンからオプションを選択

すると、本来はログイン情報に関する画面が表示されます。
が、それと同時に以下のような認証画面がポップアップする場合、
おそらく認証が切れているのでそのままAllowで許可、認証を行ってください。
Googleの認証をAllowする

再ログインする

前項の方法で解決しなかった場合、以下の方法も試してみてください。
前項と同様に上部バーからオプションを開きます。
ログアウトする
ここで一度、ログアウトして再度ログインしてください。
尚、再ログインの際にgithubのアクセストークンが必要になります。

GoogleAppsScriptAPIの設定をオンにする

それでも解決しない場合、
こちらにアクセスして設定をオンにしてください。
GoogleAppsScriptAPIを有効にする

さいごに

Googleアカウントを複数使っていたり、
二段階認証を設定していたりすると、発生頻度が高まるように感じています。
ぜひお役立ていただければと思います。

GASでGoogleDrive上のJSON内容を取得/上書きする方法

はじめに

Drive上にJSONファイルを置いておき、
それをGASから操作する際の基本的な方法を紹介します。

なぜGASでJSONファイルを使うのか

本文の前に、なぜこんなことをするのかについて。
社用webアプリ等に使う半端な量のデータを持つのに有用だからです。

社用とは言えwebアプリケーションですから、
操作すべき何らかの情報や、操作した結果情報を保存する必要があります。
これらをもし全てスプレッドシート上で管理したら、どうなるでしょうか。
web上の操作時にスプレッドシート上の数千行のデータを処理して・・・
となると、20秒とか30秒とかかかります。
webアプリを操作している時の読込みで30秒は流石に酷いですよね。
データ量が本当に多いならBigQueryなどデータベース運用が視野に入りますが、
DB使う程ではないけどシート管理も良くないという時、Drive上にJSONを置いておくのも1つの方法です。

読込のコード

以下のコードでJSONとして扱えます。

const file = DriveApp.getFileById("id");
const jsonStr = file.getBlob().getDataAsString("UTF-8");
const json = JSON.parse(jsonStr);

書き込み(上書き)のコード

以下のコードで書き込みできます。
drive上のテキストファイルの扱いとほぼ同じですね。
上書きとなりますので、差分のみ変更した場合はそのようなデータを予め作成する必要があります。

const file = DriveApp.getFileById("id");
file.setContent(JSON.stringify(json));

最後に

コード自体はごくシンプルですが、扱いようによっては非常に便利です。
いかなる時にドライブ上のJSONデータを運用するのかという点が重要だと思います。
getValuesが遅すぎると悩んでいる方は一度試してみてください。

GASの変更時トリガーが実行頻度によっては実行されない問題について検証してみた

0.はじめに

GASの変更時トリガーってあるけど、
本当に全ての変更をキャッチしてくれてるの?

と疑問に思ったので検証してみました。

例えばGoogleフォーム回答が入力されるスプレッドシートに対して、
シート変更時トリガーを仕込んでおり複数から同時に回答があったとしたら・・・?
本当はそんなスクリプトは組まない方がいいわけですが、
そんな時どうなるのか気になる方はぜひご覧ください。

 

1.検証用のコードとトリガー

本当の限界頻度を知りたいので、ごくシンプルな負荷の低いコードを使いました。


function myFunction() {
  console.log("testやで");
  GmailApp.createDraft("test@test","testやで","testやわ");
}

トリガーはこちら。普通の変更時トリガーです。
変更時トリガー設定の画像
もちろんこのスクリプトはスプレッドシートにバインドしています。

 

2.高速で実行してみた

まず、適当に10回実行してみた結果がこちらです。
セルを10個変更したスプレッドシートの画像
6行の実行ログ
10回変更したのに実行ログは6行しかあらへん。どういうこっちゃ。
実行されてるけど、実行ログだけ出てないのか?と思い、Gmailの下書きを確認します。
6件だけのメール下書き
やっぱり6件しかない。という事で、6回しか実行されていないようです。
高速での変更時トリガーは実行が欠損する
という事が分かりました。
(1回だけでは試行回数不足のため複数回、別時期にも試しております。)

 

3.実行頻度を落として実行してみた

どれぐらいが敷居なのか判断したいため、
秒間1~3回程度に実行ペースを落としてみます。

10回成功 秒間1~2回
10回成功した画像

10回成功 秒間1~3回
10回成功した画像

9回成功 秒間1~2回
9回成功した画像

無暗に高速実行した時より成功率は上がりましたが、まだムラがあります。
(3回だけでは試行回数不足のため複数回、別時期にも試しております。)

 

4.秒間1回にしてみた

では秒間1回ではどうでしょうか。

秒間1回程度で10回成功
秒間1回程度の実行で10回成功している画像

秒間1回程度で10回成功その2
秒間1回程度の実行で10回成功している画像

秒間1回程度で10回成功その3
秒間1回程度の実行で10回成功している画像

概ね、1秒に1回程度の実行であれば安定して動きそうです。
私の過去の検証や経験則を合わせても、
1秒1回以下のペースで安定すると結論付けてよいのでは、と考えています。

 

5.さいごに

いかがでしたでしょうか。
変更時トリガーが安定しない、同時実行の際に不安、
といった場合にはぜひこの内容を思い出してみてください。

それにしても、皆さんに画像で伝わるように秒間1回で実行するとかって難しいですね…w
GASの限界に挑戦するシリーズ、個人的に面白いなと思っているので、
何か思いつけばまたやってみようと思います。
「これ試してみてほしい!」等あればお気軽にコメントくださいね。

フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法

0.はじめに

以前、こんな記事を書きました。
「フォームのデータを取得できませんでした。のエラーに対処した話」

内容としては、おそらくGoogle側の問題が原因で発生するエラー、
Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。
に対処する方法についてでした。
GASのフォーム送信時トリガーで回答情報を取得しようとする際に起こるエラーですね。

ただ、エラーを防止する方法は無く、
エラーが起きても後から再実行できる方法を提案するものでした。

記事の投稿から時間が経ち、私が採用しているコードも変わりました。
相変わらずエラーを防止する方法はありませんが、
上記の記事はいまだに結構アクセスがありますので、
最新版をお届けしようと思います。

 

1.コード

// フォーム送信時トリガー
function main(e) {
  let datas = {status:false, msg:"初期値"};
  //フォームデータ取得を20回トライする
  for(let i = 1; i<=20; i++){
    datas = getFormDatas(e);
    if(datas.status) break;
    Utilities.sleep(3000);
  }
  if(!datas.status){
    //前回記事の方法やエラーメール送信などを書く
  };
}

//フォームデータの取得
function getFormDatas(e){
  try{
    // 必要な処理をする
    const formTitle = FormApp.getActiveForm().getTitle();
    const itemresponses = e.response.getItemResponses();
    const address = e.response.getRespondentEmail();
    const responseId = e.response.getId();
    return {status:true,formTitle:formTitle,itemresponses:itemresponses,address:address,category:category,responseId:responseId};
  }
  catch(er){
    console.log(er);
    return {status:false, msg:er.message};
  }
}

 

2.簡易解説

フォーム回答を取得する部分について、getFormDatasという関数にしています。
これを、成功するまで20回を上限に実行しています。ごくシンプルですね。
高速でループしてもサーバーエラーが解消しない限り無意味なので、
気分的にループ毎に3秒のwaitを入れています。
このwaitに本当に意味があるかは分かりません!

 

3.検証結果

このコードを書いた当時、SeleniumでGoogleフォームを1,500回、回答してテストしました。
※seleniumとは、ブラウザを自動操作するツールです。pythonやC#で使えます。
エラーが出ない時期もありますが、出る時期に行いました。
結果、2回以上のループに入るケースが2割ほどありましたが、
20ループを超えてエラーを吐くケースは1%未満に留まりました。

 

4.ループ回数は何回が適切なのか

ループしすぎて処理がタイムアウトにならなければいいのではないでしょうか。
あまり気にせず適当に決めていいと思いますが、
エラーが出る時期は5ループとか10ループだと結構突破してくる印象があります。

 

5.最後に

いかがでしたでしょうか。
単に試行回数を増やすという強引な対処ですが、
ランダム性のあるこの手のエラーには1つの有効な手立てとなります。
ぜひ、前回記事と合わせて活用してみてください。

AdminDirectoryでエイリアスを追加/削除する方法 – GAS

0.はじめに

エイリアスを操作しようと思っていざ調べてみると、
思いのほか情報が少なく、誤情報もあり困ったため、方法を共有します。

 

1.追加する方法


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
//AdminDirectory.Users.Aliases.insert({"alias":"エイリアスメールアドレス"}, "ユーザのプライマリメールアドレス");

 

2.削除する方法


AdminDirectory.Users.Aliases.remove("userMail@~~~~", "エイリアス@~~~~");
//AdminDirectory.Users.Aliases.remove("ユーザのプライマリメールアドレス", "エイリアスメールアドレス");

 

3.一括更新できないのか?

色々と検証してみたところ、ループを回すしか方法が無さそうです。
メソッド1発でできる方法があれば是非教えてください。

AdminDirectoryで管理者が自アカウントを変更すると403になる罠

0.はじめに

GoogleAppsScriptでAdminDirectory系のメソッドを実行した際に403が出て若干ハマったので共有します。

 

1.結論

結論:管理者アカウントを操作しようとしたからです。

 

通常、AdminDirectoryServiceはそれなりの権限がないと実行できません。
なぜならユーザー情報やグループ情報を変更したり削除したりできてしまうからです。
「運用中の通常ユーザーの情報は触りたくないから自分のアカウントで試すか~」
とやると、403が返ってきます。
あなたのアカウントは管理者アカウントではありませんか?
AdminユーザーでもAdminユーザーを変更する事ができないようです。


API call to directory.users.aliases.insert failed with error: Not Authorized to access this resource/api

「AdminDirectory 403」等で調べると、スコープや権限の話が出てきますが、
スコープやユーザー変更権限があっても実行できません。
「Groups.list()とかUsers.insert()とかできるのになんで?」となりますが、
管理者アカウントは操作できる対象ではありません。

 

2.ちなみに実行したコードはこちら

ちなみに私が実行したコードはこれでした。
自分のメールエイリアスを設定しようとしたものです。
Users.Aliasesの情報が少なく「何かコードがおかしいんだ」と悩みましたが、
変更対象アカウントを一般ユーザーにしたところ普通に動作しました。


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
AdminDirectory.Users.Aliases.remove(userKey, "userMail@~~~~");