🍣

Google DriveのデータをDocumentAIとGASでSpreadSheetに出力する

2024/09/30に公開

はじめに

こんにちは。クラウドエース バックエンド エンジニアリング部の中井です。
この記事では、Google Cloud が提供している Document AI を使って Google Drive に保存されたファイルを解析し、その結果を Google SpreadSheet に出力する方法について解説します。

クラウドエースの別の記事ではBigQueryでDocumentAIを利用する方法も紹介しているのでぜひ一読してみてください。
https://zenn.dev/cloud_ace/articles/cb2bcd1320a8e1#bigqueryで、document-aiのapiを用いるリモートモデルを作成

Document AIとは

Document AIは、Google Cloudが提供するサービスで、PDFや、画像ファイル、手書きの文書など、さまざまな形式のドキュメントから非構造化データを抽出して、構造化データに変換することで、そのドキュメントの分析・理解のサポートをしてくれます。
以下のサイトで、Document AIを試しに使ってみることができます。
https://cloud.google.com/document-ai/docs/try-docai

検証

概要

Google Apps Script (以下 GAS) から Google Drive API を利用して、Google Drive にアップロードしておいた画像を取得します。その後、Document AIのREST APIで、画像を処理するリクエストを送信し、処理結果をGoogle SpreadSheetに出力します。

Google Cloud コンソールでの作業

DriveAPI有効化

  1. Google Cloud コンソールの[APIとサービス]→[ライブラリ]からAPIライブラリのページに移動
  2. [Google Drive API]を選択し[有効にする]をクリック

プロセッサ作成

Document AIでは異なるタイプの文書に応じて事前にトレーニングされた「プロセッサ(processor)」というものが用意されています。
さまざまなタイプのプロセッサがありますが、今回は領収書の画像データを用いるので、Expense Parserというプロセッサを使いたいと思います。

  1. [Document AI]→[プロセッサギャラリー]→Expense Parserの[プロセッサを作成します]をクリック
  2. プロセッサ名とリージョンを入力・選択し、[作成]をクリック
  3. [マイプロセッサ]に作成したプロセッサが表示されるので、それをクリックする。ここで表示されるIDはあとで使用します。
  4. [バージョンの管理]タブをクリックする。バージョンIDのうち、デフォルトバージョンのIDも後ほど使用します。

これでconsoleでの作業は終わりです。次はGASで色々やっていきましょう。

GASでの作業

事前準備

  1. SpreadSheetを作り[拡張機能]→[Apps Script]をクリック

    GASのエディターが開きます
  2. 左のタブから[プロジェクトの設定]をクリックし以下の設定をする
  • 「「appsscript.json」マニフェスト ファイルをエディタで表示する」というチェックボックスを有効にする

  • 「Google Cloud Platform(GCP)プロジェクト」に今回使用するGCPのプロジェクト番号を入力する

    • ちなみにこれを入力すると、入力したプロジェクトの[APIとサービス]→[認証情報]→[OAuth 2.0 クライアント ID]に以下のような項目が追加されます
  • スクリプトプロパティに以下の値を設定

プロパティ
PROJECT_ID Google CloudのプロジェクトID
PROCESSOR_ID プロセッサの作成-3で確認した、プロセッサのID
PROCESSOR_VERSION プロセッサの作成-3で確認した、プロセッサのバージョンID(今回はデフォルトのバージョン[pretrained-expense-v1.3-2022-07-15]を使用します)

  1. [エディタ]に戻り、appscript.jsonファイルに以下の内容を記載する。
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "enabledAdvancedServices": []
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
      "https://www.googleapis.com/auth/script.external_request",
      "https://www.googleapis.com/auth/drive",
      "https://www.googleapis.com/auth/spreadsheets",
      "https://www.googleapis.com/auth/cloud-platform"
  ]
}

この中で重要なのが、認可スコープを設定するoauthScopesです。
認可スコープは、スクリプトが自身のデータにアクセスしたり、自身の代わりに動作することを承認するためのものです。
drive,spreadsheets,cloud-platformは見ての通り、それぞれGoogle Drive,スプレッドシート,Google Cloudへの認可を行うものです。
一方でscript.external_requestはGASで外部にリクエストを投げるのに必要なUrlFetchApp.fetch関数を実行するためのものです。

これで下準備は完了です!続いてはコーディングをガシガシやっていきましょう

実装

実装の大まかな流れは以下のとおりです

  1. スプレッドシートからGoogle DriveのファイルIDを取得
  2. Drive APIを使用しエンコードした画像のデータや、mimeType、ファイル名を取得
  3. Google CloudのOAuth2.0アクセストークンを取得
  4. DocumentAIにHTTPリクエストを送信
  5. レスポンスを処理し、スプレットシートに書き込む

1つずつ解説していきます。

スプレッドシートからGoogle DriveのファイルIDを取得

今回スプレッドシートには以下の画像のように、「ファイル一覧」シートに画像のファイルIDを入力しておきます。

ファイルIDの取得方法

Driveの該当の画像を右クリックし、[共有]→[リンクをコピー]

コピーしたリンクの画像赤枠の部分がファイルIDになります。

「ファイル一覧」シートからファイルIDを取得するコードは以下のとおりです。

// スプレッドシートから処理するファイルのIDを取得
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ファイル一覧');
const fileIdList = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();

Document AIのリクエストに必要なデータを用意する

ドキュメントに書いてあるDocument AIのリクエストに必要な情報を確認しましょう。
「Send request to a processor version」のところを確認してください。

↓リクエストのJSONボディ

{
  "skipHumanReview": skipHumanReview,
  "rawDocument": {
    "mimeType": "MIME_TYPE",
    "content": "IMAGE_CONTENT"
  },
  "fieldMask": "FIELD_MASK"
}

skipHumanReviewtrueにします。fieldMaskでは出力に含めるフィールドを指定することができます。今回はドキュメントの例にある通り、text,entities,pages.pageNumberとしておきます。
画像に関する情報のmimeTypecontentはあとで取得する作業を行います。

curl -X POST \
     -H "Authorization: Bearer $(gcloud auth print-access-token)" \
     -H "Content-Type: application/json; charset=utf-8" \
     -d @request.json \
     "https://LOCATION-documentai.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/processors/PROCESSOR_ID/processorVersions/PROCESSOR_VERSION:process"

リクエストを送る際のAuthorizationヘッダーにgcloud auth print-access-tokenとあります。このコマンドはユーザーの認証情報からGoogle Cloudのアクセストークンを取得するコマンドです。これも取得するコードを書く必要があります。
また、urlの、PROJECT_IDLOCATIONPROCESSOR_IDPROCESSOR_VERSIONにはプロセッサを作成したプロジェクトのID、プロセッサのリージョン、プロセッサID、プロセッサのデフォルトバージョンIDに置換します。

DriveAPIを使用しエンコードした画像のデータや、mineType、ファイル名を取得

以下のコードで、先ほど確認したDocument AIにHTTPリクエストを送る時必要になる画像データ(mimeType,content)を取得します。
contentにはbase64エンコードした画像データが必要なので、Utilities.base64Encode()でエンコードしています。

// 画像データをGoogleDriveから取得しbase64でエンコード
function getImage(fileId) {
  const file = DriveApp.getFileById(fileId);
  const blob = file.getBlob();
  const encoded = Utilities.base64Encode(blob.getBytes());
  const mineType = file.getMimeType();
  const filename = file.getName();

  return [encoded, mineType, filename];
}

※ファイル名はDoucment AIのリクエストでは必要ありませんが、スプレッドシートに出力する際に必要になるので取得しておきます。

ファイル名から画像データを取得する

以下のようにgetFilesByNameメソッドを用いて、ファイル名から画像データを取得することも可能です。(ただGoogle Driveは同じフォルダに同じファイル名のファイルが複数あってもOKとしているので注意が必要です。)

function getImage(filename) {
  const files = DriveApp.getFilesByName(filename);

  if (files.hasNext()) {
    // Gets the ID of each file in the list.
    const fileId = files.next().getId();

    // Gets the file name using its ID and logs it to the console.
    const blob = DriveApp.getFileById(fileId).getBlob();
    var mineType = DriveApp.getFileById(fileId).getMimeType();
    var encoded = Utilities.base64Encode(blob.getBytes())
  }
  return [encoded,mineType];
}

Google CloudのOAuth2.0アクセストークンを取得

GASにはユーザのOAuth2.0アクセストークンを取得するメソッド[1]が用意されているのでそれを使います。

const idToken = ScriptApp.getOAuthToken();

 
これでリクエストに必要な情報は集まりました。早速DocumentAIにリクエストを送っていきましょう。

DocumentAIにHTTPリクエストを送信

UrlFetchApp.fetchメソッドを使い、先ほど確認したドキュメントに従ってリクエストを送ります。

  const url = `https://us-documentai.googleapis.com/v1/projects/${projectId}/locations/us/processors/${processorId}/processorVersions/${processorVersion}:process`
  const requestBody = {
    skipHumanReview: true,
    rawDocument: {
      mimeType: mimeType,
      content: data
    },
    fieldMask: 'text,entities,pages.pageNumber'
  };
  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: mimeType,
    headers: {
      Authorization: `Bearer ${idToken}`
    },
    payload: JSON.stringify(requestBody)
  })

レスポンスを処理し、スプレットシートに書き込む

Document AIからは以下のようなレスポンスが得られます。画像はus_sample.pngの画像データのレスポンスになります。リクエストの際、fieldMaskで指定した通り、text,entities,pagesというフィールドに分かれています。 textフィールドは読み取ったドキュメントのテキスト情報が記載されています、entitiesフィールドでは、Expense Parserが読み取った、項目ごとの値が記載されています。(ちなみにentitiesフィールドのconfidenceは検出した値の信頼度を示しており1に近いほど信頼度が高いです)

長くなってしまうので、スプレッドシートへの出力方法の説明は省略します。記事の最後にコード全体を載せているので、気になる方はそちらを参考にしてみてください。
今回Document AIには、以下の日本語の領収書と英語の領収書の2つを渡しています。

  • jp_sample.pdf

    <出力結果>

  • us_sample.png[2][3]


<出力結果>

少し英語の領収書の方がデータの取得数が多いですが、あまり差はなさそうです。

コード全体

const projectId = PropertiesService.getScriptProperties().getProperty("PROJECT_ID");
const processorId = PropertiesService.getScriptProperties().getProperty("PROCESSOR_ID");
const processorVersion = PropertiesService.getScriptProperties().getProperty("PROCESSOR_VERSION");


function main() {
  const fileIdList = getfileIdList();
  fileIdList.forEach(fileId => {
    try {
      const [data, mimeType, filename] = getImage(fileId);
      documentAI(data, mimeType, filename);
    } catch (error) {
      Logger.log(`${filename}の処理中にエラーが発生しました: ${error.message}`);
    }
  });
}

function getfileIdList() {
  try {
    // スプレッドシートから処理するファイルのIDを取得
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ファイル一覧');
    const fileIdList = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();
    const filterdList = fileIdList.filter(subArray => subArray[0] !== '');
    return filterdList;
  } catch (error) {
    Logger.log('ファイルIDの取得に失敗しました' + error.message)
  }
}


// 画像データをGoogleDriveから取得しbase64でエンコード
function getImage(fileId) {
  const file = DriveApp.getFileById(fileId);
  const blob = file.getBlob();
  const encoded = Utilities.base64Encode(blob.getBytes());
  const mineType = file.getMimeType();
  const filename = file.getName();

  return [encoded, mineType, filename];
}

function documentAI(data, mimeType, filename) {
  console.log(filename);
  // アクセストークンを取得
  const idToken = ScriptApp.getOAuthToken();
  
  // DcumentAIにHTTPリクエストを送信
  const url = `https://us-documentai.googleapis.com/v1/projects/${projectId}/locations/us/processors/${processorId}/processorVersions/${processorVersion}:process`
  const requestBody = {
    skipHumanReview: true,
    rawDocument: {
      mimeType: mimeType,
      content: data
    },
    fieldMask: 'text,entities,pages.pageNumber'
  };
  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: mimeType,
    headers: {
      Authorization: `Bearer ${idToken}`
    },
    payload: JSON.stringify(requestBody)
  })

  console.log('Document processing complete.');
  // レスポンスを処理
  const result = JSON.parse(res.getContentText());
  const entities = result.document.entities;
  console.log(result)
  // スプレッドシートに書き込むデータを格納する配列
  var values = [];
  var headers = new Set();

  entities.forEach(entity => {
    var entityData = {};

    // エンティティの基本情報を格納
    entityData['type'] = entity.type;
    entityData['mentionText'] = entity.mentionText;
    headers.add('type').add('mentionText');

    // プロパティが存在する場合は処理
    if (entity.properties) {
      entity.properties.forEach(property => {
        var propertyType = property.type.split('/')[1] || property.type; // 'line_item/'の後の部分を取得
        entityData[propertyType] = property.mentionText;
        headers.add(propertyType); // ヘッダーセットに追加
      });
    }
    values.push(entityData);
  });

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(`処理結果:${filename}`);
  if (sheet == null) {
    var sheet = ss.insertSheet(`処理結果:${filename}`);
  }
  // ヘッダーを配列に変換し、スプレッドシートに書き込む
  var headerArray = Array.from(headers);
  sheet.getRange(1, 1, 1, headerArray.length).setValues([headerArray]);
  sheet.getRange(1, 1, 1, headerArray.length).setBackground('#fff2cc');

  // データを配列に変換し、スプレッドシートに書き込む
  var dataArray = values.map(item => headerArray.map(header => item[header] || ''));
  if (dataArray.length > 0) {
    sheet.getRange(2, 1, dataArray.length, headerArray.length).setValues(dataArray);
    sheet.getRange(2, 1, dataArray.length).setBackground('#fff2cc');
    sheet.getRange(1, 1, dataArray.length + 1, headerArray.length).setBorder(true, true, true, true, true, true)
  }

}

終わりに

機械学習の知識がほぼゼロな私でも簡単にDocument AIを使って文書の分析を行うことができました。今回はGASからRESTAPIを用いて、Drive APIやスプレッドシートと連携を行いましたが、DocumentAIはクライアントライブラリも充実しているので、工夫次第でさまざまな使い方ができそうです。Document AIには今回紹介した機能以外にもさまざまな機能が携わっているのでぜひ触って遊んでみてください!

Document AIに関する他の記事

https://zenn.dev/cloud_ace/articles/cb2bcd1320a8e1#bigqueryで、document-aiのapiを用いるリモートモデルを作成

脚注
  1. https://developers.google.com/apps-script/reference/script/script-app#getoauthtoken ↩︎

  2. https://cloud.google.com/vertex-ai/generative-ai/docs/prompt-gallery/samples/extract_extract_entities_from_an_invoice ↩︎

  3. https://cloud.google.com/vertex-ai/generative-ai/docs/prompt-gallery/images/invoice.pdf ↩︎

Discussion