Amazonで商品を見るセール会場へ

ExtendScriptの実行ログをスプレッドシートへ送る旧構成と修正点

この記事は、ExtendScriptからGoogleスプレッドシートへ実行情報を送った旧構成の記録です。掲載コードには保存先の不一致や入力検証の不足があるため、そのまま導入する完成例ではありません。記録しているのはスクリプト名・使用者名・実行日時で、処理にかかった時間や正常終了を測るコードではありません。

旧構成はJSXがバッチを作り、VBScriptからcurlを起動してGASへ送る方式です。以下ではコードを参照資料として残し、再実装前に直す必要がある点を説明します。今回、送信・受信・台帳追記を通した実機確認は行っていません。

目次

目的:スクリプトの実行履歴をログとして残したい

  • 誰がどのスクリプトをいつ実行したかを記録しておきたい
  • 管理用ログやチーム共有用の履歴をGoogleスプレッドシートに保存したい
スポンサーリンク

Googleスプレッドシートでログ台帳を用意

旧GASの列順はA列がスクリプト名、B列が使用者名、C列が実行日時です。実運用では、本人が把握できる利用目的と記録範囲を決め、必要ならログイン名ではなく作業用IDを使います。クライアントから届いた名前は、本人認証の証拠にはなりません。

ログ台帳として3項目の列を用意したGoogleスプレッドシートの画面例。

旧GASコードと受信側で不足している処理

現在はスプレッドシートの[拡張機能]→[Apps Script]から開きます。次の旧コードを公開して使うことは勧めません。e自体がない直接実行ではエラーになり、p1〜p3の必須値・長さ・型やシートの存在も検査していません。送信元の認証、重複防止、書き込み結果の応答を実装する必要があります。appendRowの仕様では先頭が=の値は数式として扱われるため、入力を文字列として安全に記録する対処も必要です。

function doGet(e) {
  var rowData = {};  
  if (e.parameter == undefined) {
      var getvalue = "undefined"
      rowData.value = getvalue;
      var result = JSON.stringify(rowData);
      return ContentService.createTextOutput(result);
  }else{
      var id = 'スプレットシートのID';
      var sheet = SpreadsheetApp.openById(id).getSheetByName("シート1");
      var array = [ e.parameter.p1 , e.parameter.p2 , e.parameter.p3];
      sheet.appendRow(array);
      var getvalue = "ok"
      rowData.value = getvalue;
      var result = JSON.stringify(rowData);
      return ContentService.createTextOutput(result);
  }
}

GASを公開する前に決めること

  1. 現在の入口は[デプロイ]→[新しいデプロイ]→[ウェブアプリ]です。Google公式のWebアプリ手順を確認します。
  2. 実行ユーザーとアクセスできるユーザーを決めます。「全員」を無条件に選ばず、受信側の認証と入力検証を先に設計します。アクセスを制限しただけでは、旧curlコードに必要な認証処理は追加されません。
  3. 検証用データで、許可された要求だけが1件追記され、不正入力と重複が拒否されるか確認します。以下のURLは旧方式の形を示すだけで、実行用ではありません。
“https://script.google.com/macros/s/発行されたID/exec?p1=スクリプト名.jsx&p2=使用者名&p3=使用日時”

旧送信コードの構成と修正が必要な箇所

ExtendScriptから直接Googleスプレッドシートへ送信するには制限があり、標準のSocket通信ではHTTPSへの直接送信に対応していません。

旧方式では、JSXがFolder.temp/tmp.batを作る一方、VBSは固定ユーザーのDesktop/tmp.batを実行しており、同じファイルを参照していません。またtmp.batという固定名は同時実行で競合します。単にファイルを起動できても、HTTPS通信や台帳への追記が成功したことにはなりません。


curlの導入

Windows 10/11にはcurlが標準搭載されています。まずcurl.exe --versionで実体と版を確認し、不足する場合のみ公式配布を検討します。

次は旧記事の配置パスです。古いcurl 7.65.3を新規導入する指定ではありません。現行環境の実行ファイルを確認し、空白を含むパスや引数を適切に扱う必要があります。

C:\Program Files (x86)\curl-7.65.3-win64-mingw

スクリプト実行ログを送信するExtendScript

  • スクリプト名
  • 使用者名(PCのログインユーザー名)
  • 実行日時

旧コードはこれらをURLのクエリへ連結しますが、各値のエンコードが統一されていません。HOMEPATHを固定文字数で切る処理も一般化できません。再実装では必要な値だけを構造化し、URLへの個人情報の埋め込みを避け、HTTPSの送信本文として渡す構成を検討します。POSTへ変更する場合は送信側だけを変えず、受信側もdoPost(e)へ対応させます。JSON本文ならe.postData.contentsを解析するなど、Content-Typeと読み取り方をそろえます。旧doGet(e)のままでPOSTを受け取れるとは判断しないでください。これらは再実装時の設計条件で、この記事に通し試験済みの新コードを掲載したものではありません。

非表示で起動することと送信確認は別です。まず通信の終了コード・応答本文・台帳の行を確認できる状態で検証します。Google公式のContent Service資料によると、返却内容はscript.googleusercontent.comの一時URLへリダイレクトされます。旧curl例の-Lはこの転送を追うための指定で、認証や書き込み成功を保証する設定ではありません。応答がログイン画面のHTMLではなく、受信側で定義した結果になっていることと、対応する台帳行を確認します。一時URLを次回の送信先として保存せず、デプロイで得た/execのURLを使います。次の旧コードではfile.execute()後の結果確認やタイムアウト、再送時の重複防止はありません。

//スクリプト名取得
var scriptName = decodeURI(File($.fileName));
var scriptName = getName(scriptName, 1);
var scriptName = encodeURI(scriptName);

//使用者名取得
var userName = $.getenv("HOMEPATH");
var userName = getName(userName, 8);
$.writeln(userName);

//名前の抽出
function getName(str, delStr) {
    var str = new String(str).substring(str.lastIndexOf('/') + delStr);
    return str
dObj = new Date();
}

//スクリプト使用日取得
var dObj = new Date();
var y = dObj.getFullYear();
var m = dObj.getMonth() + 1;
var d = dObj.getDate();
var h = dObj.getHours();
var s  = dObj.getMinutes();
var usedDate = y + "/" + m + "/" + d+ "/" + h+ ":" + s;

var webhookUrl = "https://script.google.com/macros/s/******/exec?"+"p1="+scriptName +"&p2="+userName+"&p3="+usedDate;
//変数をダブルクォーテーションで囲う
webhookUrl = "\"" + webhookUrl+ "\"";

function createBatchFile(webhookUrl) {
    const CR = String.fromCharCode(13);
    const BATCH_FILE = new File(Folder.temp + "/tmp.bat");       
    BATCH_FILE.open("w");
    BATCH_FILE.write("@echo off" + CR);
    BATCH_FILE.write("setlocal" + CR);
    BATCH_FILE.write("cd C:/Program Files (x86)/curl-7.65.3-win64-mingw/bin/" + CR);
    BATCH_FILE.write("set URL= " +webhookUrl+CR);       
    BATCH_FILE.write("curl -L %URL%"+CR);  
    BATCH_FILE.write("exit"+CR);  
    //BATCH_FILE.write("pause" + CR);
    BATCH_FILE.close();
    //BATCH_FILE.execute();
}

//実行コマンド
createBatchFile(webhookUrl);

//コマンドプロンプト開きたくないので、VBSをかます
var cmd = "C:\\Library\\Bat\\test.vbs";
var file = new File(cmd);
file.execute();
旧スクリプトへの組み込み例(参照資料)

旧JSX呼び出し例(そのまま実行しない)

//--------------------------------------------------
// スクリプト計測
//--------------------------------------------------
#include 'C:\\Library\\ESTK\\scriptLog.jsx';

var script = $.fileName;
var user = $.getenv("HOMEPATH");
var date = new Date();
$.writeln(script + user + date);
try {
    scriptLog(script, user, date);
} catch (e) {}

//--------------------------------------------------
// 実際の処理    
//--------------------------------------------------
alert("テスト!");

旧JSX参照先(未修正の参照資料)

function scriptLog(script,user,date){

    var scriptName = decodeURI(File(script));
    var scriptName = getName(scriptName, 1);
    var scriptName = encodeURI(scriptName);

    var userName = user;
    var userName = getName(userName, 8);

    function getName(str, delStr) {
        var str = new String(str).substring(str.lastIndexOf('/') + delStr);
        return str
        dObj = new Date();
    }

    var dObj = date;
    var y = dObj.getFullYear();
    var m = dObj.getMonth() + 1;
    var d = dObj.getDate();
    var h = dObj.getHours();
    var s = dObj.getMinutes();
    var usedDate = y + "/" + m + "/" + d + "/" + h + ":" + s;

    var webhookUrl = "https://script.google.com/macros/s/******/exec?" + "p1=" + scriptName + "&p2=" + userName + "&p3=" + usedDate;
    var webhookUrl = webhookUrl.replace( /%/g , "%%" ) ;   
    var webhookUrl = "\"" + webhookUrl + "\"";

    function createBatchFile(webhookUrl) {
        const CR = String.fromCharCode(13);
        const BATCH_FILE = new File(Folder.temp + "/tmp.bat");
        BATCH_FILE.open("w");
        BATCH_FILE.write("@echo off" + CR);
        BATCH_FILE.write("setlocal" + CR);
        BATCH_FILE.write("cd C:/Program Files (x86)/curl-7.65.3-win64-mingw/bin/" + CR);
        BATCH_FILE.write("set URL= " + webhookUrl + CR);
        BATCH_FILE.write("curl -L %URL%" + CR);
        BATCH_FILE.write("exit" + CR);
        //BATCH_FILE.write("pause" + CR);
        BATCH_FILE.close();
        //BATCH_FILE.execute();
    }

    createBatchFile(webhookUrl);
    var cmd = "C:\\Library\\Bat\\test.vbs";
    var file = new File(cmd);
    file.execute();
}

▶ VBScriptの内容(test.vbs)

Set ws = CreateObject("Wscript.Shell") 
ws.run "cmd /c C:\Users\min\Desktop\tmp.bat", vbhide 

実行結果

次の画像は旧記事の掲載記録で、今回の再実行結果ではありません。旧例は実処理の前に送信を呼び、catch内ではエラーを表示しません。そのため、記録があっても本処理の成功を証明できず、記録がなくても原因を特定できません。開始・成功・失敗を分け、所要時間が必要なら開始と終了の差を別項目として測ります。

スクリプトの実行によってログ情報が自動送信され、履歴が蓄積されたGoogleスプレッドシートの実行結果。

補足:日本語のスクリプト名が文字化けする問題

旧記事では、バッチ経由で日本語のスクリプト名を渡す際に問題が出たと記録しています。

%を%%へ変えるのはバッチの解釈に関する対処で、文字コードやURLエンコード全体の解決ではありません。日本語、空白、&、%、引用符を含む値を個別に試す必要があります。

var webhookUrl = webhookUrl.replace( /%/g , "%%" ) ;   

旧BridgeTalk例はGAS連携の完成例ではない

以下はBridgeへ処理を渡す旧サンプルです。末尾はGASではなく外部のHTTPサービスを呼んでおり、GASのHTTPS通信・リダイレクト・認証を確認した例ではありません。

webaccesslibとHttpConnectionの現行対応、エラー時や待機の動作も未検証です。Bridgeを起動するだけで現行GASへ送れると判断せず、このコードは参照資料として扱います。

Adobe Bridgeを経由したBridgeTalkによる通信やGAS連携の処理内容を示す画面。
BridgeTalk.prototype.sendSynch = function(timeout) {
	var self = this;
	self.onResult = function(res) {
	this.result = res.body;
	this.complete = true;
	}
	self.complete = false;
	self.send();

	if (timeout) {
		for (var i = 0; i < timeout; i++) {
			BridgeTalk.pump(); // process any outstanding messages
			if (!self.complete) {
				$.sleep(1000);
			} else {
				break;
			}
		}
	}
	var res = self.result;
	self.result = self.complete = self.onResult = undefined;
	return res;
}
// for typos, provide an alias
BridgeTalk.prototype.sendSync = BridgeTalk.prototype.sendSynch;

function loadUrl(url, timeout) {
    var bt = new BridgeTalk();
    bt.target = 'bridge';
    var httpTimeout = timeout;
 
    var script = '';
    script += "if ( !ExternalObject.webaccesslib )\n";
    script += "  ExternalObject.webaccesslib = new ExternalObject('lib:webaccesslib');\n";
    script += "var response = null;\n";
    script += "var retry = true;\n";
    script += "while (retry) {\n";
    script += "  var http = new HttpConnection('" + url + "') ; \n";
    script += "  http.timeout  = " + httpTimeout + ";\n";
    script += "  http.execute() ;\n";
    script += "  try{\n";
    script += "    response = http.response;\n";
    script += "    retry = false;\n";
    script += "  } catch (e){\n";
    script += "    BridgeTalk.bringToFront('bridge');\n";
    script += "    if (!confirm('There was an error communicating with the server. Would you like to retry?'))\n";
    script += "      retry = false;\n";
    script += "  }\n";
    script += "}\n";
    script += "response;\n";
 	
    bt.body = script;
    return bt.sendSynch(timeout);
}

alert(loadUrl('http://rpc.geocoder.us/service/csv?address=1600+Pennsylvania+Ave,+Washington+DC',50));

旧Socket例とHTTPSの不整合

以下のコードはSocketで平文HTTPを送る例で、TLSを実装していません。末尾のhttps://を指定してもHTTPS通信にはなりません。HTTPステータス取得失敗時のnull処理やレスポンス長、リダイレクトにも対応していないため、そのままダウンロード用に使わないでください。

function getHttpResponse(requests) {
    var parseUrl = function(url) {
        var urlObj = {};
        //[url, scheme, slash, host, port, path, query, fragment] via O'REILLY JavaScript: The Good Parts  
        var url_re = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
        var m = url_re.exec(url);
        urlObj.host = m[3];
        urlObj.port = m[4] || '80';
        urlObj.path = m[5];
        urlObj.query = (m[6]) ? '?' + m[6] : '';
        urlObj.frag = (m[7]) ? '#' + m[7] : '';
        return urlObj;
    }
    var urlObj = parseUrl(requests.url);
    var encoding = requests.encoding || 'UTF-8';
    var method = requests.method || 'GET';
    var auth = (requests.basic_auth != undefined) ? 'Authorization: Basic ' + requests.basic_auth + '\r\n' : '';
    var conn = new Socket;
    conn.timeout = 10;
    if (conn.open(urlObj.host + ':' + urlObj.port, encoding)) {  
        conn.write(method + ' /' + urlObj.path + urlObj.query + urlObj.frag + ' HTTP/1.0\r\n' + 'Host: ' + urlObj.host + '\r\n' + 'User-Agent: ' + 'InDesign/6.0' + '(Macintosh; U; Intel Mac OS X 10_5_6; ja-jp)' + '\r\n' + auth + '\r\n');
        var reply = conn.read(999999);
        conn.close();
        return reply; //ヘッダ込みで返す  
    } else {
        return conn.error;
    }
}

//Webからファイルをダウンロードする  
function downloadFile(url, localFile) {
    var rep = getHttpResponse({
        method: 'GET',
        url: url,
        encoding: 'BINARY',
    });
    if (rep.match(/HTTP.*\d{3}/).toString().indexOf('200') != -1) {
        //レスポンスのヘッダを除去   
        var body = rep.slice(rep.indexOf('\r\n\r\n') + 4);
        //ファイルに書き出し  
        var f = new File(localFile);
        if (f.open("w")) {
            f.encoding = 'BINARY';
            f.write(body);
        }
        f.close();
        return f;
    } else {
        return false;
    }
}

//ソースコードをダウンロード
//var f = downloadFile('https://www.cg-method.com/blog/', '~/Desktop/cg-method.txt');  

//スクリプトをダウンロード
//var f = downloadFile('https://www.cg-method.com/download/5652/', '~/Desktop/CGM_SavePng.jsx');  

//画像をダウンロード
var f = downloadFile('https://secure.gravatar.com/avatar/077a21e2303f1efa3d3a5ecbf5e5193d?s=100&d=mm&r=g', '~/Desktop/suimin.png');

if (f) {
    alert(f.name + 'を保存しました。');
}

Photoshopのほかの操作方法は、まとめから確認できます。

この記事で紹介しているコードは、GitHubのMOTOKI-LLC/cg-method-codeにもまとめています。

ExtendScriptの開発環境は、ほかにも書いています。

スポンサーリンク

まとめ

  • ExtendScript単体でのHTTPS通信には制限が多い
  • 再実装では送信認証・入力検証・応答確認・重複防止が必要
  • 旧BridgeTalk例はGAS連携を検証した例ではなく、旧Socket例はHTTPS非対応
  • 旧コードの保存先不一致を修正しても、送信と台帳追記の通し試験は別途必要

よくある質問:ExtendScriptの実行ログをスプレッドシートに記録する

この記事は、何を作る方法ですか?

JSXのスクリプト名・使用者名・実行日時を送った旧構成の記録です。所要時間や正常終了は測っていません。掲載コードはそのまま導入する完成例ではありません。

ExtendScriptから、直接スプレッドシートに送れないのは、なぜですか?

掲載Socket例はTLS非対応です。HTTPSを扱う外部の送信処理が必要になります。旧バッチ/VBS/curl構成には保存先不一致、入力検証と応答確認の不足があります。

スプレッドシート側は、どう準備しますか?

旧コードの列順はスクリプト名・使用者名・実行日時です。現在の入口は[拡張機能]→[Apps Script]ですが、掲載GASをそのまま公開することは勧めません。認証、必須値・型・長さ、数式として解釈される入力、重複の対策が必要です。

GASは、どうやって公開しますか?

現在の入口は[デプロイ]→[新しいデプロイ]→[ウェブアプリ]です。実行権限とアクセス範囲を決めます。無条件に「全員」を選ばず、アクセス制限時には送信側の認証も実装してください。旧コードに認証処理はありません。

curlは、どう用意しますか?

Windows 10/11ではまずcurl.exe --versionで標準搭載のcurlを確認します。記事の7.65.3のパスは旧記録で、新規導入先の指定ではありません。

ExtendScript側では、何を送りますか? 黒い画面は出ませんか?

旧例は3項目をURLへ連結しますが、非表示起動と送信成功は別です。VBSとJSXのバッチパスも一致していません。再実装後は通信結果と台帳の行を照合し、開始・成功・失敗を区別します。

日本語のスクリプト名が、文字化けするときは?

%から%%への置換だけで文字化け全般が直るわけではありません。UTF-8、値ごとのURLエンコード、バッチの文字解釈を分け、日本語や記号を含む入力で確認してください。

curlやバッチファイルを使わずに、送る方法はありますか? 記事のコードは、どこにありますか?

掲載BridgeTalk例は外部HTTPサービス向けで、現行GASのHTTPS・認証を検証したものではありません。Socket例もTLS非対応です。旧コードは記事内のGitHubリンクにありますが、今回更新していません。今回の通し実行・実機確認は未実施です。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Photoshopのおすすめ書籍

動作要件とPCの確認項目

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次