スペースへの添付ファイルの保存 (第10日目) - 12日間の
デジタルオーシャンの12日間のイベントの10日目に戻ってきてくれて歓迎します!昨日は、デジタルオーシャンのGenAIエージェントを使って、アプリがメールの内容から情報を抽出する方法を教えました。それは大きなステップでしたが、現実を見てみましょう—領収書や請求書はメール本文の中に常にあるわけではありません。むしろ、それらは添付ファイルとして存在することが多いです。
今日は、それに対処します。アプリにこれらの添付ファイルを抽出し、安全にDigitalOcean Spacesに保存し、各ファイルの公開URLを生成する方法を教えます。これらのURLは最終的にデータベースに保存され、経費をレビューする際に添付ファイルをプレビューできるようになります。
さあ、始めましょう。
🚀 あなたが学ぶこと
今日のセッションの終わりまでに、あなたは次のことができるようになります:
- 添付ファイルを保存するためにDigitalOceanスペースを作成します。
- PostmarkのメールからBase64エンコードされた添付ファイルを抽出してデコードします。
- boto3を使用してDigitalOcean Spacesにファイルをアップロードします。
- 上書きを防ぐために、uuidを使用してユニークなファイル名を生成します。
- 複数の添付ファイルをシームレスに処理するために、ワークフロー全体を調整します。
🛠 必要なもの
このチュートリアルを最大限に活用するために、以下のことを前提としています。
- すでにDigitalOceanに展開されたFlaskアプリ: まだFlaskアプリを展開していない場合は、Day 7 - メールベースの領収書処理プログラムの構築と展開の指示に従うことができます。
- メールテストのために設定されたPostmark:メールから領収書の処理パイプラインをテストするには、Flaskアプリにメールを転送するためにPostmarkの設定が必要です。手順に従ったガイドについては、Day 8 - FlaskアプリへのPostmark接続を参照してください。
- DigitalOceanスペースの設定:処理された添付ファイルはDigitalOceanスペースに保存します。まだスペースをお持ちでない場合は、このチュートリアルで作成する手順をお教えします。
ノート: すべての設定が整っていなくても、以下のことを学ぶことができます:
添付ファイルを保存するためにDigitalOceanスペースを作成します。
プログラムでBase64エンコードされた添付ファイルをデコードする。
ファイルをboto3を使用してDigitalOcean Spacesにアップロードします。
Flaskアプリに添付ファイル処理をシームレスに統合します。
<$>
ステップ1:DigitalOceanスペースを作成する
まず、添付ファイルを保存する場所が必要です。DigitalOcean Spacesはオブジェクトストレージサービスで、領収書や請求書のようなファイルを安全に扱うのに最適です。スケーラブルで安全性が高く、私たちのアプリとシームレスに統合されます。
スペースを作る
DigitalOceanのダッシュボードにログインし、Spacesオブジェクトストレージをクリックします。
次に、バケットを作成をクリックします。
ユーザーに近い地域を選択してください(例:
nyc3はニューヨーク用です)。あなたのスペースに名前を付けてください(例:
email-receipts)
これにより、email-receiptsという名前のバケットが作成され、https://email-receipts.nyc3.digitaloceanspaces.comのようなURLで利用可能になります。
アクセスキーを生成する
あなたのスペースにプログラムでアクセスするには(例えば、boto3を介して)、アクセスキーとシークレットキーが必要です。
スペースを開き、設定をクリックし、アクセスキーまでスクロールします。
アクセキーを作成をクリックしてください。
アプリがファイルを読み書きし、削除できるように、すべての権限を権限に設定してください。
-
キーに名前を付ける(またはデフォルトを使用する)して、アクセスキーを作成をクリックします。
アクセスキーとシークレットキーを保存してください—シークレットキーを見るのはこれが唯一の機会です!
環境変数を更新する
DigitalOceanアプリプラットフォームのダッシュボードで:
設定 > 環境変数に移動してください。
以下を追加してください:
SPACES_ACCESS_KEY: あなたのSpacesアクセスキーID。
SPACES_SECRET_KEY: あなたのスペースシークレットキー。SPACES_BUCKET_NAME: あなたのスペースの名前(例:email-receipts).SPACES_REGION: あなたのスペースの地域 (例:nyc3)。
ステップ2: アタッチメントを処理してDigitalOcean Spacesにアップロードする
アプリで添付ファイルを処理するために、app.pyを更新し、新しい関数をいくつか作成します。各関数は特定の目的を持っており、添付ファイルのデコードからDigitalOcean Spacesへのアップロードまで行います。それぞれを一つずつ見ていきましょう。
注意: まだアプリを設定していない場合は、Day 7 - メールベースの領収書処理システムの構築とデプロイの手順に従って、それを作成しDigitalOceanのApp Platformにデプロイしてください。
添付ファイルをデコードして保存する
Postmarkは、添付ファイルをJSONペイロード内のBase64エンコードデータとして送信します。最初のステップは、このデータをデコードし、Pythonのbase64ライブラリを使用してローカルに保存することです。この関数は、uuidライブラリの助けを借りて、各ファイルに一意の名前を付けることを保証します。
Base64とは何ですか?それはバイナリファイル(PDFなど)のための翻訳者のようなものです。それはそれらをウェブ上で送信するのに安全なプレーンテキスト形式に変換します。一度それをバイナリにデコードすると、通常のファイルのように扱うことができます。
ファイルはどこに保存されますか?: デコードされたファイルは一時的に/tmpに保存されます。これはほとんどのシステムで利用可能な短期保存ディレクトリです。スクラッチパッドのようなものだと思ってください — 短期間の使用には最適で、アプリが実行を停止するとすべてがクリアされます。
添付ファイルをデコードするための関数です。ファイル名が一意であることを確認し(uuidのおかげで)、/tmpに保存します。
import os
import base64
import uuid
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
デジタルオーシャンスペースに添付ファイルをアップロードする
ファイルをデコードして保存したので、次のステップはそれらをDigitalOcean Spacesにアップロードすることです。AWS互換のAPIを扱うための強力なPython SDKであるboto3を使用して、アップロードを行います。SpacesはS3バケットのように機能するため、完璧な適合です。
この機能はファイルをあなたのスペースにアップロードし、公開URLを返します。
import boto3
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
複数の添付ファイルを処理する
すべてをまとめましょう。この関数はすべてを調整します:
- 各添付ファイルをデコードします。
- それをスペースにアップロードします。
- アップロードされたファイルのURLを収集します。
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
/inbound ルートを更新する
最後に、/inbound ルートを更新して、添付ファイルの処理を含めます。このルートは、メールの内容の処理、添付ファイルのデコードとアップロード、そして最終的な応答を返すことを行います。
@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)
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)
最終完全コード
こちらがすべての更新が含まれたapp.pyファイルです:
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
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
)
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 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
@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)
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)
ステップ3:DigitalOceanにデプロイする
更新されたFlaskアプリをデプロイするには、7日目の手順に従ってください。以下は簡単なまとめです:
更新されたコードをGitHubにプッシュする: Flaskアプリに必要な変更を加えたら、更新されたコードをコミットし、GitHubにプッシュします。これにより、DigitalOceanのApp Platformで自動デプロイがトリガーされます。
git add . git commit -m "Add attachment processing with DigitalOcean Spaces" git push origin mainモニターデプロイメント: アプリのダッシュボードのデプロイメントセクションで進捗を追跡できます。
展開を確認する: 展開が完了したら、アプリの公開URLに移動し、その機能をテストします。また、ダッシュボードでランタイムログを確認して、アプリが正常に起動したことを確認することもできます。
ステップ4:全体のワークフローをテストする
アプリが完全に設定され、準備が整ったので、全体のワークフローをテストする時が来ました。メール本文が処理され、添付ファイルがデコードされてDigitalOceanのスペースにアップロードされ、最終的な出力には必要なすべてのものが含まれることを確認します。
段階的にテストする方法は次のとおりです:
テストメールを送信: テキスト本文と添付ファイルを持つメールをPostmarkに送信します。Postmarkの設定方法がわからない場合は、「Day 8: FlaskアプリにPostmarkを接続する」を確認してください。ここでは、Postmarkを設定して、メールをアプリに転送する方法を説明しました。
ポストマークのアクティビティJSONを確認する: ポストマークのダッシュボードで、アクティビティタブに移動してください。送信したメールを見つけ、JSONペイロードにテキスト本文とBase64エンコードされた添付データが含まれていることを確認します。これにより、ポストマークが正しくメールデータをアプリに転送していることが確認できます。
ログを監視する: あなたのDigitalOcean App Platformダッシュボードで実行時ログを確認して、アプリがJSONペイロードを処理していることを確認してください。実行時ログへのアクセス方法はDay 9で説明しました。
スペースのアップロードを確認する: デジタルオーシャンスペースにアクセスして、ファイルが正常にアップロードされたことを確認してください。バケットに添付ファイルが表示されるはずです。
最終出力を確認する: アプリは抽出されたデータと添付ファイルのURLをログに記録する必要があります。これらのログには次の内容が含まれます:
- メール本文から抽出された詳細。
- アップロードされた添付ファイルの公開URL。
実行時ログの検査に関するヒントは、9日目を参照してください。
これらの手順を終える頃には、あなたのワークフローはデータベースにデータを保存する準備が整い、次にそれに取り組むことになります。
🎁 まとめ
今日は、あなたのアプリがプロのように添付ファイルを処理できるように教えました。私たちがやったことは次のとおりです。
- 安全でスケーラブルなストレージのためにDigitalOcean Spaceを作成しました。
- Postmark JSONからBase64エンコードされた添付ファイルをデコードしました。
uuidを使用してユニークなファイル名を保証しました。- boto3を使用してDigitalOcean Spacesにアップロードした添付ファイル。
- 各ファイルの生成された公開URLが、レシート処理システムで使用できる準備が整いました。
次に、このデータをデータベースに統合します。これにより、抽出したメールの詳細や添付ファイルのURLを長期的に保存でき、レシートプロセッサーがさらに強力になります。お楽しみに!












