代理店から返信があった納入価をエクセルファイルに書き込む

English version:

実現したこと

以前、研究室のネットワークドライブに用意している発注表からメールで注文や見積を飛ばすPythonスクリプトを紹介しました。

今回は、見積を依頼するメールに対して代理店から納入価の返信が来たら、価格情報などをエクセルファイルに飛ばすPythonスクリプトを紹介します。

スクリプトの概要

  • メールの取得と解析:

    • IMAPを使用して指定されたメールボックスに接続します。

    • 直近1週間以内の「見積のお願い」という件名のメールを検索します。

    • 見つかったメールの本文を抽出し、送信者の情報と合わせて保存します。

  • データの抽出:

    • OpenAI GPT-4o-mini APIを利用して、メール本文から製品名、会社名、品番、定価、納入価格などの重要情報を抽出します。

    • 抽出されたデータは構造化され、後の処理に備えます。

  • 代理店情報の紐付け:

    • 事前に定義された代理店とメールアドレスのマッピングを使用して、各メールに対応する代理店を特定します。

  • Excelファイルへの出力:

    • 抽出されたデータをpandasデータフレームに変換します。

    • 既存のExcelファイルに新しいデータを追加します。各項目は指定された列に書き込まれます。

環境構築

必要なものをインストール

conda install -c conda-forge pandas openpyxl openai python-dotenv

スクリプト

import imaplib
import email
from email.header import decode_header
import os
import datetime
import pandas as pd
from openai import OpenAI
from openai import BadRequestError
from email.utils import parseaddr
from pathlib import Path
import re
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

# 環境変数からセンシティブな情報を取得
username = os.getenv('EMAIL_USERNAME')
password = os.getenv('EMAIL_PASSWORD')
imap_url = os.getenv('IMAP_URL')

# 実行しているOSを確認
if os.name == 'nt':  # Windowsの場合
    excel_file_path = Path(os.getenv('EXCEL_FILE_PATH_WINDOWS'))
elif os.name == 'posix':  # macOSの場合
    excel_file_path = Path(os.getenv('EXCEL_FILE_PATH_MAC'))
else:
    raise EnvironmentError("Unsupported Operating System")

# OpenAI APIの設定
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

# 代理店とメールアドレスのマッピング
agency_mapping = {
    '代理店A': {'email1@example.com', 'email2@example.com'},
    '代理店B': {'email3@example.com'},
    '代理店C': {'email4@example.com'},
    '代理店D': {'email5@example.com'},
    '代理店E': {'email6@example.com', 'email7@example.com'},
}

def decode_mime_words(s):
    return u''.join(
        word.decode(encoding or 'utf-8') if isinstance(word, bytes) else word
        for word, encoding in decode_header(s))

# メールからデータを取得する関数
def get_mail_data(username, password, imap_url, retries=50):
    attempt = 0
    while attempt < retries:
        try:
            mail = imaplib.IMAP4_SSL(imap_url, 993)
            mail.login(username, password)
            mail.select("INBOX")

            # 直近1週間の日付を計算
            one_week_ago = (datetime.datetime.now() - datetime.timedelta(days=7)).strftime("%d-%b-%Y")

            # 直近1週間のメールの検索と取得
            status, messages = mail.search(None, f'SINCE "{one_week_ago}"')
            messages = messages[0].split(b' ')
            extracted_data = []

            if not messages or messages == [b'']:
                print("No matching emails found.")
                mail.logout()
                return extracted_data

            for mail_id in messages:
                res, msg = mail.fetch(mail_id, '(RFC822)')
                for response in msg:
                    if isinstance(response, tuple):
                        msg = email.message_from_bytes(response[1])
                        subject = decode_mime_words(msg.get("Subject"))

                        if '見積のお願い' in subject:
                            from_address = decode_mime_words(msg.get("From"))
                            from_address = parseaddr(from_address)[1]
                            print("From address:", from_address)
                            body = ""

                            if msg.is_multipart():
                                for part in msg.walk():
                                    if part.get_content_type() == "text/plain":
                                        body = part.get_payload(decode=True)
                                        charset = part.get_content_charset()
                                        body = body.decode(charset, errors='replace') if charset else body.decode('utf-8', errors='replace')

                            else:
                                body = msg.get_payload(decode=True)
                                charset = msg.get_content_charset()
                                body = body.decode(charset, errors='replace') if charset else body.decode('utf-8', errors='replace')

                            body = body.replace("\r\n", " ")
                            agency = next((key for key, val in agency_mapping.items() if from_address in val), "不明")

                            extracted_data.append((body, agency))

            mail.logout()
            return extracted_data

        except imaplib.IMAP4.error as e:
            print(f"Login attempt {attempt + 1} failed: {e}")
            attempt += 1

    print("All login attempts failed.")
    return []

# OpenAI APIを使ってデータを抽出する関数
def extract_data_with_gpt(text_data, agency_mapping):
    parsed_data = []
    print("Input Data:", text_data)

    for text, agency in text_data:
        text = str(text)
        text = text.encode('utf-8', errors='ignore').decode('utf-8')
        text = re.sub(r'[^\x20-\x7Eぁ-んァ-ン一-龥ー]', ' ', text)

        if len(text) > 5000:
            print("Text too long, truncating")
            text = text[:5000]

        print("Sending text to API:", text[:500])
        try:
            chat_completion = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[
                    {"role": "system", "content": "以下のテキストから製品名、会社、品番、定価、納入価を抽出してください。その際、定価と納入価については数字の価格情報のみにして下さい。最後に得られた情報をPandas dataframeに適した「セミコロン区切り形式」にしてください。データフレーム構造は「製品名;会社名;品番;定価;納入価格」ですのでヘッダーとして0行目に配置してください。レスポンスにはセミコロン区切り形式のデータだけ含めてください。"},
                    {"role": "user", "content": text}
                ]
            )
            
            print("API Response:", chat_completion)
            extracted_info = chat_completion.choices[0].message.content
            print("Extracted info:", extracted_info)

            if ';' in extracted_info:
                data_rows = [item.split(';') for item in extracted_info.split("\n") if item.strip() and "製品名" not in item]
            else:
                product_name = [line.split(': ')[1] for line in extracted_info.split("\n") if line.startswith('製品名')]
                company_name = [line.split(': ')[1] for line in extracted_info.split("\n") if line.startswith('会社名')]
                product_number = [line.split(': ')[1] for line in extracted_info.split("\n") if line.startswith('品番')]
                list_price = [line.split(': ')[1] for line in extracted_info.split("\n") if line.startswith('定価')]
                delivery_price = [line.split(': ')[1] for line in extracted_info.split("\n") if line.startswith('納入価')]
                data_rows = list(zip(product_name, company_name, product_number, list_price, delivery_price))

            print("Data rows before adding agency:", data_rows)
        
            for row in data_rows:
                row.append(agency)

            print("Data rows after adding agency:", data_rows)

            df = pd.DataFrame(data_rows, columns=["製品名", "会社名", "品番", "定価", "納入価格", "代理店"])
            parsed_data.append(df)

        except BadRequestError as e:
            print("Error with API request:", e)

    return pd.concat(parsed_data, ignore_index=True) if parsed_data else pd.DataFrame()

def write_to_excel(df, excel_file_path):
    with pd.ExcelWriter(excel_file_path, engine='openpyxl', mode='a', if_sheet_exists='overlay') as writer:
        if 'Sheet1' in writer.book.sheetnames:
            start_row = writer.book['Sheet1'].max_row
        else:
            start_row = 0

        column_positions = {"製品名": 0, "会社名": 1, "品番": 2, "定価": 4, "納入価格": 5, "代理店": 8}
        for column in df.columns:
            col_idx = column_positions.get(column, 0)
            df[column].to_excel(writer, sheet_name='Sheet1', startcol=col_idx, startrow=start_row, index=False, header=False)

# メインの処理
if __name__ == "__main__":
    mail_data = get_mail_data(username, password, imap_url)
    df = extract_data_with_gpt(mail_data, agency_mapping)
    print(df)
    write_to_excel(df, excel_file_path)

.env

# メールサーバー設定
EMAIL_USERNAME=your_email@example.com
EMAIL_PASSWORD=your_email_password
IMAP_URL=imap.example.com

# OpenAI API設定
OPENAI_API_KEY=your_openai_api_key

# Excelファイルのパス
EXCEL_FILE_PATH_WINDOWS=C:\path\to\your\excel\file.xlsx
EXCEL_FILE_PATH_MAC=/path/to/your/excel/file.xlsx

まとめ

これで、各代理店毎の納入価格を自動的にデータベース化できます。
ちなみに処理した後は、INBOXからDONEというフォルダにメール移動してます。

これで教育・研究活動に時間が使えるようになりハッピーになると良いですね。

他にも色々と仕組みを動かしていますので、都度紹介します!

質問や改善点があれば、ぜひコメントで教えてください。










この記事が気に入ったらサポートをしてみませんか?