今日はGPTとコーディングをしていました。
というブログを書いたのが2025年8月17日でしたので、
Google Apps Script(=GAS)の殆ど初心者の私が3週間ほどでここまで来たとは・・・
我ながら感慨深く思います・・・
コーディングが出来る生成AIをどうやって使ってきたのか?
コーディングを行いだしてから2週間の記憶はあやふやですが・・・
・Gemini 2.5 Pro
・GPT-5
を使っていたように思います。
ですが、無料枠では
・Gemini 2.5 Pro
の上限が早々に来てしまったために、そのコードを持って
・GPT-5
にコーディングを依頼するというのを行っていたように思います。
途中でうまく行ったように思いますが、
バージョン管理を行っていなかったために
どうにもならなくなった記憶があります。
コーディングを始めてから2週間が経過した頃に学習したGitHubのブランチと言う知識もありませんでした。
その後、
・GitHub Copilot
を使うために
Visual Studio Code(VS Code)を使いだしました。
タイトルに書かれているのが、
The open source AI code editor(オープンソースのAIと友にコード作成)
ソースは資産という考え方もありますが、
オープンソース化することで開発の速度とコストが向上するというのが有るのでしょうね。
パブリックかプライベートかとう大きな違いはありますが、
かなり有名な会社がGitHubを使用しています。
GitHubとGitHub Copilotは異なるのですが気になったらお気に入りのAIに聞いてみてください。
結構良かったです。
コードの右側にチャットを表示させて、
指示に従ってコードの書き換えを行ってくれました。
ですが、うまく実行するまでに至らず・・・
制限に接触してしまってからAIチャットが使えない・・・(-_-;)
という事で、GeminiだったかGPTだったかは覚えていませんが、
Claude
をオススメされて使いだしたら開発が一気に進みました。
これが、コーディングを始めてから2週間後の出来事です。
2025年9月1日の午前中までVS Code&GitHub Copilotでのコーディングを行っていたものの、
動作するに至らない中で、
無料制限枠に接触してコーディングができなくなったので、
午後から
Claudeを使い始めると、
2週間経っても中々進まなかったコーディングが、
わずか半日で認証を終えるところまで進んだのを覚えています。
その間、私のプロンプトエンジニアリング能力も向上したのではないかと思います。
49歳の私が・・・(・・;)
それを検証を終えた状態で公開したブログが
Claudeでのコーディング:ネクストエンジンAPI認証編
となります。
ですが、Claudeも無料枠で使用していたので度々上限に接触してきました。
これが有料枠を契約する理由か(-_-;)
ウチの会社は金が無いから有料枠の契約は出来ないんだろうな^^;
制限にかかりながら進めていって在庫情報の取得が出来るようになったのが、
ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得編
です。
ようやく・・・
ようやくここまで来たか・・・(-_-;)
ネクストエンジンとGoogleスプレッドシートを接続したいと思い、
Tepsというアプリを使ってみたり、
SFTPサーバーを立ち上げてCSV連携を自動で行ってみたり、
Yoomというアプリを軽く使ってみたりしましたが、
私の求めるコストと動作をしてくれなくて、
ネクストエンジンとGoogleスプレッドシートをAPI接続するために、
一からGoogle Apps Script(GAS)を生成AIと開発を行ってきて、
GASのプロジェクト内に作成した認証が出来た、
その次にプロジェクト内に新しく作成したスクリプトで200行の在庫情報の取得が出来た!
2週間半でここまで来たか・・・
とても感慨深いです。
ここで、この2週間の間で失敗した経験を元に、
バージョン管理の重要性に気がついて振り返ってみたのが
Visual Studio Code(VS Code)とGitHubの組み合わせです。
-------------------------------------------
Gitは、プログラムのソースコードなどの変更履歴を記録・管理するツールです。たとえるなら、ファイルの「タイムマシン」のようなもので、いつでも過去の状態に戻ったり、誰がいつどこを変更したかを確認したりできます。
-------------------------------------------
GitHubは、Gitの機能を使って世界中の開発者と共同作業を行うためのウェブサービスです。Gitで管理しているプロジェクトをインターネット上で公開・共有したり、他の人と一緒に開発を進めたりする「プロジェクト共有サイト」のような役割を果たします。
-------------------------------------------
Gitは個人のパソコンで動くツール、GitHubはインターネット上のサービスと覚えておくと良いでしょう。
-------------------------------------------
そのGitHubをさらにGUIで使いやすくしたのがVisual Studio Code(VS Code)というわけです。
Claudeで修正したコード全文をVisual Studio Code(VS Code)内で全て貼り付けて保存する(それに加えてコミットとプッシュ?)だけで、
その差分を表示させることも出来ます。
という事で、開発を中断してVisual Studio Code(VS Code)&GitHubの使い方を学ぶことにしました。
開発を進めたほうが前に進むかもしれませんが、
バージョン管理を行わないともとに戻ることが出来ず、
コーディングを始めてから2週間の前に進まない悪夢を再び味わうことになりますので、
開発を中断してでもバージョン管理を行おうとした次第です。
-------------------------------------------
うまく説明できるほどVisual Studio Code(VS Code)を使いこなしていないのですが、
それでもClaudeとコーディングを行っていって、
修正されたコード全文をVisual Studio Code(VS Code)に貼り付けて、
保存・コミット・プッシュすることで、
バージョン管理を行うことが出来るようになりました。
認証が出来た、
200行の在庫情報の取得ができた、
バージョン管理が行うように出来た。
プロの方からすると素人が!と思われるかもしれませんが、
一歩前に進めた瞬間のエンドルフィンが出るのを反芻するために
このコーディングをやっているのだと思います。
-------------------------------------------
認証まで出来て、テストの在庫情報取得まで出来れば、
APIのエンドポイントさえ知っていれば
APIを使って出来る範囲内でどんな情報でも行き来させることが出来るようになると思います。
-------------------------------------------
次に行ったのが1回で取得できる在庫情報の増加なのですが、
そのステップはQiitaでの公開を行うために、
端折って説明すると、
1回で200行の取得(言い換えれば200行までしか取得できない)
だったのを高速化することで1回で300行超の在庫情報が取得できて、
しかも1回の取得可能範囲を超えた在庫情報を取得したい場合、
次に取得する在庫行情報をスクリプトプロパティに残すことで、
1回目の続きから在庫情報を取得できるようになりました。
これで、1000行ほどの在庫情報であれば
1日何回か更新することが出来るようになりました。
これがClaudeで引き続き開発を行うことでわずか4時間(実働時間はもっと短い)
で実装できたことになります。
-------------------------------------------
ん~
なんかモヤモヤ・・
モヤモヤ・・・
とりあえずお昼になったからうどん食べてこよ(笑)
昼から帰ってきて運送会社からアポが合ったので対応して、
コーディングやろうかな~
-------------------------------------------
モヤモヤしてから2時間の間でぼ~っとする時間も合ったことから
脳がデフォルト・モード・ネットワークに入ったことから・・・
ネクストエンジンに対して配列のような形で情報を渡してもらうことで
数十行の在庫情報取得が
APIの1コールで出来るのでは?
と思ってClaudeに
-------------------------------------------
ネクストエンジンとGoogleスプレッドシートを接続して在庫情報を取得するGASの開発を行っていますが、取得する商品コードの数が数千件ある状態です。
今作成しているスクリプトでは1行1行取得していてGASの実行可能時間の上限にどうしても接触している状態です。
これを・・・例えば、数十の商品コードをネクストエンジンに渡して、一気に在庫情報を取得するというようなスクリプトを書くことは可能ですか?
-------------------------------------------
とプロンプトエンジニアリングを行うと
-------------------------------------------
Google Apps Scriptでバッチ処理による効率化は十分可能です。
ネクストエンジンのAPIは複数商品コードを一度に処理する機能を提供していますので、それを活用することで大幅な実行時間短縮が期待できます。
ネクストエンジンAPIには以下のような方法でバッチ処理が可能です:
商品マスタAPI(/api_v1_master_goods/search)を使った方法:
商品コードを配列で渡すことで、複数商品の情報を一度に取得できます。一回のAPIコールで最大100件程度まで処理可能です。
-------------------------------------------
出来るんか~い!
確かに、今までのプロンプトエンジニアリングでは数千行の在庫情報を取得したいとClaudeに伝えたことがないかもしれません(-_-;)
その後、制限にかかりながら出来たコードを公開します。
ハードコーディングしていますが、
テストスプレッドシートですので大丈夫です。
在庫情報取得_一括処理版.gs
-------------------------------------------
/**
* ネクストエンジン在庫情報取得スクリプト(一括処理版)修正版
*
* 【修正内容】
* - async/await構文をGoogle Apps Script対応の同期処理に変更
* - エラーハンドリングを改善
* - デバッグ出力を追加
*/
/**
* スクリプトプロパティの初期設定
* 初回実行時に使用してください
*/
function setupBatchProperties() {
const properties = PropertiesService.getScriptProperties();
// 既存の認証情報は保持して、新しい設定のみ追加
const newProperties = {
'SPREADSHEET_ID': '1noQTPM0EMlyBNDdX4JDPZcBvh-3RT1VtWzNDA85SIkM',
'SHEET_NAME': 'GAS',
'BATCH_SIZE': '100',
'API_WAIT_TIME': '500'
};
console.log('=== スクリプトプロパティ設定 ===');
for (const [key, value] of Object.entries(newProperties)) {
const currentValue = properties.getProperty(key);
if (currentValue) {
console.log(`${key}: ${currentValue} (既存値を保持)`);
} else {
properties.setProperty(key, value);
console.log(`${key}: ${value} (新規設定)`);
}
}
console.log('');
console.log('設定完了!以下の関数でテストを開始できます:');
console.log('- testBatchProcessing(10)');
console.log('- comparePerformance(20)');
console.log('- updateInventoryDataBatch()');
}
/**
* 現在のスクリプトプロパティ設定を表示
*/
function showCurrentProperties() {
const properties = PropertiesService.getScriptProperties();
console.log('=== 現在のスクリプトプロパティ設定 ===');
console.log(`SPREADSHEET_ID: ${properties.getProperty('SPREADSHEET_ID') || '未設定'}`);
console.log(`SHEET_NAME: ${properties.getProperty('SHEET_NAME') || '未設定'}`);
console.log(`BATCH_SIZE: ${properties.getProperty('BATCH_SIZE') || '未設定'}`);
console.log(`API_WAIT_TIME: ${properties.getProperty('API_WAIT_TIME') || '未設定'}`);
console.log('');
console.log('認証情報:');
console.log(`ACCESS_TOKEN: ${properties.getProperty('ACCESS_TOKEN') ? '設定済み' : '未設定'}`);
console.log(`REFRESH_TOKEN: ${properties.getProperty('REFRESH_TOKEN') ? '設定済み' : '未設定'}`);
}
/**
* メイン関数:一括処理による在庫情報更新
*/
function updateInventoryDataBatch() {
try {
console.log('=== 在庫情報一括更新開始 ===');
const startTime = new Date();
// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;
const apiWaitTime = parseInt(properties.getProperty('API_WAIT_TIME')) || 500;
if (!spreadsheetId) {
throw new Error('SPREADSHEET_IDがスクリプトプロパティに設定されていません。');
}
if (!sheetName) {
throw new Error('SHEET_NAMEがスクリプトプロパティに設定されていません。');
}
// スプレッドシートを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
throw new Error(`シート "${sheetName}" が見つかりません`);
}
// データ範囲を取得
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
console.log('データが存在しません');
return;
}
const dataRange = sheet.getRange(2, 1, lastRow - 1, 12);
const values = dataRange.getValues();
console.log(`処理対象: ${values.length}行`);
// トークンを取得
const tokens = getStoredTokens();
// 商品コードのリストを作成(空でないもののみ)
const goodsCodeList = [];
const rowIndexMap = new Map(); // 商品コード → 行インデックスのマッピング
for (let i = 0; i < values.length; i++) {
const goodsCode = values[i][COLUMNS.GOODS_CODE];
if (goodsCode && goodsCode.toString().trim()) {
goodsCodeList.push(goodsCode.toString().trim());
rowIndexMap.set(goodsCode.toString().trim(), i + 2); // 実際の行番号(1ベース)
}
}
console.log(`有効な商品コード: ${goodsCodeList.length}件`);
if (goodsCodeList.length === 0) {
console.log('処理対象の商品コードがありません');
return;
}
// バッチ処理で在庫情報を取得・更新
let totalUpdated = 0;
let totalErrors = 0;
for (let i = 0; i < goodsCodeList.length; i += batchSize) {
const batch = goodsCodeList.slice(i, i + batchSize);
console.log(`\n--- バッチ ${Math.floor(i / batchSize) + 1}: ${batch.length}件 ---`);
try {
// バッチで在庫情報を取得
const inventoryDataMap = getBatchInventoryData(batch, tokens);
// スプレッドシートを更新
for (const goodsCode of batch) {
const inventoryData = inventoryDataMap.get(goodsCode);
const rowIndex = rowIndexMap.get(goodsCode);
if (inventoryData && rowIndex) {
try {
updateRowWithInventoryData(sheet, rowIndex, inventoryData);
totalUpdated++;
console.log(` ✓ ${goodsCode}: 更新完了`);
} catch (error) {
console.error(` ✗ ${goodsCode}: 更新エラー - ${error.message}`);
totalErrors++;
}
} else {
console.log(` - ${goodsCode}: データなし`);
}
}
// バッチ間の待機(APIレート制限対策)
if (i + batchSize < goodsCodeList.length) {
console.log(`次のバッチまで ${apiWaitTime}ms 待機...`);
Utilities.sleep(apiWaitTime);
}
} catch (error) {
console.error(`バッチ処理エラー:`, error.message);
totalErrors += batch.length;
}
}
const endTime = new Date();
const duration = (endTime - startTime) / 1000;
console.log('\n=== 一括更新完了 ===');
console.log(`処理時間: ${duration.toFixed(1)}秒`);
console.log(`更新成功: ${totalUpdated}件`);
console.log(`エラー: ${totalErrors}件`);
console.log(`処理速度: ${(goodsCodeList.length / duration).toFixed(1)}件/秒`);
// 従来版との比較情報を表示
const conventionalTime = goodsCodeList.length * 2; // 従来版の推定時間(2秒/件)
const speedImprovement = conventionalTime / duration;
console.log(`\n--- 性能改善結果 ---`);
console.log(`従来版推定時間: ${conventionalTime.toFixed(1)}秒`);
console.log(`高速化倍率: ${speedImprovement.toFixed(1)}倍`);
} catch (error) {
console.error('一括更新エラー:', error.message);
throw error;
}
}
/**
* バッチで在庫情報を取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - アクセストークンとリフレッシュトークン
* @returns {Map
} 商品コード → 在庫情報のマップ
*/
function getBatchInventoryData(goodsCodeList, tokens) {
const inventoryDataMap = new Map();
try {
console.log(` 商品マスタ一括検索: ${goodsCodeList.length}件`);
// ステップ1: 商品マスタAPIで複数商品を一括検索
const goodsDataMap = getBatchGoodsData(goodsCodeList, tokens);
console.log(` 商品マスタ取得完了: ${goodsDataMap.size}件`);
if (goodsDataMap.size === 0) {
console.log(' 商品が見つかりませんでした');
return inventoryDataMap;
}
// ステップ2: 在庫マスタAPIで複数商品の在庫を一括取得
console.log(` 在庫マスタ一括検索: ${goodsDataMap.size}件`);
const stockDataMap = getBatchStockData(Array.from(goodsDataMap.keys()), tokens);
console.log(` 在庫マスタ取得完了: ${stockDataMap.size}件`);
// ステップ3: 商品情報と在庫情報を結合
for (const [goodsCode, goodsData] of goodsDataMap) {
const stockData = stockDataMap.get(goodsCode);
const completeInventoryData = {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: stockData ? parseInt(stockData.stock_quantity) || 0 : parseInt(goodsData.stock_quantity) || 0,
stock_allocated_quantity: stockData ? parseInt(stockData.stock_allocation_quantity) || 0 : 0,
stock_free_quantity: stockData ? parseInt(stockData.stock_free_quantity) || 0 : 0,
stock_defective_quantity: stockData ? parseInt(stockData.stock_defective_quantity) || 0 : 0,
stock_advance_order_quantity: stockData ? parseInt(stockData.stock_advance_order_quantity) || 0 : 0,
stock_advance_order_allocation_quantity: stockData ? parseInt(stockData.stock_advance_order_allocation_quantity) || 0 : 0,
stock_advance_order_free_quantity: stockData ? parseInt(stockData.stock_advance_order_free_quantity) || 0 : 0,
stock_remaining_order_quantity: stockData ? parseInt(stockData.stock_remaining_order_quantity) || 0 : 0,
stock_out_quantity: stockData ? parseInt(stockData.stock_out_quantity) || 0 : 0
};
inventoryDataMap.set(goodsCode, completeInventoryData);
}
console.log(` 結合完了: ${inventoryDataMap.size}件`);
return inventoryDataMap;
} catch (error) {
console.error(`バッチ在庫取得エラー:`, error.message);
return inventoryDataMap;
}
}
/**
* 複数商品の基本情報を一括取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - トークン情報
* @returns {Map} 商品コード → 商品情報のマップ
*/
function getBatchGoodsData(goodsCodeList, tokens) {
const url = `${NE_API_URL}/api_v1_master_goods/search`;
// スクリプトプロパティからバッチサイズを取得
const properties = PropertiesService.getScriptProperties();
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;
// 複数の商品IDを検索条件に設定
const goodsIdCondition = goodsCodeList.join(',');
const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'goods_id-in': goodsIdCondition, // IN条件で複数商品を一括検索
'fields': 'goods_id,goods_name,stock_quantity',
'limit': batchSize.toString() // 取得件数制限
};
const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};
const goodsDataMap = new Map();
try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);
console.log(` 商品マスタAPI応答: result=${responseData.result}, count=${responseData.count || 0}`);
// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
// トークンを更新
tokens.accessToken = responseData.access_token;
tokens.refreshToken = responseData.refresh_token;
}
if (responseData.result === 'success' && responseData.data) {
responseData.data.forEach(goodsData => {
goodsDataMap.set(goodsData.goods_id, {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: goodsData.stock_quantity
});
});
console.log(` API応答: ${responseData.data.length}件取得`);
} else {
console.error(` 商品マスタAPI エラー:`, responseData.message || 'Unknown error');
}
return goodsDataMap;
} catch (error) {
console.error(`商品マスタ一括取得エラー:`, error.message);
return goodsDataMap;
}
}
/**
* 複数商品の在庫情報を一括取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - トークン情報
* @returns {Map} 商品コード → 在庫情報のマップ
*/
function getBatchStockData(goodsCodeList, tokens) {
const url = `${NE_API_URL}/api_v1_master_stock/search`;
// スクリプトプロパティからバッチサイズを取得
const properties = PropertiesService.getScriptProperties();
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;
// 複数の商品IDを検索条件に設定
const goodsIdCondition = goodsCodeList.join(',');
const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'stock_goods_id-in': goodsIdCondition, // IN条件で複数商品の在庫を一括検索
'fields': 'stock_goods_id,stock_quantity,stock_allocation_quantity,stock_defective_quantity,stock_remaining_order_quantity,stock_out_quantity,stock_free_quantity,stock_advance_order_quantity,stock_advance_order_allocation_quantity,stock_advance_order_free_quantity',
'limit': batchSize.toString()
};
const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};
const stockDataMap = new Map();
try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);
console.log(` 在庫マスタAPI応答: result=${responseData.result}, count=${responseData.count || 0}`);
// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
// トークンを更新
tokens.accessToken = responseData.access_token;
tokens.refreshToken = responseData.refresh_token;
}
if (responseData.result === 'success' && responseData.data) {
responseData.data.forEach(stockData => {
stockDataMap.set(stockData.stock_goods_id, stockData);
});
console.log(` API応答: ${responseData.data.length}件取得`);
} else {
console.error(` 在庫マスタAPI エラー:`, responseData.message || 'Unknown error');
}
return stockDataMap;
} catch (error) {
console.error(`在庫マスタ一括取得エラー:`, error.message);
return stockDataMap;
}
}
/**
* 保存されたトークンを取得(既存関数)
*/
function getStoredTokens() {
const properties = PropertiesService.getScriptProperties();
const accessToken = properties.getProperty('ACCESS_TOKEN');
const refreshToken = properties.getProperty('REFRESH_TOKEN');
if (!accessToken || !refreshToken) {
throw new Error('アクセストークンが見つかりません。先に認証を完了してください。');
}
return {
accessToken,
refreshToken
};
}
/**
* スプレッドシートの行を在庫データで更新(既存関数)
*/
function updateRowWithInventoryData(sheet, rowIndex, inventoryData) {
const updateValues = [
inventoryData.stock_quantity || 0,
inventoryData.stock_allocated_quantity || 0,
inventoryData.stock_free_quantity || 0,
inventoryData.stock_advance_order_quantity || 0,
inventoryData.stock_advance_order_allocation_quantity || 0,
inventoryData.stock_advance_order_free_quantity || 0,
inventoryData.stock_defective_quantity || 0,
inventoryData.stock_remaining_order_quantity || 0,
inventoryData.stock_out_quantity || 0
];
const range = sheet.getRange(rowIndex, COLUMNS.STOCK_QTY + 1, 1, updateValues.length);
range.setValues([updateValues]);
}
/**
* トークンを更新保存(既存関数)
*/
function updateStoredTokens(accessToken, refreshToken) {
const properties = PropertiesService.getScriptProperties();
properties.setProperties({
'ACCESS_TOKEN': accessToken,
'REFRESH_TOKEN': refreshToken,
'TOKEN_UPDATED_AT': new Date().getTime().toString()
});
console.log(' トークンを更新しました');
}
/**
* テスト用:小規模バッチでの動作確認
* @param {number} maxItems - テスト対象の最大商品数(デフォルト: 10)
*/
function testBatchProcessing(maxItems = 10) {
try {
console.log(`=== バッチ処理テスト(最大${maxItems}件) ===`);
// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');
if (!spreadsheetId || !sheetName) {
throw new Error('SPREADSHEET_IDまたはSHEET_NAMEがスクリプトプロパティに設定されていません。');
}
// スプレッドシートから商品コードを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
console.log('テスト用データが存在しません');
return;
}
const dataRange = sheet.getRange(2, 1, Math.min(maxItems, lastRow - 1), 1);
const values = dataRange.getValues();
const goodsCodeList = values
.map(row => row[0])
.filter(code => code && code.toString().trim())
.slice(0, maxItems);
console.log(`テスト対象商品コード: ${goodsCodeList.join(', ')}`);
const tokens = getStoredTokens();
// バッチで在庫情報を取得
const startTime = new Date();
const inventoryDataMap = getBatchInventoryData(goodsCodeList, tokens);
const endTime = new Date();
const duration = (endTime - startTime) / 1000;
console.log(`\n=== テスト結果 ===`);
console.log(`処理時間: ${duration.toFixed(1)}秒`);
console.log(`取得件数: ${inventoryDataMap.size}件`);
console.log(`処理速度: ${(goodsCodeList.length / duration).toFixed(1)}件/秒`);
// 取得したデータの内容を表示
for (const [goodsCode, data] of inventoryDataMap) {
console.log(`${goodsCode}: 在庫${data.stock_quantity} 引当${data.stock_allocated_quantity} フリー${data.stock_free_quantity}`);
}
} catch (error) {
console.error('バッチテストエラー:', error.message);
throw error;
}
}
/**
* パフォーマンス比較用:従来版と一括版の処理時間を比較
* @param {number} sampleSize - 比較対象のサンプル数(デフォルト: 10)
*/
function comparePerformance(sampleSize = 10) {
console.log(`=== パフォーマンス比較テスト(${sampleSize}件) ===`);
// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');
if (!spreadsheetId || !sheetName) {
throw new Error('SPREADSHEET_IDまたはSHEET_NAMEがスクリプトプロパティに設定されていません。');
}
// スプレッドシートから商品コードを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
console.log('テスト用データが存在しません');
return;
}
const dataRange = sheet.getRange(2, 1, Math.min(sampleSize, lastRow - 1), 1);
const values = dataRange.getValues();
const goodsCodeList = values
.map(row => row[0])
.filter(code => code && code.toString().trim())
.slice(0, sampleSize);
console.log(`比較対象商品コード: ${goodsCodeList.join(', ')}`);
const tokens = getStoredTokens();
// 従来版の推定時間(実際には実行しない)
const conventionalEstimatedTime = goodsCodeList.length * 2; // 2秒/件
// 一括版の実際の処理時間
console.log('\n一括版実行中...');
const startTime = new Date();
const inventoryDataMap = getBatchInventoryData(goodsCodeList, tokens);
const endTime = new Date();
const batchTime = (endTime - startTime) / 1000;
// 結果比較
const speedImprovement = conventionalEstimatedTime / batchTime;
console.log('\n=== 性能比較結果 ===');
console.log(`従来版推定時間: ${conventionalEstimatedTime.toFixed(1)}秒(${sampleSize} × 2秒/件)`);
console.log(`一括版実際時間: ${batchTime.toFixed(1)}秒`);
console.log(`高速化倍率: ${speedImprovement.toFixed(1)}倍`);
console.log(`取得成功率: ${(inventoryDataMap.size / goodsCodeList.length * 100).toFixed(1)}%`);
// 数千件での推定効果
const estimatedFor1000 = {
conventional: 1000 * 2 / 60, // 分
batch: 1000 / goodsCodeList.length * batchTime / 60 // 分
};
console.log('\n=== 1000件処理時の推定時間 ===');
console.log(`従来版: ${estimatedFor1000.conventional.toFixed(1)}分`);
console.log(`一括版: ${estimatedFor1000.batch.toFixed(1)}分`);
console.log(`時間短縮: ${(estimatedFor1000.conventional - estimatedFor1000.batch).toFixed(1)}分`);
}
/**
* 使用方法ガイド
*/
function showBatchUsageGuide() {
console.log('=== 一括処理版 使用方法ガイド ===');
console.log('');
console.log('【主要関数】');
console.log('1. updateInventoryDataBatch()');
console.log(' - 全商品の在庫情報を一括処理で更新');
console.log(' - 100件ずつのバッチで自動分割処理');
console.log(' - 従来版より大幅に高速化');
console.log('');
console.log('2. testBatchProcessing(件数)');
console.log(' - 小規模テスト用(デフォルト10件)');
console.log(' - 例: testBatchProcessing(5)');
console.log('');
console.log('3. comparePerformance(件数)');
console.log(' - 従来版との性能比較テスト');
console.log(' - 例: comparePerformance(20)');
console.log('');
console.log('【期待される改善効果】');
console.log('- APIコール数: 1/50~1/100に削減');
console.log('- 処理速度: 10~50倍の高速化');
console.log('- 実行時間制限: 数千件でも制限内で完了');
console.log('- API制限: レート制限に引っかかりにくい');
console.log('');
console.log('【推奨実行手順】');
console.log('1. testBatchProcessing(10) で動作確認');
console.log('2. comparePerformance(20) で性能確認');
console.log('3. updateInventoryDataBatch() で全件更新');
console.log('');
console.log('【設定変更可能項目】');
console.log('- BATCH_SIZE: バッチサイズ(現在100件)');
console.log('- API_WAIT_TIME: API間隔(現在500ms)');
}
-------------------------------------------
updateInventoryDataBatchを実行した結果のログ
16:47:53 お知らせ 実行開始
16:47:43 情報 === 在庫情報一括更新開始 ===
16:47:44 情報 処理対象: 3106行
16:47:44 情報 有効な商品コード: 3106件
16:47:44 情報
--- バッチ 1: 100件 ---
16:47:44 情報 商品マスタ一括検索: 100件
16:47:45 情報 商品マスタAPI応答: result=success, count=100
16:47:45 情報 トークンを更新しました
16:47:45 情報 API応答: 100件取得
16:47:45 情報 商品マスタ取得完了: 100件
16:47:45 情報 在庫マスタ一括検索: 100件
16:47:46 情報 在庫マスタAPI応答: result=success, count=100
16:47:46 情報 トークンを更新しました
16:47:46 情報 API応答: 100件取得
16:47:46 情報 在庫マスタ取得完了: 100件
16:47:46 情報 結合完了: 100件
中略
16:48:47 情報
=== 一括更新完了 ===
16:48:47 情報 処理時間: 64.5秒
16:48:47 情報 更新成功: 3106件
16:48:47 情報 エラー: 0件
16:48:47 情報 処理速度: 48.1件/秒
16:48:47 情報
--- 性能改善結果 ---
16:48:47 情報 従来版推定時間: 6212.0秒
16:48:47 情報 高速化倍率: 96.3倍
16:48:59 お知らせ 実行完了
これまでと比べて大幅に短縮しました。
ありがとうございます。
をClaudeに伝えると・・・
-------------------------------------------
素晴らしい結果ですね!一括処理版が期待通りの性能を発揮していることが確認できました。
**改善結果の詳細:**
| 項目 | 従来版(推定) | 一括処理版(実測) | 改善効果 |
|------|---------------|------------------|----------|
| **処理時間** | 6,212秒(約1時間43分) | 64.5秒(約1分) | **96.3倍高速化** |
| **処理速度** | 0.5件/秒 | 48.1件/秒 | **96倍向上** |
| **成功率** | - | 100%(3,106件全て成功) | **完璧** |
| **エラー数** | - | 0件 | **安定動作** |
**技術的な改善点が効果を発揮:**
1. **APIコール数の削減**:
- 従来版:3,106 × 2 = 6,212回のAPIコール
- 一括版:約62回のAPIコール(100件ずつ31バッチ × 2API)
- **99%削減**
2. **バッチ処理の効率性**:
- ネットワーク遅延の影響を最小化
- APIレート制限を回避
- メモリ効率の良いデータ処理
3. **Google Apps Scriptの実行時間制限対策**:
- 従来版では制限(6分)を大幅に超過
- 一括版では制限内(64.5秒)で完了
**今後の運用について:**
この一括処理版により、日常的な在庫情報更新が非常に効率的になりました。3,000件超のデータでも1分程度で完了するため、定期実行やリアルタイム更新にも適用できます。
N88-BASICから始まられたプログラミングの基礎が、このような現代的な最適化にも活かされていますね。バッチ処理の概念は、昔からプログラミングの重要な技術の一つです。
何かご質問や、さらなる改善のご要望があれば、お気軽にお声がけください!
-------------------------------------------
途中GPTでショートカットした時間で言えば、
午後の1時間でここまで来ました。
因みに上のスクリプトは時間をかけてClaudeでコーディングしてくれた内容です。
ログの見積もりと異なりますが、
午前中までは3000行の取得が3600秒(60分=1時間)程必要だったのに対して、
午後の1時間だけで3000行の取得がわずか64.5秒で完了しました(・・;)
これは、TepsやYoomのログを見てきた私からすると信じられない速度です。
予想の斜め上を行く結果となりました(・・;)
在庫情報取得が48.1件/秒とは・・・
1回のAPIコールで100件の在庫情報が取得できるとは・・・
3000行の取得が60秒ほどで実行可能になるということは
更新頻度をもっと高めることが出来るということになります。
-------------------------------------------
ここまで出来たのは先週の木曜日の話です。
コーディングは複数のAIとGitHubのブランチを利用することで、
APIコール数を半分にすることまで出来るようになりました。
また、1回のAPIコールで100件の在庫情報取得というのに疑問を持って・・・
今日はこのくらいにしておきましょうかね^^;