前回は、「プロンプトとモデルを調整してJSON形式で応答結果を得る」を紹介しました。
ここまでくれば、あとはシンプルGoogle Apps Script(以降GAS)によるGoogleドライブ操作です。
今回の開発方針をおさらいすると…
- Drive APIのOCR機能でレシート内のテキストを抽出
- 抽出したテキストから必要な情報をGPT APIで抽出
- ↑で取得した情報をもとにPDFファイルをリネーム
なので最後の「取得した情報をもとにPDFファイルをリネーム」に挑戦します。
要件定義
さっそくこのパートの要件を定義していきます。
- レシートを保存しているフォルダを指定
- フォルダ内のPDF・画像ファイルすべてに対してリネーム処理
- リネーム処理はOCRでテキスト抽出しGPT APIで必要情報を抜粋して付与
- 処理終了後のファイルを「処理済み」フォルダに移動する
の流れで実装します。
フォルダ内のすべてのPDF・画像ファイルをリネームする
いきなり完成形のスクリプトを出しちゃいます。
/**
*
* 画像ファイルをOCRでテキストに変換し、その情報をもとにファイル名を変更する関数
* この関数は、特定のフォルダ内の画像ファイルを対象に、各ファイルをOCRでテキストに変換し、
* ChatGPTを使用してテキストから特定の情報を抽出します。その後、抽出した情報をもとにファイル名を変更します。
*/
function reNameImgFiles() {
const idReceiptFolder = 'レシートを保存しているフォルダのID':
const systemRole = 'あなたは、領収書やレシートを仕訳入力する事業会社の会計責任者です。';
const prompt =
'#制約条件に従いつつ、以下の領収書・レシートをOCRしたテキストから支払日, 支払総額, 支払先を抽出し'
+ '{"payment_date": 支払日,"total_amount": 支払総額,"payment_to": 支払先}'
+ 'のJSON形式の文字列を出力してください。' + '\n'
+ '\n'
+ '#制約条件' + '\n'
+ '- 支払日はyyyyMMdd形式の文字列' + '\n'
+ '- 支払総額は数値のみ' + '\n'
+ '- 支払先はスペースなしの文字列' + '\n'
+ '\n'
+ '#テキスト' + '\n'
+ '###' + '\n';
// 指定フォルダから画像ファイルのIDを取得
const aryImgId = getImgPdfIds_(idReceiptFolder);
// 処理すべき画像ファイルがない場合は処理を中断
if (aryImgId.length === 0) { return };
// 各画像ファイルを処理
const aryObjReceipt = aryImgId.map(idImg => {
// 画像をテキストに変換
const ocrText = img2Text_(idImg);
// ChatGPTに問い合わせ
const objChat = askChatGPT_(systemRole, prompt + ocrText, 'gpt-4-1106-preview'); // gpt-3.5-turbo-1106 or gpt-4-1106-preview
// レスポンスから必要な情報を抽出
const answer = objChat.choices[0].message.content;
const objJson = JSON.parse(answer);
objJson.idFile = idImg;
return objJson;
});
// 各ファイルの名前を変更
aryObjReceipt.forEach(objReceipt => {
const file = DriveApp.getFileById(objReceipt.idFile);
const payment_date = objReceipt.payment_date.replace(/[ .,]/g, '');
const total_amount = objReceipt.total_amount;
if (typeof (total_amount) === 'string') { total_amount.replace(/[ .,]/g, '') }; // 文字列の場合のみ不要なスペース等を削除
const payment_to = objReceipt.payment_to.replace(/[ .,]/g, '');
// 新しいファイル名を設定
const newFileName = `${payment_date}_${total_amount}_${payment_to}`;
file.setName(newFileName);
});
}
このスクリプト内で使用されているimg2Text_関数とaskChatGPT_関数は、過去記事をご参照ください。
指定されたフォルダ内のPDFおよび画像ファイルのIDを取得する関数
新登場の関数はこちら。
/**
* 指定されたフォルダ内のPDFおよび画像ファイルのIDを取得する関数
*
* @param {string} idFolder - ファイルを検索するフォルダのID
* @return {Array.<string>} PDFおよび画像ファイルのIDの配列
*/
function getImgPdfIds_(idFolder) {
// 指定されたIDのフォルダからファイルを取得
const folder = DriveApp.getFolderById(idFolder);
const files = folder.getFiles();
const aryFileId = new Array();
while (files.hasNext()) {
const file = files.next();
const mimeType = file.getMimeType();
// PDFまたは画像ファイル(JPEG、PNGなど)の場合、そのIDを配列に追加
if (mimeType === MimeType.PDF ||
mimeType === MimeType.JPEG ||
mimeType === MimeType.PNG ||
mimeType === MimeType.GIF ||
mimeType === MimeType.BMP ||
mimeType === MimeType.TIFF) {
aryFileId.push(file.getId());
}
}
return aryFileId;
}
getImgPdfIds_関数は、指定されたフォルダ内のPDFおよび画像ファイルのIDを取得する関数となっています。
こちらであまり馴染みがないかもなのがMimeTypeです。これは、ファイルの形式(タイプ)を識別するために使用される識別子です。
たとえば、PDFファイルはMimeType.PDF、JPEG画像はMimeType.JPEG、PNG画像はMimeType.PNGなどになります。
PDFおよび画像ファイルすべてから情報抽出
続いて、getImgPdfIds_関数で得られたPDFおよび画像ファイルのID配列を用いて、すべてのファイルからOCR&GPT処理で必要なJSONオブジェクトを取り出します。
// 各画像ファイルを処理
const aryObjReceipt = aryImgId.map(idImg => {
// 画像をテキストに変換
const ocrText = img2Text_(idImg);
// ChatGPTに問い合わせ
const objChat = askChatGPT_(systemRole, prompt + ocrText, 'gpt-3.5-turbo-1106'); // gpt-3.5-turbo-1106 or gpt-4-1106-preview
// レスポンスから必要な情報を抽出
const answer = objChat.choices[0].message.content;
const objJson = JSON.parse(answer);
objJson.idFile = idImg;
return objJson;
});
この時、ファイルのIDもJSONオブジェクトに新しいプロパティとして追加しておきます。
JSONオブジェクトから値を取り出し、ファイル名をリネーム
JSONオブジェクトを格納した配列を用いて、全てのファイルをリネームしていきます。
// 各ファイルの名前を変更
aryObjReceipt.forEach(objReceipt => {
const file = DriveApp.getFileById(objReceipt.idFile);
const payment_date = objReceipt.payment_date.replace(/[ .,]/g, '');
const total_amount = objReceipt.total_amount;
if (typeof (total_amount) === 'string') { total_amount.replace(/[ .,]/g, '') }; // 文字列の場合のみ不要なスペース等を削除
const payment_to = objReceipt.payment_to.replace(/[ .,]/g, '');
// 新しいファイル名を設定
const newFileName = `${payment_date}_${total_amount}_${payment_to}`;
file.setName(newFileName);
});
GPT APIへのプロンプトで、「支払日・支払総額・支払先」にはスペースやカンマが含まれないように指示していますが、時折この指示を無視することがあるので、.replace(/[ .,]/g, ”)という正規表現を用いて、スペース、ピリオド、カンマなど不要な文字を削除しています。
2024/01/23追記
const total_amount = objReceipt.total_amount.replace(/[ .,]/g, '');
の変数total_amountが数値型の場合にエラーが出てしまうので、以下に書き換えました。
const total_amount = objReceipt.total_amount;
if (typeof (total_amount) === 'string') { total_amount.replace(/[ .,]/g, '') };
typeof演算子で型判定しています。
処理後のファイルを「処理済み」フォルダに移動する
このリネーム処理は、トリガーなどを用いて定期的に実行することを想定しているで、同一フォルダにそのまま保存していると何度もGPT APIが処理することになります。
APIへのリクエストが有料なので、一度リネームが終わったファイルは「処理済み」フォルダに移動させましょう。
/**
* 指定したGoogleドライブのフォルダから別のフォルダへすべてのファイルを移動する関数
*
* @param {string} idFolderFrom - ファイルを移動させたい元のフォルダのID
* @param {string} idFolderTo - ファイルを移動させる先のフォルダのID
*
*/
function moveFilesToAnotherFolder_(idFolderFrom, idFolderTo) {
// 元のフォルダを取得
const folderFrom = DriveApp.getFolderById(idFolderFrom);
// 移動先のフォルダを取得
const folderTo = DriveApp.getFolderById(idFolderTo);
// 元のフォルダ内のすべてのファイルを取得
const files = folderFrom.getFiles();
// すべてのファイルを移動先のフォルダへ移動
while (files.hasNext()) {
const file = files.next();
file.moveTo(folderTo);
}
}
moveFilesToAnotherFolder_関数は、idでファイルを移動させたい元のフォルダのIDと移動させる先のフォルダのIDを指定して、フォルダ内のすべてのファイルを移動させることができます。
リネームしたファイルをfreeeにアップロードする
最後にリネームしたファイルをfreeeに自動でアップロードします。ここで活躍するのは、freee APIライブラリですね笑
リネームしたファイル名をアップロード際のメモに指定することポイントです。そうすることで、ファイルボックスの検索時に日付や金額、支払先で絞り込むことができます。
接頭辞に勘定科目も加えたい
さらに欲張り処理としては、リネームする時の接頭辞に勘定科目を加えたいというのがあります。
これは、勘定科目ごとのフォルダを用意し、そのなかに画像ファイルを格納し、ここまでで紹介したリネーム処理に加えて、フォルダ名_オリジナルファイル名となるようなGASを追加します。
/**
* 指定したGoogleドライブのフォルダ内にあるすべてのファイルの名前を
* 「フォルダ名_元のファイル名」 にリネームする関数
*
* @param {string} folderId - ファイル名をリネームしたいフォルダのID
*
*/
function renameFilesInFolder_(folderId) {
// フォルダIDからフォルダを取得
const folder = DriveApp.getFolderById(folderId);
const folderName = folder.getName();
// フォルダ内のすべてのファイルを取得
const files = folder.getFiles();
// フォルダ内の各ファイルについて処理を行う
while (files.hasNext()) {
const file = files.next();
const fileName = file.getName();
// ファイル名を "フォルダ名_ファイル名" にリネーム
const newFileName = `${folderName}_${fileName}`;
file.setName(newFileName);
}
}
おわりに
本記事で「連載:Googleドライブに保存したレシートを自動リネームする」はめでたく終了です。今回作成したスクリプトを早速実務で使用していますが、鼻血が出るほど便利です。
ある程度プログラミングの基礎を勉強された方であれば、連載の内容をゆっくり読みながら実装できると思います。ぜひ使ってみた感想などいただけると嬉しいです。
もし連載を読み解いて実装するのが難しい…面倒だ…という方にnoteでコミュニティを作成し、メンバー限定で、コピペでOKなまでに整理したスクリプトを色々と公開しようかなと考えています。
ということで、noteのメンバーシップ機能を使ってfreeeラボ コミュニティのメンバーを募集中です!
まだ海の物とも山の物ともつかぬコミュニティなので、【最初の30人】の方にはアーリーバードプランとして、お手頃価格に設定していますので、ご興味のある方はぜひ。
シリーズ目次
- Googleドライブに保存したレシートを自動リネームする その1 – ドライブに保存したPDFからOCRでテキスト取得
- Googleドライブに保存したレシートを自動リネームする その2 – GASとOpenAI APIを使って質問に答えてもらう
- Googleドライブに保存したレシートを自動リネームする その3 – プロンプトとモデルを調整してJSON形式で応答結果を得る
- Googleドライブに保存したレシートを自動リネームする その4 – フォルダ内のすべてのPDF・画像ファイルをリネームする
Google Apps Scriptを勉強したい方へ
この記事を見て、GASを勉強したいなと思われた方はぜひノンプログラマーのためのスキルアップ研究会(通称 ノンプロ研)にご参加ください。私も未経験からこの学習コミュニティに参加し、講座を受講したことでGASが書けるようになりました。
学習コミュニティ「ノンプログラマーのためのスキルアップ研究会」
挫折しがちなプログラミングの学習も、コミュニティの力で継続できます。ノンプロ研でお待ちしております!
※入会時にfreeelover.comを見て入りました!と言うと、私がちょっぴりお小遣い的なのがもらえるので、私が喜びます。
Amazon欲しい物リスト公開しています。
ブログのモチベーションアップのためにAmazon欲しい物リストを公開しております。役に立ったよ!という方で、感謝の気持ちを示したい方は、何かいただけると嬉しいです笑