はじめに
前回の続きになります。
少し長いので3つに分割しています。
こちらはPert3 です。
↓Pert1(前々回) はこちら↓
↓Pert2(前回) はこちら↓
普段使っているLINE、無料で使えるGoogle Apps Script(以下、GAS)、簡単にLLMのアプリが作れると話題のDifyを連携して領収書管理アプリを作っていきます。
記事の内容
• LINEとGASを連携させ、ユーザーのアップロードした画像を処理する方法
• GASを使って画像をDifyに送信する方法
• 領収書の画像からデータを抽出し、Googleスプレッドシートに保存する方法
※Difyは最低限しか使っていない(使わなくてもよい)ので、詳しい使い方は他記事を参考にしてください。
※本格的というより、とりあえずやってみたいよーと思ってる方向けです。
Step 6: GoogleDriveへ画像を保存
Difyを通して無事画像の解析が出来たら、画像をGoogleDriveへ保存します。
こちらも別ファイルへ関数を作成。
/**
* Google Drive
*/
// 保存先GoogleDriveのフォルダID
const FOLDER_ID = '1234567890abcdefghijklmnopqrstuvwxyz';
/**
* 画像Blobを受け取り、Google Driveに保存します。
* @param {Blob} imageBlob 画像データのBlob
* @param {string} fileName 保存するファイルの名前
* @return {GoogleAppsScript.Drive.File} 保存されたファイルオブジェクト
*/
function saveImageToDrive(imageBlob, fileName) {
// フォルダを取得
const folder = DriveApp.getFolderById(FOLDER_ID);
// ファイルを指定フォルダに保存
return folder.createFile(imageBlob).setName(fileName);
}
Step 7: スプレッドシートへ書き込み
7-1: writeMultiDataToSpreadsheet関数
データをスプレッドシートへ書き込む関数を作成します。
配列データを渡し書き込む関数です。
こちらも別ファイルへ関数を作成。
/**
* スプレッドシート
*/
// スプレッドシートIDを指定
const SPREADSHEET_ID = 'abcdefghijklmnopqrstuvwxyz';
// シート名を指定
const SHEET_NAME = 'db';
/**
* 複数データを一括で書き込む
* @param {Array} array 保存する配列データ
* @return void
*/
function writeMultiDataToSpreadsheet(array) {
// スプレッドシートとシートを取得
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
// 現在の最終行を取得
const lastRow = sheet.getLastRow();
// スプレッドシートの次の行にPOSTデータを書き出す
sheet.getRange(lastRow + 1, 1, array.length, array[0].length).setValues(array);
}
このような配列を渡すと
[
["A1","B1","C1"],
["A2","B2","C2"],
["A3","B3","C3"],
]
下記のような形で書き込みします
A列 | B列 | C列 | |
---|---|---|---|
1行目 | A1 | B1 | C1 |
2行目 | A2 | B2 | C2 |
3行目 | A3 | B3 | C3 |
7-2: 書き込みデータについて
今回受け取るJsonはitemsが配列になっているこのようなものになっています。
{
"store":"とっても美味しいお弁当店",
"purchase_date":"2023-10-25",
"time":"20:51",
"items":[
{
"name":"肉野菜炒め弁当",
"quantity":1,
"price":590
},
{
"name":"鰻天丼",
"quantity":1,
"price":690
},
{
"name":"から揚げ",
"quantity":1,
"price":180
}
],
"total":1430,
"payment_method":"PayPay"
}
そのため、下記のような形で書き込むことにしました。
timestamp | purchase_date | time | store | items_name | items_quantity | items_price | total | payment_method | image_url |
---|---|---|---|---|---|---|---|---|---|
データ | データ | データ | データ | n item | n item | n item | データ | データ | データ |
データ | データ | データ | |||||||
データ | データ | データ | |||||||
データ | データ | データm |
itemsの部分は1行目に何個のデータがあるか
2行目以降に各itemデータを記載
併せて、下記2つを追加しています
timestamp :書き込み日時
image_url :保存したGoogleDriveのURL
7-3: 書き込み用の配列生成
getSpreadsheetHeaderIndexObject関数
この後の処理で出てくる内容ですが、どのカラムが何列目なのかを取得する関数
今後もカラムを変えても対応出来るようにスプレッドシートの1行目から取得する
/**
* スプレッドシートの1行目からカラム名と列番号を取り出し、keyがカラム名、valueが列番号となるオブジェクトを作成
* @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 対象となるスプレッドシートのシートオブジェクト
* @returns {Object} カラム名をキー、列番号を値とするマッピングオブジェクト
*
* 出力されるオブジェクトの例:
* {
* 'date': 1,
* 'store': 2,
* 'items_name': 3,
* 'items_price': 4,
* 'total': 5
* }
*/
function getSpreadsheetHeaderIndexObject() {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
const headerRow = sheet.getRange('1:1').getValues()[0].filter((value) => value);
const indexObject = {};
headerRow.forEach((value, index) => {
indexObject[value] = index;
});
return indexObject;
}
generateSheetDataFromJson関数
下記の流れで配列を生成します。
・前項のgetSpreadsheetHeaderIndexObject関数を使って、カラムのインデックスを取得
・itemsの処理用のitem_〇〇のインデックスも抽出
・空の配列作成関数と、item配列作成関数を準備
・1行目の空の配列を準備して、時間とURLとアイテム数を書き込み
・JSONをループ処理
- itemsはアイテム用配列を作って配列ごと追加
- それ以外は1行目の配列に内容を書き込み
/**
* JSONデータからスプレッドシートに書き込むための配列を生成します。
* @param {Object} jsonData スプレッドシートに書き込む元となるJSONデータ
* @returns {Array} スプレッドシートに書き込むための2次元配列
*
* JSONデータの構造:
* {
* 'date': string,
* 'store': string,
* 'items': [
* {
* 'name': string,
* 'price': number
* }
* ],
* 'total': number,
* }
*
* 出力される配列の形式:
* [
* [date, store, '', '', total],
* ['', '', items_name1, items_price1, ''],
* ['', '', items_name2, items_price2, '']
* ]
*/
function generateSheetDataFromJson(jsonData, fileUrl = '') {
const headerIndex = getSpreadsheetHeaderIndexObject();
const itemHeader = Object.keys(headerIndex).filter((key) => {
return key.includes('items');
});
const getNewArray = () => {
return new Array(Object.keys(headerIndex).length).fill(null);
};
const getItemArray = (item) => {
const itemArray = getNewArray();
Object.entries(item).map(([key, value]) => {
itemArray[headerIndex['items_' + key]] = value;
});
return itemArray;
}
const result = [getNewArray()];
result[0][headerIndex['timestamp']] = new Date();
result[0][headerIndex['image_url']] = fileUrl;
itemHeader.map((item_key) => {
result[0][headerIndex[item_key]] = jsonData.items.length + ' item';
});
Object.entries(jsonData).map(([key, value]) => {
if(key === 'items'){
value.map((item) => {
result.push(getItemArray(item));
})
} else {
result[0][headerIndex[key]] = value;
}
});
return result;
}
書き込み処理
これで配列作成→スプレッドシート書き込みと行えるようになりました
// スプレッドシート書き込み
const answerArray = generateSheetDataFromJson(answerJson, file.getUrl());
writeMultiDataToSpreadsheet(answerArray);
Step 8: 結果リプライ処理
ここまで来たらあとは、結果を応答メッセージとして送るだけです。
pert1で作成したreplyMessageToLine
関数を使って、送りましょう。
// メッセージをリプライ
const resultMessage = 'レシートデータの保存が完了しました!\n\n' +
'保存した画像URL: ' + file.getUrl() + '\n\n' +
'読み取ったデータ: ' + JSON.stringify(answerJson);
replyMessageToLine(replyToken, resultMessage);
Step 9: 一連のテスト
個別の処理関数は各ファイルに分割して、メインの処理ファイルは
全部をつなぎ合わせるとこのようになりました。
/**
* LINE webhook からのリクエスト受け取り処理
* @param {GoogleAppsScript.Events.DoPost} e フォーム送信時のイベントオブジェクト
* @return {GoogleAppsScript.Content.TextOutput} レスポンスとして返すメッセージ
*/
function doPost(e) {
if (!e){
Logger.log('None data');
return ContentService.createTextOutput('データがありません');
}
// LINEからのメッセージデータ取得
const json = JSON.parse(e.postData.contents);
const replyToken = json.events[0].replyToken;
const message = json.events[0].message;
const messageType = message.type;
if (typeof replyToken === 'underfined') {
return ContentService.createTextOutput('リプライトークンが見つかりません');
}
if (messageType !== 'image'){
replyMessageToLine(replyToken, '画像を送ってください');
}
try{
// 画像データ準備
const imageBlob = getImageBlobByLineMessage(message);
const extension = getExtensionByMimeType(imageBlob.getContentType());
const fileName = generateTimestampedFileName(extension);
// Difyアプリ利用
const uploadResponse = uploadImageToDify(imageBlob,fileName);
const messageResponse = sendMessageToDify('レシート画像', uploadResponse.id);
const answerJson = JSON.parse(messageResponse.answer);
// GoogleDriveに保存
const file = saveImageToDrive(
imageBlob,
fileName
);
// スプレッドシート書き込み
const answerArray = generateSheetDataFromJson(answerJson, file.getUrl());
writeMultiDataToSpreadsheet(answerArray);
// メッセージをリプライ
const resultMessage = 'レシートデータの保存が完了しました!\n\n' +
'保存した画像URL: ' + file.getUrl() + '\n\n' +
'読み取ったデータ: ' + JSON.stringify(answerJson);
replyMessageToLine(replyToken, resultMessage);
} catch(error){
replyMessageToLine(replyToken, error.message);
}
}
実際にラインから画像を送ってみると...
きました👏
さいごに
- 画像からテキストデータを読み取って保存するだけというシンプルな内容ですが、以外と画像処理に手こずってしまいました
- GASを使ってWebhookを処理するのはとっても簡単ですが、署名検証出来ないため他の選択肢を考える必要がありそうです
- 今回は、久しぶりにGASを触ってみて、あれやこれやと勉強になったのでよかったです