6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[LINE×GAS×Dify連携]領収書管理アプリを作ってみたpert3 〜スプレッドシート書き込み〜

Last updated at Posted at 2024-10-27

はじめに

前回の続きになります。

少し長いので3つに分割しています。
こちらはPert3 です。

↓Pert1(前々回) はこちら↓

↓Pert2(前回) はこちら↓

普段使っているLINE、無料で使えるGoogle Apps Script(以下、GAS)、簡単にLLMのアプリが作れると話題のDifyを連携して領収書管理アプリを作っていきます。

記事の内容

• LINEとGASを連携させ、ユーザーのアップロードした画像を処理する方法
• GASを使って画像をDifyに送信する方法
• 領収書の画像からデータを抽出し、Googleスプレッドシートに保存する方法
※Difyは最低限しか使っていない(使わなくてもよい)ので、詳しい使い方は他記事を参考にしてください。
※本格的というより、とりあえずやってみたいよーと思ってる方向けです。

Step 6: GoogleDriveへ画像を保存

Difyを通して無事画像の解析が出来たら、画像をGoogleDriveへ保存します。
こちらも別ファイルへ関数を作成。

constants.gs
/**
 * Google Drive
 */
// 保存先GoogleDriveのフォルダID
const FOLDER_ID = '1234567890abcdefghijklmnopqrstuvwxyz';
Drive.gs
/**
 * 画像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関数

データをスプレッドシートへ書き込む関数を作成します。
配列データを渡し書き込む関数です。
こちらも別ファイルへ関数を作成。

constants.gs
/**
 * スプレッドシート
 */
// スプレッドシートIDを指定
const SPREADSHEET_ID = 'abcdefghijklmnopqrstuvwxyz';
// シート名を指定
const SHEET_NAME = 'db';
spreadsheet.gs
/**
 * 複数データを一括で書き込む
 * @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行目から取得する

spreadsheet.gs
/**
 * スプレッドシートの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行目の配列に内容を書き込み

spreadsheet.gs
/**
 * 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: 一連のテスト

個別の処理関数は各ファイルに分割して、メインの処理ファイルは
全部をつなぎ合わせるとこのようになりました。

main.gs
/**
 * 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);
  }
}

実際にラインから画像を送ってみると...

image.png

きました👏

image.png

さいごに

  • 画像からテキストデータを読み取って保存するだけというシンプルな内容ですが、以外と画像処理に手こずってしまいました
  • GASを使ってWebhookを処理するのはとっても簡単ですが、署名検証出来ないため他の選択肢を考える必要がありそうです
  • 今回は、久しぶりにGASを触ってみて、あれやこれやと勉強になったのでよかったです
6
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?