ウェブサイト検索

12 日間 (11 日目) - 領収書のデータと添付ファイルを Google スプレッドシートに保存する


導入

「12 日間の DigitalOcean 」 シリーズの 11 日目 へようこそ!昨日、私たちは受信メールの添付ファイルを安全に保存するために DigitalOcean Spaces をセットアップしました。今回は、これをさらに一歩進めて、Google スプレッドシートを統合して、抽出した領収書の詳細とアップロードされた添付ファイルの URL の両方を保存します。

Google スプレッドシートはシンプルで使い慣れており、領収書データの管理と追跡に最適です。このチュートリアルを完了すると、アプリは抽出された領収書の詳細と添付ファイルの URL を Google スプレッドシートにシームレスに保存し、簡単に整理できるようになります。始めましょう!

注: このプロジェクトでは Google スプレッドシートを使用していますが、より堅牢なストレージ ソリューションとして PostgreSQL や MongoDB などのデータベースを使用することもできます。 PostgreSQL をお好みの場合は、Birthday Reminder シリーズの次のチュートリアルを参照して開始してください。

  • DigitalOcean での PostgreSQL データベースのセットアップ
  • Python を使用して PostgreSQL に接続する

学習内容

  1. Google Cloud プロジェクトを構成し、Sheets API を有効にします。
  2. Google スプレッドシートに安全にアクセスするためのサービス アカウントを作成します。
  3. Google 認証情報を DigitalOcean アプリ プラットフォームに安全に保存します。
  4. gspread を使用して、領収書データと添付ファイルの URL を使用して Google スプレッドシートをプログラム的に更新します。

🛠 必要なもの

このチュートリアルを最大限に活用するには、次のことを前提としています。

  1. すでに DigitalOcean にデプロイされている Flask アプリ: Flask アプリをまだデプロイしていない場合は、「7 日目: 電子メールベースの受信プロセッサの構築とデプロイ」の手順に従うことができます。
  2. 電子メール テスト用に構成された消印: 電子メールから受信までの処理パイプラインをテストするには、メールを Flask アプリに転送するように消印を設定する必要があります。ステップバイステップのガイドについては、「Day 8: Postmark を Flask アプリに接続する」を参照してください。
  3. DigitalOcean Spaces のセットアップ: 処理された添付ファイルは DigitalOcean Space に保存されます。まだスペースをお持ちでない場合は、「10 日目: DigitalOcean スペースに添付ファイルを保存する」の手順に従ってください。
  4. Google Cloud Console へのアクセス: API の設定、サービス アカウントの作成、Google スプレッドシートの認証情報の生成を行うには、Google Cloud Console にアクセスする必要があります。
  5. Receipts という名前の Google シート: 抽出したデータを保存できるように、Receipts (またはその他の名前) という名前の空の Google シートを用意してください。このシートを、このチュートリアルで作成したサービス アカウントと共有します。

注: まだすべてを設定していない場合でも、次の方法を学習します。

  • Google スプレッドシートと対話できるように Google Cloud プロジェクトを構成します。
  • Google スプレッドシートに安全にアクセスするためのサービス アカウントを作成します。
  • gspread ライブラリを使用して、Google スプレッドシートをプログラムで更新します。
  • 機密の認証情報を DigitalOcean アプリ プラットフォームに安全に保存します。

ステップ 1: Google Sheets API を設定する

Google スプレッドシートに何かを保存する前に、アプリにそれを操作する権限があることを確認する必要があります。これには、Google Cloud プロジェクトの作成、必要な API の有効化、面倒な作業を処理するためのサービス アカウントの設定が含まれます。

1.1 Google Cloud プロジェクトを作成する

Google Cloud プロジェクトは、Google API を使用して行うあらゆる作業の基盤です。これは、リソースと構成を管理するためのアプリのホームベースとして機能します。

  1. Google Cloud コンソールに移動し、[新しいプロジェクト] をクリックします。

  2. プロジェクトに名前を付け (例: Receipt Processor)、[作成] をクリックします。

1.2 API を有効にする

Google Sheets API を使用すると、アプリは Google スプレッドシートに書き込むことができ、Google Drive API を使用すると、アプリは Google ドライブに保存されているファイルにアクセスできるようになります。

  1. 新しいプロジェクトに移動します。次に、[API とサービス] > [有効な API とサービス] に移動します。
  2. Google Sheets APIGoogle Drive API を検索します。
  3. 両方とも [有効] をクリックします。

1.3 サービス アカウントを作成する

あなたのアプリは、あなたと同じ方法で Google スプレッドシートにログインできません。代わりに、 アプリの認証と権限を処理する特別なボットであるサービス アカウントが必要です。これにより、手動ログインやユーザーの操作を必要とせずに、アプリが Google スプレッドシートに安全にアクセスできるようになります。

  1. [API とサービス] > [認証情報] に移動し、[+ 認証情報の作成] をクリックします。

  2. サービス アカウントを選択します。

  3. サービス アカウントに名前 (例: Receipt Bot) と説明を付けます。「領収書追跡のために Google スプレッドシートとの通信を処理します。」 」

    <$> [情報] 注: サービス アカウントにより電子メール アドレスが生成されます (例: something@project-id.iam.gserviceaccount.com)。これは、後で Google スプレッドシートへのアクセスを共有するために必要になります。 <$>

  4. 認証情報画面に戻るまで、[作成して続行] をクリックします。追加の権限が必要ない場合は、ロールの割り当てをスキップできます。

1.4 認証情報のダウンロード

サービス アカウントには、その身元を証明するための資格情報が必要です。これらは JSON ファイルの形式で提供されます。

  1. [資格情報] 画面でサービス アカウントを見つけ、鉛筆アイコンをクリックして編集します。

  2. [キー] タブに移動し、[キーの追加] > [新しいキーの作成]をクリックします。

  3. JSON を選択し、ファイルをダウンロードします。

<$> [情報] 注: このファイルには、アプリが Google スプレッドシートで認証するために必要なものがすべて含まれています。これをパスワードのように扱い、共有したり、Git にコミットしたりしないでください。 <$>

1.5 Google スプレッドシートを共有する

最後に、サービス アカウントに Google スプレッドシートへのアクセス許可を与えます。

  1. ダウンロードした JSON ファイルから client_email を取得します。

  2. Google スプレッドシートを開きます。 [共有] をクリックし、電子メール アドレスを貼り付け、編集者 アクセスを付与します。

これで、アプリは Google スプレッドシートを安全に読み書きできるようになります。

ステップ 2: 認証情報を安全に保存する

サービス アカウントの JSON のような機密性の高い認証情報がコードベースに存在することは望ましくありません。これはセキュリティ リスクです。代わりに、資格情報を環境変数として DigitalOcean アプリ プラットフォームに保存します。安全であり、実行時にのみアクセスできるようになります。

厄介な点は、Google の認証情報 JSON ファイルは読みやすいようにフォーマットされていますが、環境変数には 1 行の文字列が必要であるということです。まずはそれを修正しましょう。

2.1 JSON のフォーマット

Google の認証情報 JSON は美しいですが、環境変数として保存するにはコンパクトにする必要があります。これを単一行の文字列に変換する方法は次のとおりです。

  1. この Python スニペットをローカル マシンで実行します。

    import json
    with open("path/to/service-account.json", "r") as file:
        creds = json.load(file)
    print(json.dumps(creds))
    

これにより、JSON が 1 行で出力されます。結果をコピーします。

2.2 DigitalOcean に追加

次に、単一行の JSON を DigitalOcean アプリ プラットフォームに安全に保存しましょう。

  1. アプリの[設定] > [環境変数]に移動します。

  2. 新しい変数を追加します。

    1. 「Raw Editor」をクリックし、単一行の JSON 文字列を貼り付けます。
    2. 単一行の JSON 文字列を値として貼り付けます。
    GOOGLE_CREDS={"type": "service_account", "project_id": "receipt-processor-45a6b", ......}
    

  3. 認証情報を安全に保つには、[暗号化] オプションをオンにします。 「保存」をクリックします。

  4. このアクションにより、アプリの自動再デプロイがトリガーされます。

    それでおしまい!認証情報は安全に保存され、アプリは実行時に認証情報を使用できるようになりました。

ステップ 3: アプリを更新する

認証情報が安全に保存されたので、アプリを Google スプレッドシートに接続し、領収書のデータと添付ファイルを処理できるように更新します。

コードに入る前に、必要な依存関係がインストールされ、requirements.txt ファイルが更新されていることを確認してください。これにより、アプリにシームレスに実行するために必要なライブラリがすべて確実に含まれるようになります。

3.1 依存関係のインストール

次のコマンドを実行して、必要な Python ライブラリをすべてインストールします。

pip install flask boto3 python-dotenv gspread oauth2client openai

次に、依存関係を requirements.txt ファイルに凍結します。

pip freeze > requirements.txt

このステップでは、アプリのすべての依存関係をキャプチャし、DigitalOcean アプリ プラットフォームでのデプロイと管理が容易になります。

3.2 Google スプレッドシートに接続する

この手順により、アプリは GOOGLE_CREDS に保存されている認証情報を使用して Google スプレッドシートで認証できるようになります。認証されると、アプリはプログラムでシートの読み取りと書き込みができるようになります。

このコードをアプリに追加します。

import gspread
from oauth2client.service_account import ServiceAccountCredentials
import os
import json
Load credentials from environment variablesGOOGLE_CREDS is the single-line JSON string that contains the service account credentials.
creds_json = os.getenv("GOOGLE_CREDS")
creds_dict = json.loads(creds_json)  # Convert the JSON string back into a dictionary.
Define the required scopes for accessing Google Sheets and Google Drive.The "spreadsheets" scope allows the app to read/write Sheets, and the "drive" scope allows access to Sheets stored in Drive.
scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
Authenticate using the service account credentials.
credentials = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
Create a gspread client to interact with Google Sheets.
sheets_client = gspread.authorize(credentials)
Open the Google Sheet by name. Replace "Receipts" with the name of your Sheet.This provides access to the first worksheet in the Sheet, which you can use to read/write rows.
sheet = sheets_client.open("Receipts").sheet1

3.3 領収書のデータと添付ファイルを保存する

アプリはメール データと添付ファイルを処理します。この機能により、両方が Google スプレッドシートに保存されます。各行には、領収書の詳細 (ベンダー、金額、通貨など) と添付ファイルの URL が含まれます。

この関数をアプリに追加します。

def save_to_google_sheets(extracted_data, attachment_urls):
    """
    Save extracted receipt data and attachment URLs to Google Sheets.
    """
    try:
        # Combine all attachment URLs into a single string, separated by commas.
        # This ensures all URLs are stored in one cell in the Sheet.
        attachments_str = ", ".join([attachment["url"] for attachment in attachment_urls])

        # Append a new row with extracted data and attachment URLs to the Google Sheet.
        # Each element in the list corresponds to a column in the Sheet.
        sheet.append_row([
            extracted_data.get("vendor", ""),
            extracted_data.get("amount", ""),
            extracted_data.get("currency", ""),
            extracted_data.get("date", ""),
            attachments_str  # Store all attachment URLs in a single column
        ])
        # Log a success message to confirm data was saved.
        logging.info("Data and attachments saved to Google Sheets.")
    except Exception as e:
        # Log an error if something goes wrong while saving to the Sheet.
        logging.error(f"Failed to save data to Google Sheets: {e}")

3.4 最終コード

これは、これまでに取り組んできたすべての部分を統合した、アプリの完全なコードです。

from flask import Flask, request, jsonify
import os
import base64
import uuid
import boto3
from dotenv import load_dotenv
from openai import OpenAI
import logging
import json
import gspread
from oauth2client.service_account import ServiceAccountCredentials
Load environment variables
load_dotenv()
Initialize Flask app
app = Flask(__name__)
Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
Initialize DigitalOcean GenAI client
SECURE_AGENT_KEY = os.getenv("SECURE_AGENT_KEY")
AGENT_BASE_URL = os.getenv("AGENT_BASE_URL")
AGENT_ENDPOINT = f"{AGENT_BASE_URL}/api/v1/"
client = OpenAI(base_url=AGENT_ENDPOINT, api_key=SECURE_AGENT_KEY)
DigitalOcean Spaces credentials
SPACES_ACCESS_KEY = os.getenv("SPACES_ACCESS_KEY")
SPACES_SECRET_KEY = os.getenv("SPACES_SECRET_KEY")
SPACES_BUCKET = os.getenv("SPACES_BUCKET_NAME")
SPACES_REGION = os.getenv("SPACES_REGION")
SPACES_ENDPOINT = f"https://{SPACES_BUCKET}.{SPACES_REGION}.digitaloceanspaces.com"
Initialize DigitalOcean Spaces client
session = boto3.session.Session()
s3_client = session.client(
    's3',
    region_name=SPACES_REGION,
    endpoint_url=SPACES_ENDPOINT,
    aws_access_key_id=SPACES_ACCESS_KEY,
    aws_secret_access_key=SPACES_SECRET_KEY
)
Google Sheets API setup
creds_json = os.getenv("GOOGLE_CREDS")
if not creds_json:
    raise ValueError("Google credentials not found in environment variables.")
creds_dict = json.loads(creds_json)

scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
credentials = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
sheets_client = gspread.authorize(credentials)
sheet = sheets_client.open("Receipts").sheet1  # Replace "Receipts" with your sheet name

def extract_text_from_email(email_content):
    """Extract relevant details from the email content using DigitalOcean GenAI."""
    logging.debug("Extracting details from email content.")
    prompt = (
        "Extract the following details from the email:\n"
        "- Date of transaction\n"
        "- Amount\n"
        "- Currency\n"
        "- Vendor name\n\n"
        f"Email content:\n{email_content}\n\n"
        "Ensure the output is in JSON format with keys: date, amount, currency, vendor."
    )
    response = client.chat.completions.create(
        model="your-model-id",  # Replace with your GenAI model ID
        messages=[{"role": "user", "content": prompt}]
    )
    logging.debug("GenAI processing completed.")
    return json.loads(response.choices[0].message.content)

def decode_and_save_attachment(attachment):
    """Decode base64-encoded attachment and save it locally with a unique name."""
    file_name = attachment.get("Name")
    encoded_content = attachment.get("Content")

    if not file_name or not encoded_content:
        logging.warning("Invalid attachment, skipping.")
        return None

    unique_file_name = f"{uuid.uuid4()}_{file_name}"
    file_path = os.path.join("/tmp", unique_file_name)

    try:
        with open(file_path, "wb") as file:
            file.write(base64.b64decode(encoded_content))
        logging.info(f"Attachment saved locally: {file_path}")
        return file_path
    except Exception as e:
        logging.error(f"Failed to decode and save attachment {file_name}: {e}")
        return None

def upload_attachment_to_spaces(file_path):
    """Upload a file to DigitalOcean Spaces and return its public URL."""
    file_name = os.path.basename(file_path)
    object_name = f"email-receipt-processor/{file_name}"
    try:
        s3_client.upload_file(file_path, SPACES_BUCKET, object_name, ExtraArgs={"ACL": "public-read"})
        file_url = f"https://{SPACES_BUCKET}.{SPACES_REGION}.cdn.digitaloceanspaces.com/{object_name}"
        logging.info(f"Attachment uploaded to Spaces: {file_url}")
        return file_url
    except Exception as e:
        logging.error(f"Failed to upload attachment {file_name} to Spaces: {e}")
        return None

def process_attachments(attachments):
    """Process all attachments and return their URLs."""
    attachment_urls = []
    for attachment in attachments:
        file_path = decode_and_save_attachment(attachment)
        if file_path:
            file_url = upload_attachment_to_spaces(file_path)
            if file_url:
                attachment_urls.append({"file_name": os.path.basename(file_path), "url": file_url})
            os.remove(file_path)  # Clean up local file
    return attachment_urls

def save_to_google_sheets(extracted_data, attachment_urls):
    """Save extracted receipt data and attachment URLs to Google Sheets."""
    try:
        # Combine all attachment URLs into a single string (comma-separated)
        attachments_str = ", ".join([attachment["url"] for attachment in attachment_urls])

        # Append a new row with extracted data and attachment URLs
        sheet.append_row([
            extracted_data.get("vendor", ""),
            extracted_data.get("amount", ""),
            extracted_data.get("currency", ""),
            extracted_data.get("date", ""),
            attachments_str  # Store all attachment URLs in a single column
        ])
        logging.info("Data and attachments saved to Google Sheets.")
    except Exception as e:
        logging.error(f"Failed to save data to Google Sheets: {e}")

@app.route('/inbound', methods=['POST'])
def handle_inbound_email():
    """Process inbound emails and return extracted JSON."""
    logging.info("Received inbound email request.")
    data = request.json

    email_content = data.get("TextBody", "")
    attachments = data.get("Attachments", [])

    if not email_content:
        logging.error("No email content provided.")
        return jsonify({"error": "No email content provided"}), 400

    extracted_data = extract_text_from_email(email_content)
    attachment_urls = process_attachments(attachments)

    # Save extracted data and attachment URLs to Google Sheets
    save_to_google_sheets(extracted_data, attachment_urls)

    response_data = {
        "extracted_data": extracted_data,
        "attachments": attachment_urls
    }

    # Log the final combined data
    logging.info("Final Response Data: %s", response_data)

    return jsonify(response_data)

if __name__ == "__main__":
    logging.info("Starting Flask application.")
    app.run(port=5000)

ステップ 4: DigitalOcean にデプロイする

更新された Flask アプリをデプロイするには、「7 日目: 電子メールベースの受信プロセッサの構築とデプロイ」の手順に従います。簡単な要約は次のとおりです。

  1. 更新されたコードを GitHub にプッシュする: Flask アプリに必要な変更を加えた後、更新されたコードをコミットして GitHub にプッシュします。これにより、DigitalOcean のアプリ プラットフォームへの自動デプロイメントがトリガーされます。

    git add .
    git commit -m "Add attachment processing with DigitalOcean Spaces"
    git push origin main
    
  2. デプロイメントの監視: アプリのダッシュボードのデプロイメントセクションで進行状況を追跡できます。

  3. デプロイメントを確認する: デプロイメントが完了したら、アプリのパブリック URL に移動し、その機能をテストします。ダッシュボードのランタイム ログをチェックして、アプリが正常に起動したことを確認することもできます。

ステップ 5: ワークフロー全体をテストする

アプリが完全に構成され準備が整ったので、ワークフロー全体をテストします。メール本文が処理され、添付ファイルがデコードされて DigitalOcean Spaces にアップロードされ、最終出力には領収書の詳細と添付ファイルの URL が含まれ、すべて Google スプレッドシートに保存されることを確認します。

段階的にテストする方法は次のとおりです。

  1. テストメールを送信: テキスト本文と添付ファイルを含むメールを消印まで送信します。 Postmark の設定方法がわからない場合は、「Day 8: Postmark を Flask アプリに接続する」を参照してください。ここでは、メールをアプリに転送するための Postmark の設定について説明しました。

  2. 消印アクティビティ JSON を確認: 消印ダッシュボードで、アクティビティ タブに移動します。送信した電子メールを見つけて、JSON ペイロードにテキスト本文と Base64 でエンコードされた添付データが含まれていることを確認します。これにより、Postmark がメール データをアプリに正しく転送していることが確認できます。

  3. ログを監視: DigitalOcean アプリ プラットフォーム ダッシュボードのランタイム ログをチェックして、アプリが JSON ペイロードを処理していることを確認します。ランタイム ログにアクセスする方法については、「9 日目: DigitalOcean の GenAI エージェントを使用したレシート解析の自動化」で説明しました。

  4. スペースのアップロードを確認: DigitalOcean Space にアクセスして、ファイルが正常にアップロードされたことを確認します。バケット内に添付ファイルが表示されるはずです。

  5. Google スプレッドシートを確認する: Google スプレッドシートを開き、領収書の詳細と添付ファイルの URL が新しい行として保存されていることを確認します。詳細には以下を含める必要があります。

    • 電子メール本文から抽出されたベンダー、金額、通貨、日付。
    • 最後の列には、アップロードされた添付ファイルのカンマ区切りの URL。

これらの手順が完了するまでに、アプリはメールから Google スプレッドシートへの完全なワークフローを正常に完了し、さらなる自動化のための準備を整えます。

🎁 まとめ

素晴らしい作品です!今日は、Google スプレッドシートをアプリにシームレスに統合して領収書データを管理する方法を学びました。具体的には、次のことを行います。

  • Google Cloud プロジェクトを設定し、Sheets API を有効にします。

  • サービス アカウントを作成し、その認証情報を DigitalOcean アプリ プラットフォームに安全に保存しました。

  • gspread を使用して、領収書の詳細と添付ファイルの URL を含む Google スプレッドシートをプログラムで更新しました。

次の作業: 最後のチュートリアルでは、領収書が正常に処理されるたびに確認メールを送信できるようにアプリを有効にして自動化を完了します。これらのメールには、抽出された領収書の詳細と、簡単にアクセスして確認できるように Google スプレッドシートへの直接リンクが含まれます。

DigitalOcean シリーズの 12 日間の最終章でお会いしましょう! 🚀