見出し画像

We Love Python (4) ~クソだるいメール業務を何とかする[2]


メール内容をCSVに出力する

前回の続きです。

ここ数週間で、動くところまで作りました。
手が遅すぎるのは勘弁してください・・・。

Pythonコード(my_outlook.py)

import win32com.client
import csv
import datetime as dtm

# Outlook オブジェクトインスタンス(グローバル変数)
global outlook
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")

# メインルーチン定義
def main(): 
    acnt = "myaddress@test.com" # メールアドレス
    file = "email.csv" # CSV出力
    outlook2csv(acnt, file) # CSV読み出し

# outlookメールをCSVに取り出す。
def outlook2csv(acnt, file):

    # 対象アカウントを取得
    accounts = outlook.Folders
    for account in accounts:
        if account.Name == acnt:
            break

    # 対象フォルダを取得
    folders = account.Folders
    for folder in folders:
        if folder.Name == "受信トレイ":
            break

    # メールを取得
    mails = folder.Items
    mails.Sort("[ReceivedTime]", True) 
    print("Mail Get Ended")

    # CSVファイルに書き出し
    with open(file, 'w', newline='', encoding='utf_8') as csvfile:
        fieldnames = ['ReceivedTime', 'Subject', 'Sender', 'Body']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        i = 0
        for mail in mails:
            i += 1
            dt = mail.ReceivedTime - dtm.timedelta(hours=9) # UTC -> JST
            writer.writerow({
                'ReceivedTime' : dt,
                'Subject' : mail.Subject,
                'Sender' : mail.Sender,
                'Body' : mail.Body
            })

if __name__ == "__main__":
    main()

全体の説明

プログラムの流れですが、3つのimport 文と
outlook = win32com.client.dispatch(…
のあとは、いきなり最終行のmain()を実行します(その前のif 文は後述)

その間の2つのdef は関数定義です。Pythonでは変数にしろ関数にしろ、使用する前に定義しなければいけないため、このような作りになっています。

main() の本体は上の方にあり、2つの変数にメールアドレス(架空)とCSVファイルを入れたあと、outlook2csv()関数を呼びます。
直下の関数を呼ぶように見えますが、実際に呼び出しを行うのは最下部のmain() の中であるため、outlook2csv()は呼び出し前にきちんと定義されています。

結局main()って何なの?

さて、少し脱線しますが、前章で作ったテンプレートについてここで説明しておきます。

def main():
    pass

if __name__ == "__main__":
    main()

今回のプログラムは、上記テンプレートをもとに以下のような方針で書いていきました。

import 必要なモジュール

その他グローバルな定義

def main():
    メインルーチンの中身
    関数1(引数,...)
    関数2(引数,...)

def 関数1(引数,...):
    いろいろな処理

def 関数2(引数,...):
    いろいろな処理

if __name__ == "__main__":
    main()
 

一番下から2行目「_name_」という謎の変数は、Pythonの定義すみ定数で、このPythonソースファイル自体を実行するときは常に”_main_”という文字列値を持っています。
★”_”は’_'を2個並べますが、全角”_”で表現しています。

このPythonソースファイルに、test.pyという名前を付けて保存します。
test.pyをimportして中の関数たちをモジュールとして呼べるようになります。

# test2.py

import test

a = 1
test.関数1(a)

こうするとtest.py 側の_name_は”_main_”ではなくモジュール名の”test"となるため、以下条件文はFalseとなり、main()は実行されません!

_name_ == "__main_"

この仕組みはPython関数のモジュール化を支援しているといえますね。
関数を追加するときは、またtest.pyのmainで単体テストしていけばいいのです。

もう一つのdef main()を使うご利益は単純ですが、main()で呼ぶ関数定義をずらずらと下の方に書いていけることです。main()本体は一番下にありますので。

Outlookのメールを抜き取る。

outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")

で、Outlookメールオブジェクトの実体を取り出した(インスタンス化した)ので、outlook2csv()の中でOutlookメールデータを取り出します。

目的のメールボックスの「受信トレイ」を探します。

   # 対象アカウントを取得
    accounts = outlook.Folders
    for account in accounts:
        if account.Name == acnt:
            break

    # 対象フォルダを取得
    folders = account.Folders
    for folder in folders:
        if folder.Name == "受信トレイ":
            break

上記は、forループなど使わずに決め打ちで取得する例がネット記事では多く見受けられます。

#対象アカウントを取得
account = outlook.Folders(0) 

#対象フォルダを取得
folder = account.Folders(4)

しかし、対象アカウントや対象フォルダの引数を知るには、OutlookのCOM APIマニュアル(英語!)を読む必要があることを知り、アカウントやフォルダ名で検索するとしました。
(オリジナルではなく、インターネット記事からのアイデアだが、ソースの場所を忘れてしまった・・・)

対象が見つかったら、メール本体を抜き出すことができます。

# メールを取得
mails = folder.Items
mails.Sort("[ReceivedTime]", True) 

変数mails に事前に取り出したOutlook.Folders(0).Folders(4).Items オブジェクト、人間の言葉でいうとmyaddress@test.comの”受信フォルダ" のメールを取り出し、mails.Sort() メソッドで送信日をソートします。

この辺のことはもう少し調べないと、仕組みがわかりません。
何度も使ってみないと、ですかね。

CSVファイルへの書き出し

いよいよオブジェクトをCSVに書き出します。

    with open(file, 'w', newline='', encoding='utf_8') as csvfile:
        fieldnames = ['ReceivedTime', 'Subject', 'Sender', 'Body']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        i = 0
        for mail in mails:
            i += 1
            dt = mail.ReceivedTime - dtm.timedelta(hours=9) # UTC -> JST
            writer.writerow({
                'ReceivedTime' : dt,
                'Subject' : mail.Subject,
                'Sender' : mail.Sender,
                'Body' : mail.Body
            })

初めてwith文というものを使用します。
引数でわたされたfile (email.csv)を開いて書き込んでいくのですが

with open(file, 'w', newline='', encoding='utf_8) as csvfile:
    ファイルへの書き込み処理

open() 関数でファイル(第1引数)、モード(’w/r/a':第2引数)を指定し第3引数以降で改行、文字コードを指定します。

最後のas csvfile が分かりにくいですが、上のwith文は

csvfile = open(file, 'w', newline='', encoding='utf_8')
ファイルへの書き込み処理
csvfile.close()

と書き換えられます。withブロックを使うと、openの範囲がブロックで表現できてclose() を省略できるからエレガントだよね、ってお話です。

あとはcsv モジュールの関数を使い、mails.Items の各フィールドをCSV出力するだけです。
結果的にこんな出力が得られました。

いいなと思ったら応援しよう!