【Dify x GASで業務効率化】PDFから抽出した顧客データをスプレッドシートに保存
AIとGoogleのサービスを連携して業務を効率化したいと考えている方は多いのではないでしょうか。本記事では、PDFファイルに記載された顧客情報を自動で取得し、Googleスプレッドシートに書き込む処理をDifyとGoogle Apps Script(GAS)を活用して実現する方法を紹介しています。
今回作成するツールを利用すれば、PDFがテキストベースで必要な情報が含まれている限り、請求書や注文書など種類を問わず活用可能です。また、本記事の範囲外ですがPDFではなく画像ファイルから文字列を抽出する仕組みを追加すれば名刺管理などにも応用できます。このように、業務効率化のアイデアとして非常にわかりやすい例になっているので興味のある方はぜひご自身の業務に合わせてカスタマイズしてみてください。
初心者でも安心して取り組めるよう、すべての手順をキャプチャ画像付きでステップバイステップで解説しています。
目次
処理の流れ
Google Driveに保存したPDFから顧客情報を抽出し、それをGoogleスプレッドシートに書き込む処理は以下のステップで行います。
- PDFファイルをGoogle Driveに保存
まず、対象となるPDFファイルを指定したGoogle Driveのフォルダに保存します。 - Google Apps Script(GAS)でPDFをDifyに送信
GASを使用して、Google Drive上に保存されているPDFファイルをDifyに送ります。 - DifyでPDFからテキストを抽出
Difyが受け取ったPDFファイルからテキストを抽出します。 - 大規模言語モデル(LLM)で顧客情報を抽出
抽出されたテキストをLLMで解析し、必要な顧客情報のみを取り出します。 - GASに顧客情報を戻す
Difyから抽出した顧客情報をGASに返します。 - Googleスプレッドシートに情報を書き込む
GASが受け取った顧客情報をGoogleスプレッドシートに自動で書き込みます。
実践的な内容とするためYahooショッピングの注文書伝票のPDFを利用しています。顧客情報はダミーデータに書き換えていますがPDF内のフォーマットはそのままです。
本記事はDify, GASなどを一度は利用して経験がある人を前提にしています。初めての人はDify, GASの基本操作については本ブログの下記の記事で公開済みなので参考にしてください。
顧客情報の取得
PDFファイルから顧客情報の取得はDifyのワークフローを利用して行います。PDFを入力フォームからアップロードを行い、JSONデータで顧客情報を取得するまでの流れを設定していきます。
ワークフローの作成
Difyのスタジオページから”最初から作成”でワークフローのアプリの作成を行います。アプリの名前は任意の名前をつけることができるので「PDF顧客情報抽出アプリ」としています。
作成するとブラウザ上には”開始ノード”が表示されるので終了ノードを作成してください。
入力フィールドの設定
開始ノードの入力フィールドの”+”ボタンをクリックして入力フィールドの追加画面を表示させてフィールドタイプに”単一ファイル”を選択して変数名、ラベル名を”pdf”、サポートされたファイルタイプを”ドキュメント”、アップロードされたファイルのタイプを”ローカルアップロード”にして保存します。
PDFファイルの内容を抽出
入力フィールドで設定したpdfファイルからテキストを抽出するために開始ノードと終了ノードの間にテキスト抽出ツールブロックを追加します。
テキスト抽出ツールブロックでは入力変数のみ設定することができます。入力変数にはアップロードしたPDFファイルを保存したpdf変数を設定します。
テキスト抽出ツールでは出力変数にtextが設定されるので、終了ノードでtextを出力変数として設定してPDFファイルからLLMに渡すために必要なテキストが抽出できるているか確認します。
設定が完了したら動作確認を行うため”実行”ボタンをクリックします。実行ボタンをクリックすると入力フィールでファイルを選択することができます。
ファイルを選択したら”実行を開始”ボタンをクリックしてください。結果にはPDFから抽出された情報が表示されます。
PDFのファイルアップロードとテキスト抽出ツールが問題なく動作することが確認できました。ここでテキストが抽出できない場合には先に進んでも処理はできません。テキストで出力できない場合はPDFの内容がスキャンした表などの画像データの可能性が高いのチェックしてください。
顧客情報の取得
テキスト抽出ツールではPDFに記載されているすべてのテキスト情報が抽出されているのでこの中から得意先情報のみを取り出す必要があります。ここで大規模言語モデル(LLM)の出番です。テキストから必要な情報を取得するためにはLLMに渡すプロンプトも重要になり、各自の処理に合わせて修正が必要となります。
テキスト抽出ツールノードと終了ノードの間にLLMブロックを追加します。
LLMブロックの設定では各自利用可能なモデルを選択してください。ここでは”gpt-4o-mini”を選択しています。コンテキストはテキスト抽出ツールの出力変数のtextを使います。
SystemのプロンプトにはJSONデータとして顧客データを取り出しています。JSONデータとして取り出すことでGASなどの外部からのアクセスツールでも扱いやすいデータ形式にしています。
プロンプトは下記を設定していますが取り出したい情報に合わせて修正してください。動作確認をして期待通りの値が戻されない場合にも修正する必要があります。
{dify変数}から以下の情報を抽出してください:
抽出対象:
氏名
氏名カナ
郵便番号
住所
電話番号
メールアドレス
{
"customer_name": "氏名",
"customer_name_kana": "氏名カナ",
"zip_code": "郵便番号",
"address": "住所",
"tel": "電話番号",
"email": "メールアドレス"
}
注意事項:
データが欠損している場合、そのフィールドは空文字列にしてください。
{
"customer_name": "山田 太郎",
"customer_name_kana": "ヤマダ タロウ",
"zip_code": "123-4567",
"address": "東京都新宿区1-2-3",
"tel": "090-1234-5678",
"email": "yamada@example.com"
}
このフォーマットでデータを抽出し、JSONとして返してください。
終了ノードの出力変数はLLMの出力変数のtextに変更しています。同じtextという変数名でも先ほどの設定はテキスト抽出ツールノードからのtextが設定されていたので忘れずに変更を行ってください。
LLMブロックを追加後に実行すると期待通りのJSONが戻されることが確認できました。
JSONデータとして取得できましたが、このままこのデータを利用した場合にマークダウンなどを利用した際にコードとして認識できるようにバッククォートの「“`json」という文字列がLLMから戻されるデータに含まれます。バッククォートの文字列をコードを利用して削除できますがプロンプトを更新することでバッククォートなしで抽出するように変更します。
このフォーマットでデータを抽出し、JSONとして返してください。
↓
このフォーマットでデータを抽出し、JSONデータをバッククオートなしで返してください
再度実行するとJSONという文字列が消えています。
ここまでの動作確認でアップロードしたPDFファイルから顧客情報を取得することができました。ここで期待したデータが取得できない場合はプロンプトの修正が本当にテキスト抽出ツールで抽出したテキストの必要なテキストが含まれているのかチェックしてください。
DifyのAPIの設定
GASなど外部からのリクエストを受けてワークフローを実行するためにはDifyのAPIの設定が必要となります。サイドメニューにある「APIアクセス」のリンクをクリックして、右上にある「APIキー」をクリックするとAPIシークレットキーの作成画面が表示されるので”新しいシークレットキーを作成”をクリックしてください。
表示されるシークレットキーがDifyのAPIに対して外部からアクセスする際に必要となるキーです。GASのスクリプトの中でこのキーを利用します。
ここまででDify側の設定は完了です。ここからはGASの設定を行なっていきます。
Google Apps Script(GAS)の設定
GASではGoogle Drive上のフォルダに保存されているPDFファイルをDifyに送信して、その結果戻される得意先の情報をGoogle Sheetsのスプレッドシートに追加する処理を設定していきます。
PDFファイルの保存
Google Driveに保存されたPDFを利用するのでGoogle Drive(https://drive.google.com/)にアクセスして「注文書」という名前のフォルダを作成してアップロードを行いたいPDFファイルを保存します。
GASプロジェクトの作成
GASを利用してGoogle Driveに保存したファイルをアップロードするスクリプトを作成するためApps Script(https://script.google.com/)で新しいプロジェクトを作成します。
ここでは「PDF顧客情報取得」という名前のプロジェクト名を設定しています。
PDFのファイル情報取得の関数
最初にGoogle Drive上に作成した「注文書」フォルダに保存されているPDFファイルを取得するaccessFiles関数を作成します。folderIdには各自が利用しているフォルダのIDを設定してください。フォルダIDはGoogle Driveからフォルダにアクセスした際のURLに含まれています。
function accessFiles() {
const folderId = 'YOUR_FOLDER_ID';
const foler = DriveApp.getFolderById(folderId);
const files = foler.getFiles();
while (files.hasNext()) {
let file = files.next();
Logger.log('ファイル名: ' + file.getName());
Logger.log('ファイルID: ' + file.getId());
}
}
スクリプトを保存して実行すると初回実行時は、スクリプトのアクセス権や信頼に関する確認の画面が表示されます。許可を行い、スクリプトを実行すると実行ログにファイル名とファイルIDが表示されます。while文を設定しているので注文書にファイルが複数存在すればそのファイル分の情報が表示されます。
GASについての基本的な利用方法は実行のアクセス権も含め、下記の記事に記載しているので参考にしてください。
Difyへのファイルのアップロード
ここがポイントなのですがDifyにファイルをアップロードして利用するためには、ファイルをアップロードするリクエストとアップロードしたファイルを利用してワークフローを実行するリクエストの計2回のリクエストを送信する必要があります。
ファイルのアップロードを行うためのコードを記述します。リクエストを送信するURLはhttps://api.dify.ai/v1/files/uploadでsendFileToDify関数はDifyのAPIのsecret_keyとaccessFiles関数で取得したファイルのIDを引数に持ちます。payloadのfileプロパティでGoogle Driveに保存したPDFファイルのBlobデータを指定します。
function sendFileToDify(secret_key, fileId) {
const url = 'https://api.dify.ai/v1/files/upload';
const file = DriveApp.getFileById(fileId);
const fileBlob = file.getBlob();
const payload = {
file: fileBlob,
user: 'abc-user'
};
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log("リクエスト成功: " + response.getContentText());
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
accessFiles関数からsendFileToDify関数を呼び出して実行します。accessFiles関数で設定したsecret_keyは各自がDify上で設定したシークレットキーを設定してください。
function accessFiles() {
const secret_key = 'YOUR_SECRET_KEY';
const folderId = 'YOUR_FOLDER_ID';
const foler = DriveApp.getFolderById(folderId);
const files = foler.getFiles();
while (files.hasNext()) {
const file = files.next();
Logger.log('ファイル名: ' + file.getName());
Logger.log('ファイルID: ' + file.getId());
sendFileToDify(secret_key, file.getId())
}
}
function sendFileToDify(secret_key, fileId) {
const url = 'https://api.dify.ai/v1/files/upload';
const file = DriveApp.getFileById(fileId);
const fileBlob = file.getBlob();
const payload = {
file: fileBlob,
user: 'abc-user'
};
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log("リクエスト成功: " + response.getContentText());
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
コードを更新して実行すると実行ログにはDify APIから戻されるファイルの情報が表示されます。その中のidがDifyにアップロードしたPDFファイルのIDなので次のリクエストに利用します。
ファイルアップロード後にDifyから戻される情報を確認するとアップロードしたファイルがPDFファイルであることもわかります。名前、サイズ、拡張子やmime_typeが含まれています。
{
"id": "5a94a6f4-1644-421a-8a87-74ebf9eb417c",
"name": "orderSlip20241203.pdf",
"size": 101276,
"extension": "pdf",
"mime_type": "application/pdf",
"created_by": "64aa57j9-9d20-4f6d-a331-2eb6780940e3",
"created_at": 1733317218
}
ワークフローを実行するためのリクエストを行うためにsendRequest関数を追加します。sendRequest関数はsecret_keyとfileIdを引数に持ちます。リクエストを送信するURLはhttps://api.dify.ai/v1/workflows/runでpayloadのinputsプロパティでは開始ノードの入力フィールドで設定した変数名”pdf”、サポートされたファイルタイプ(transfer_methodプロパティに対応)、アップロードされたファイルのタイプ(typeプロパティに対応)で設定した値を設定します。値は’local_file’と’document’になります。
function sendRequest(secret_key, fileId){
const workflow_url = "https://api.dify.ai/v1/workflows/run"
const payload = JSON.stringify({
inputs: {
pdf: {
transfer_method: 'local_file',
upload_file_id: fileId,
type: 'document'
}
},
response_mode: 'blocking',
user: 'abc-user'
});
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
'Content-Type': 'application/json'
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(workflow_url, options);
Logger.log("リクエスト成功: " + response.getContentText());
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
sendRequest関数はaccessFiles関数の中で呼び出されて実行されます。sendFileToDify関数の戻り値のファイルIDを引数に設定します。コードは下記となります。
function accessFiles() {
const secret_key = 'YOUR_SECRET_KEY';
const folderId = 'YOUR_FOLDER_ID';
const foler = DriveApp.getFolderById(folderId);
const files = foler.getFiles();
while (files.hasNext()) {
const file = files.next();
Logger.log('ファイル名: ' + file.getName());
Logger.log('ファイルID: ' + file.getId());
const difyFileId = sendFileToDify(secret_key, file.getId())
sendRequest(secret_key, difyFileId);
}
}
function sendFileToDify(secret_key, fileId) {
const url = 'https://api.dify.ai/v1/files/upload';
const file = DriveApp.getFileById(fileId);
const fileBlob = file.getBlob();
const payload = {
file: fileBlob,
user: 'abc-user'
};
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log("リクエスト成功: " + response.getContentText());
const responseBody = JSON.parse(response.getContentText())
Logger.log("ファイルID: " + responseBody.id);
return responseBody.id;
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
function sendRequest(secret_key, fileId){
const workflow_url = "https://api.dify.ai/v1/workflows/run"
const payload = JSON.stringify({
inputs: {
pdf: {
transfer_method: 'local_file',
upload_file_id: fileId,
type: 'document'
}
},
response_mode: 'blocking',
user: 'abc-user'
});
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
'Content-Type': 'application/json'
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(workflow_url, options);
Logger.log("リクエスト成功: " + response.getContentText());
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
実行後に戻されるDifyからのJSONデータを確認するとoutputsプロパティの中にcustomer_nameなどの情報を確認することができます。
{
"task_id": "94d3afae-5d92-4310-bf0e-134cfe88d007",
"workflow_run_id": "26402057-2c80-47f1-98e9-b273c917dcde",
"data": {
"id": "26402057-2c80-47f1-98e9-b273c917dcde",
"workflow_id": "5bb6b640-10cb-49a4-9b2e-8750d3699114",
"status": "succeeded",
"outputs": {
"result": "```json\n[\n {\n \"customer_name\": \"\u5c71\u7530 \u592a\u90ce\",\n \"customer_name_kana\": \"\u30e4\u30de\u30c0 \u30bf\u30ed\u30a6\",\n \"zip_code\": \"1638001\",\n \"address\": \"\u6771\u4eac\u90fd\u65b0\u5bbf\u533a\u897f\u65b0\u5bbf\uff12\u4e01\u76ee\uff18\uff0d\uff11\",\n \"tel\": \"09098765432\",\n \"email\": \"taro@icloud.com\"\n }\n]\n```"
},
"error": null,
"elapsed_time": 1.9080744259990752,
"total_tokens": 986,
"total_steps": 4,
"created_at": 1733319707,
"finished_at": 1733319709
}
}
上記のようにresultの値に“`jsonが入っている場合はここから先のコードが正しく動作しないのでDifyのプロンプトにバッククォートなしで返しての一文が入っているか確認してください。
Google Sheetsへの書き込み
DifyにPDFファイルをアップロードして顧客情報が戻ることが確認できたので、Difyから戻された顧客情報をGASを利用してGoogle Sheetsへ書き込みます。GASのスクリプトの中では戻されたデータの中から顧客情報のみ取り出すという処理が必要になります。
Google Sheets(https://docs.google.com/spreadsheets)にアクセスを行い、顧客情報を保存するための得意先一覧ファイルを作成します。得意先一覧ファイルには以下の列を追加して保存します。
Google Sheetsへの書き込みを行うwriteToSheet関数を追加します。引数からJSONデータの顧客情報を受け取り、JSON.parseでJSONデータからJavaScriptのオブジェクトに変換して、sheetのappendRowメソッドで作成した得意先一覧ファイルに書き込みを行っています。書き込みを行いたい得意先一覧ファイルを取得する際に利用するスプレッドシートIDはスプレッドシート開いた時のURLから取得することができます。
function writeToSheet(data) {
const spreadsheetId = "YOUR_SPREADSHEET_ID";
const sheetName = "得意先一覧";
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const customer = JSON.parse(data);
sheet.appendRow([customer.customer_name,customer.customer_name_kana,customer.zip_code, customer.address, customer.tel, customer.email]);
}
writeToSheet関数の渡す引数のdataはsendRequest関数の中でDifyから取得した結果(data.outputs.result)を渡しています。
function sendRequest(secret_key, fileId){
const workflow_url = "https://api.dify.ai/v1/workflows/run"
const payload = JSON.stringify({
inputs: {
pdf: {
transfer_method: 'local_file',
upload_file_id: fileId,
type: 'document'
}
},
response_mode: 'blocking',
user: 'abc-user'
});
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
'Content-Type': 'application/json'
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(workflow_url, options);
const responseBody = response.getContentText()
const jsonResponse = JSON.parse(responseBody);
Logger.log("リクエスト成功: " + JSON.stringify(jsonResponse.data.outputs.result));
return jsonResponse.data.outputs.result;
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
全体のスクリプトコードは下記のようになります。
function accessFiles() {
const secret_key = 'YOUR_SECRET_KEY';
const folderId = 'YOUR_FOLDER_ID';
const foler = DriveApp.getFolderById(folderId);
const files = foler.getFiles();
while (files.hasNext()) {
const file = files.next();
Logger.log('ファイル名: ' + file.getName());
Logger.log('ファイルID: ' + file.getId());
const difyFileId = sendFileToDify(secret_key, file.getId())
const customer = sendRequest(secret_key, difyFileId);
writeToSheet(customer)
}
}
function writeToSheet(data) {
const spreadsheetId = "YOUR_SPREADSHEET_KEY";
const sheetName = "得意先一覧";
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const customer = JSON.parse(data);
sheet.appendRow([customer.customer_name,customer.customer_name_kana,customer.zip_code, customer.address, customer.tel, customer.email]);
}
function sendFileToDify(secret_key, fileId) {
const url = 'https://api.dify.ai/v1/files/upload';
const file = DriveApp.getFileById(fileId);
const fileBlob = file.getBlob();
const payload = {
file: fileBlob,
user: 'abc-user'
};
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log("リクエスト成功: " + response.getContentText());
const responseBody = JSON.parse(response.getContentText())
Logger.log("ファイルID: " + responseBody.id);
return responseBody.id;
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
function sendRequest(secret_key, fileId){
const workflow_url = "https://api.dify.ai/v1/workflows/run"
const payload = JSON.stringify({
inputs: {
pdf: {
transfer_method: 'local_file',
upload_file_id: fileId,
type: 'document'
}
},
response_mode: 'blocking',
user: 'abc-user'
});
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + secret_key,
'Content-Type': 'application/json'
},
payload,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(workflow_url, options);
const responseBody = response.getContentText()
const jsonResponse = JSON.parse(responseBody);
Logger.log("リクエスト成功: " + JSON.stringify(jsonResponse.data.outputs.result));
return jsonResponse.data.outputs.result;
} catch (error) {
Logger.log("リクエスト失敗: " + error.message);
}
}
作成したコードを利用して動作確認を行います。
実行すると得意先一覧のスプレッドシートにPDFファイルから抽出した顧客情報が追加されていることが確認できます。
新たに別のPDFファイルを注文書フォルダに保存してスクリプトを実行するとフォルダに含まれているファイルをすべて読み込むことになるので2つのファイルのPDFの情報を追加して3行となります。
ここまでの設定でGoogle Driveに保存したPDFから得意先一覧のスプレッドシートにスクリプトを実行するだけで数秒で処理が行えることが確認できました。
次はフォルダ内のすべてのファイルを毎回読み込むのではなくフォルダに新しいファイルが追加されたら自動でPDFから得意先一覧ファイルに書き込みを行うという機能の実装が必要となります。
フォルダへのファイル追加検知
フォルダへの新しいファイルの追加の検知はGASに事前にそのような機能がないか調べても内容なので時間主導型で定期的にチェックをすることで検知を行うことができます。ファイル追加検知は以下のような方法が考えられます。
- ファイルリストの比較
フォルダ内に保存されているファイルのリストを比較して新たに保存されたファイルを特定 - タイムスタンプの利用
ファイルが作成されたタイムスタンプが前回のチェックしたタイムスタンプ以降などうかチェックしてファイルを特定
これらの方法についてまた別の記事で公開する予定です。