業務分析を行って負荷のかかっている単純作業があった場合、AIを活用してpythonプログラムを作成し、コストをかけずに解決策を提示することも可能です。
ここでは顧客への大量のメール送信を自動化する例を示します。
ここでは顧客への大量のメール送信を自動化する例を示します。
■想定シーン
既存顧客向けにチラシ等の添付ファイル付お知らせメールを大量に送信する。ここでは下書き作成まで自動化し、送信は目視確認兼ねて手動を想定する。
■事前準備
1.python開発環境Anacondaのインストール
以下のサイト等を参考に実施ください。
https://www.python.jp/install/anaconda/
2.ライブラリの事前インストール
メール操作に必要なライブラリを先にインストールする必要があります。
以下のコマンドをAnaconda Promptから入力ください。
pip install pywin32 pandas openpyxl xlrd
3.データの準備
以下のようなエクセルシートにデータを準備ください。
以下のテキストを準備ください。
テキストデータA ⇒件名を記載したもの
テキストデータB ⇒本文を記載したもの
添付ファイルを保存するフォッルダに添付ファイル(複数可能)を保存
下書格納用のフォルダ
4.プログラミング
以下のプロンプトでAI(今回はcopilot)に指示してください。
---------------
アウトルック(windows)メールの下書き(.msg)をGUIから任意に選択するフォルダに作成するpythonプログラム
を作って下さい。(送信は決してしない)
GUIから任意のエクセルファイルを選択、そのエクセルの1行目は項目名、
エクセルの2行目以降の全行についてアウトルックメール下書きを作成してください。下書きメールの各項目を以下のように設定する。
宛先(T)には選択したエクセルファイルの項目「メールアドレス」を設定
件名(U)にはGUIで任意に選択するテキストデータA
添付ファイル GUIから任意に選択するフォルダーにある全てのファイルを添付する
本文 1行目 エクセルファイルの項目「メール宛先」複数行の場合は改行を生かして複数行に記載
空白行を1行挿入
GUIで任意に選択するテキストデータBを貼り付ける
ファイル名:1行目に設定したエクセルファイルの項目「メール宛先」改行なし
---------------
以下のようにプログラムを生成してくれます。
⇒コードをコピーしてメモ帳でファイル名「create_outlook_drafts_msg.py」で保存
5.実行
以下のコマンドをAnaconda Promptから入力ください。
cd プログラムの存在するフォルダ名
python create_outlook_drafts_msg.py
⇒GUIの指示に従って準備したリストなどを選択
全て選択すればメール下書きが指定フォルダに自動作成されます。
■エラー時の対処
Anaconda Promptに表示されるエラーメッセージをAIに読み込ませるとデバックしてくれます。
例えば指定するファイルが開かれているとエラーになります。
■コード全文
# -*- coding: utf-8 -*-
import os
import re
import sys
import traceback
import pandas as pd
EXCEL_ENGINE_XLSX = "openpyxl"
EXCEL_ENGINE_XLS = "xlrd"
OL_SAVEAS_MSG = 3
# GUI
import tkinter as tk
from tkinter import filedialog, messagebox
# Outlook COM
import win32com.client as win32
import pythoncom
def log(msg):
print(msg, flush=True)
def read_text_file(path):
for enc in ("utf-8", "cp932"):
try:
with open(path, "r", encoding=enc) as f:
return f.read()
except Exception:
continue
with open(path, "rb") as f:
return f.read().decode("utf-8", errors="replace")
def sanitize_filename(name):
"""Windows の禁止文字を置換し、長過ぎる場合は短縮"""
name = re.sub(r'[\\/:*?"<>|]+', "_", str(name))
name = name.strip()
return (name[:100] or "no_subject")
def load_excel_dataframe(excel_path):
ext = os.path.splitext(excel_path)[1].lower()
if ext == ".xlsx":
df = pd.read_excel(excel_path, engine=EXCEL_ENGINE_XLSX)
elif ext == ".xls":
df = pd.read_excel(excel_path, engine=EXCEL_ENGINE_XLS)
else:
try:
df = pd.read_excel(excel_path, engine=EXCEL_ENGINE_XLSX)
except Exception:
df = pd.read_excel(excel_path)
return df
def ensure_outlook():
try:
pythoncom.CoInitialize()
except Exception:
pass
log("[INFO] Outlook.Application を取得します...")
outlook = win32.Dispatch("Outlook.Application")
log("[INFO] Outlook.Application 取得済み")
return outlook
def collect_attachments(attach_folder):
files = []
try:
entries = os.listdir(attach_folder)
except Exception:
entries = []
for entry in entries:
fp = os.path.join(attach_folder, entry)
if os.path.isfile(fp):
files.append(fp)
return files
def build_body_linebreaks(text):
if text is None:
return ""
s = str(text)
s = s.replace("\r\n", "\n").replace("\r", "\n")
return s.replace("\n", "\r\n")
def create_msg_draft(outlook, to_addr, subject, body_text, attachments, save_dir, index, filename_label=""):
"""
1件分の MailItem を作成し、.msg で保存(送信しない)。
ファイル名ラベルに「メール宛先」1行目(改行なし)を使う。
"""
mail = outlook.CreateItem(0) # olMailItem
mail.To = to_addr or ""
mail.Subject = subject or ""
mail.Body = body_text or ""
for fp in attachments:
try:
mail.Attachments.Add(fp)
except Exception as e:
log(f"[WARN] 添付失敗: {fp} => {e}")
# ---- ファイル名の組み立て(指定仕様)----
# メール宛先(1行目・改行なし)をラベルに
safe_label = sanitize_filename(filename_label or "")
safe_subject = sanitize_filename(subject or "")
base_name = f"{index:03d}_{safe_label}_{safe_subject}"
out_path = os.path.join(save_dir, base_name + ".msg")
mail.SaveAs(out_path, OL_SAVEAS_MSG)
return out_path
def ask_file(title, patterns, parent):
path = filedialog.askopenfilename(parent=parent, title=title, filetypes=patterns)
log(f"[DEBUG] 選択結果: {title} => {path if path else '(未選選択)'}")
return path
def ask_dir(title, parent):
path = filedialog.askdirectory(parent=parent, title=title)
log(f"[DEBUG] 選択結果: {title} => {path if path else '(未選択)'}")
return path
def console_fallback(prompt):
log(f"[FALLBACK] {prompt}")
try:
path = input("> 入力(空でスキップ): ").strip()
except EOFError:
path = ""
return path
def main():
print(f"[ENV] Python: {sys.executable}", flush=True)
root = tk.Tk()
root.title("Outlook下書き作成ツール")
root.geometry("300x50+100+100")
root.attributes("-topmost", True)
root.lift()
try:
try:
root.call("tk", "scaling", 1.3)
except Exception:
pass
messagebox.showinfo("開始", "処理を開始します。ダイアログに従って選択してください。", parent=root)
log("[STEP] エクセルファイル選択")
excel_path = ask_file(
"エクセルファイルを選択してください(1行目が項目名)",
[("Excel files", "*.xlsx;*.xls"), ("All files", "*.*")],
parent=root
)
if not excel_path:
excel_path = console_fallback("GUIが表示されない場合、エクセルファイルのフルパスを入力してください。")
if not excel_path or not os.path.isfile(excel_path):
messagebox.showerror("エラー", "エクセルファイルが選択/入力されていません。処理を中止します。", parent=root)
log("[ERROR] エクセル未選択")
return
log("[STEP] 件名用テキスト(A)選択")
subject_text_path = ask_file(
"件名用テキスト(A)ファイルを選択してください",
[("Text files", "*.txt;*.csv;*.md"), ("All files", "*.*")],
parent=root
)
if not subject_text_path:
subject_text_path = console_fallback("件名用テキスト(A)ファイルのフルパスを入力してください。")
if not subject_text_path or not os.path.isfile(subject_text_path):
messagebox.showerror("エラー", "件名用テキスト(A)が選択/入力されていません。処理を中止します。", parent=root)
log("[ERROR] 件名A未選択")
return
subject_text = read_text_file(subject_text_path).strip()
log("[STEP] 本文用テキスト(B)選択")
body_b_path = ask_file(
"本文用テキスト(B)ファイルを選択してください",
[("Text files", "*.txt;*.csv;*.md"), ("All files", "*.*")],
parent=root
)
if not body_b_path:
body_b_path = console_fallback("本文用テキスト(B)ファイルのフルパスを入力してください。")
if not body_b_path or not os.path.isfile(body_b_path):
messagebox.showerror("エラー", "本文用テキスト(B)が選択/入力されていません。処理を中止します。", parent=root)
log("[ERROR] 本文B未選択")
return
body_b = read_text_file(body_b_path)
log("[STEP] 添付フォルダ選択")
attach_folder = ask_dir("添付ファイルを含むフォルダを選択してください", parent=root)
if not attach_folder:
attach_folder = console_fallback("添付フォルダのフルパスを入力してください。")
if not attach_folder or not os.path.isdir(attach_folder):
messagebox.showerror("エラー", "添付フォルダが選択/入力されていません。処理を中止します。", parent=root)
log("[ERROR] 添付フォルダ未選択")
return
attachments = collect_attachments(attach_folder)
log(f"[INFO] 添付件数: {len(attachments)}")
log("[STEP] 保存先フォルダ選択")
save_dir = ask_dir(".msg の保存先フォルダを選択してください", parent=root)
if not save_dir:
save_dir = console_fallback(".msg 保存先フォルダのフルパスを入力してください。")
if not save_dir or not os.path.isdir(save_dir):
messagebox.showerror("エラー", "保存先フォルダが選択/入力されていません。処理を中止します。", parent=root)
log("[ERROR] 保存先未選択")
return
log("[STEP] エクセル読み込み")
try:
df = load_excel_dataframe(excel_path)
except Exception as e:
messagebox.showerror("エラー", f"エクセルの読込に失敗しました:\n{e}", parent=root)
log(f"[ERROR] エクセル読込失敗: {e}")
return
required_cols = ["メールアドレス", "メール宛先"]
missing = [c for c in required_cols if c not in df.columns]
if missing:
messagebox.showerror("エラー", f"エクセルに必須列がありません: {missing}\n(列名は完全一致で「メールアドレス」「メール宛先」)", parent=root)
log(f"[ERROR] 必須列不足: {missing}")
return
outlook = ensure_outlook()
created_paths = []
for i, row in df.iterrows():
to_addr = str(row.get("メールアドレス") or "").strip()
dest_text = row.get("メール宛先")
if pd.isna(dest_text):
dest_text = ""
# ---- ファイル名用の「メール宛先」1行目(改行なし)を抽出 ----
dest_firstline = ""
if str(dest_text).strip():
# splitlines() は \r\n, \r, \n すべてに対応。先頭行のみ使用。
dest_firstline = str(dest_text).splitlines()[0]
# 改行は含めない仕様なので、念のため改行文字を除去
dest_firstline = dest_firstline.replace("\r", "").replace("\n", "")
# 本文 = 「メール宛先」(改行保持) + 空白行 + B
body_full = build_body_linebreaks(dest_text) + "\r\n\r\n" + build_body_linebreaks(body_b)
try:
out_path = create_msg_draft(
outlook=outlook,
to_addr=to_addr,
subject=subject_text,
body_text=body_full,
attachments=attachments,
save_dir=save_dir,
index=i + 2, # ヘッダーが1行目のため
filename_label=dest_firstline or to_addr or "no_label", # 空ならメールアドレス等でフォールバック
)
created_paths.append(out_path)
log(f"[OK] Saved: {out_path}")
except Exception as e:
log(f"[ERROR] 行 {i+2} の下書き作成に失敗: {e}")
traceback.print_exc()
message = f"下書き(.msg)の作成が完了しました。\n作成数: {len(created_paths)} 件\n保存先: {save_dir}"
messagebox.showinfo("完了", message, parent=root)
log("[DONE] " + message)
except Exception as e:
traceback.print_exc()
messagebox.showerror("エラー", f"処理中にエラーが発生しました:\n{e}", parent=root)
log(f"[FATAL] {e}")
finally:
try:
root.destroy()
except Exception:
pass
if __name__ == "__main__":
main()
0 件のコメント:
コメントを投稿