はじめに
仕事で使用する端末がWindows環境の場合、グループウェアはMicrosoft 365を採用している組織が多いと思います。
日々仕事を行うにあたり、以下の様なルーチンワークを繰り返し行なっている場合は、自動化できる可能性があります。
- ファイルの加工
- Outlookでメールの送信
- ブラウザ操作
本記事では、上記の様な単純作業についてPythonで自動化するためのヒントについて記載しています。
Pythonのインストール
Windows環境におけるPythonのインストール方法は公式ドキュメントの「4. Windows で Python を使う」をご参照ください。
インストール時のポイントとして「Add Python 3.x to PATH」にチェックすることで、環境変数 PATHにインストールディレクトリが追加されます。
ファイル操作
まずはファイルをオブジェクトとして扱う方法を知りましょう。
ファイルの基本操作
Pythonでファイルを操作する場合、pathlibやosモジュールを使用することで、基本的な操作が可能です。
なお、pathlibやos以外にもいくつかの方法や外部ライブラリが存在します。
以下はpathlibやosモジュール用いて、ファイルの存在確認を行う例です。
- pathlib
from pathlib import Path
# ファイルパスを定義
image_path = Path('./images') / 'python.png'
# ファイルの存在を確認
if image_path.exists():
print(f'{image_path} exists.')
else:
print(f'{image_path} does not exist.')
- os
import os
def check_file(file: str) -> Path:
if not os.path.exists(file):
raise FileNotFoundError(f'{file} not found')
return Path(file)
pathlibを使用するにあたって/
を使ってパスを結合しても問題ありません。Windowsではバックスラッシュである\
が通常のパス区切り文字ですが、pathlibが適切に処理します。
CSVファイルの読み込み
csvモジュールのcsv.DictReader
クラスを使用することで、各行を辞書として読み込むことができます。
import csv
def load_csv(csv_file: str) -> list[dict[str, str]]:
department = []
with open(csv_file, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
department.append(row)
return department
CSVファイルのデータ集計
Jupyter Notebookとpandasを使用することで、Excelを使用しなくても簡単にデータ集計を行うことができます。
以下はsample.csv
ファイルのカテゴリ列をグループ化して、優先度の列を集計する例です。
import pandas as pd
csv_file = 'sample.csv'
df = pd.read_csv(csv_file, encoding='CP932')
result = df.groupby('カテゴリ', dropna=False)['優先度'].value_counts().unstack(fill_value=0)
Windows環境で日本語を含むCSVファイルを扱うにあたり、ファイルがCP932でエンコードされている場合は、encoding='CP932'
を指定する必要があります。
ファイルの加工
システム運用では、元ファイルを加工してデータを取り込み、システムにインポートすることがよくあります。
以下はpandasを用いてCSVファイルを加工し、新しいファイルを生成する例です。pandasを活用することで、特定の列を削除したり、列名を変更するなどの作業を自動化できます。
import argparse
import datetime
import shutil
import pandas as pd
def copy_file(source_path: str, destination_path: str):
try:
shutil.copy(source_path, destination_path)
print(f'File {source_path} successfully copied to {destination_path}')
except Exception as e:
print(f'Error copying file: {e}')
def get_args() -> str:
parser = argparse.ArgumentParser()
parser.add_argument('-f', type=str, help='CSVファイル名', required=True)
return parser.parse_args()
def get_day() -> str:
today = datetime.date.today().strftime('%Y%m%d')
return today
def remove_columns_by_index(df, start: int, end: int):
return df.drop(df.columns[start:end], axis=1)
def rename_columns(df, columns: str, new_columns: str):
return df.rename(columns={columns:new_columns})
if __name__ == "__main__":
args = get_args()
if args.f:
source_file = args.f
day = get_day()
destination_file = f'output{day}.csv'
copy_file(source_file, destination_file)
df = pd.read_csv(destination_file)
print(f'変更前:{df.head()}')
# A~G列を削除
df = remove_columns_by_index(df, 0, 7)
# カラムの名称を「foo」→「bar」に変更
df = rename_columns(df, 'foo', 'bar')
# D~E列を削除
df = remove_columns_by_index(df, 3, 5)
print(f'変更後:{df.head()}')
df.to_csv(destination_file, encoding="utf-8_sig", index=False)
オプションにencoding="utf-8_sig"
を指定することで、ファイルの先頭にBOMが付与されます。CSVファイルをExcelで開く際の文字化けを防ぐことができます。
フォルダ作成、コピー、文字列置換、圧縮、削除
例えば、グループごとにフォルダを作成し、テンプレートとなるファイルをコピーして中身を編集し、最後に圧縮及び削除などの作業を繰り返す場合、非常に手間がかかります。
以下は上記の様なフォルダ及びファイルに関する手作業を自動化する例です。
def process_department_templates(department_list: list[dict[str, str]],
project_name: str,
templates_path: str,
) -> None:
if not os.path.exists('work'):
work = Path('work')
work.mkdir(exist_ok=True)
if project_name == 'A':
batch_file = BATCH_FILE_A
line_number = LINE_NUMBER_A
tag = TAG_A
elif project_name == 'B':
batch_file = BATCH_FILE_B
line_number = LINE_NUMBER_B
tag = TAG_B
os.chdir('work')
for _ in department_list:
work_folder = Path.cwd()
# フォルダ作成
make_folder(work_folder, _['Department'])
project_folder = Path(f'{work_folder}/{_['Department']}/{project_name}')
# ファイルのコピー
shutil.copytree(f'../{templates_path}', project_folder, dirs_exist_ok=True)
# 文字列置換処理
with open(f'../{templates_path}/{batch_file}', 'r') as templates:
lines = templates.readlines()
if line_number <= len(lines):
lines[line_number - 1] = lines[line_number - 1].replace(tag, _['Tag'])
with open(project_folder/batch_file, 'w') as edited_file:
edited_file.writelines(lines)
# zipファイルに圧縮
os.chdir(f'{work_folder}/{_['Department']}')
shutil.make_archive(templates_path, 'zip', root_dir=project_folder)
# フォルダ削除
shutil.rmtree(project_name)
os.chdir(work_folder)
shutilモジュールは、ファイルやフォルダに関する高水準の操作方法を提供します。
Outlook
pywin32は、PythonからWindowsのAPIを呼び出すことができるライブラリです。
Component Object Model(COM)オブジェクトを操作することができるため、OutlookなどMicrosoft 365のアプリケーションに関する操作が可能です。(※すべてのMicrosoft 365アプリケーションを操作できるわけではありません)
定型文を使用した複数のメール送信
社内などで定型文を使用して複数の部署宛にメール送信を行う場合、適宜宛先を用意して、本文も編集することで、非常に手間がかかります。
以下はメール送信先のメールアドレスや部署名が書かれたCSVファイルを基に、繰り返しメールを送信する例です。共通の設定は.ini
ファイルを使用し、configparserモジュールを用いて読み込んでいます。
- config.ini
[DEFAULT]
sender_email_address = foo@email.com
cc = bar@email.com
subject = 件名
text = 本文
sign = 署名
- 送信プログラム
import argparse
import configparser
import csv
import os
from pprint import pprint
from string import Template
import win32com.client
config = configparser.ConfigParser()
config.read('config.ini', encoding='utf-8')
config_default = config['DEFAULT']
def get_args() -> str:
parser = argparse.ArgumentParser()
parser.add_argument('-a', type=str, help='添付ファイル名')
parser.add_argument('-f', type=str, help='CSVファイル名', required=True)
return parser.parse_args()
def load_recipients(csv_file: str) -> list:
recipients = []
with open(csv_file, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
recipients.append(row)
return recipients
def send_emails(recipients: list[dict[str, str]]):
outlook_app = win32com.client.Dispatch('Outlook.Application')
template = Template("$body $sign")
for recipient in recipients:
# 0で新規作成
mail_item = outlook_app.CreateItem(0)
mail_item.SentOnBehalfOfName = config_default.get('sender_email_address')
mail_item.to = f"{recipient['to_email_address1']};{recipient['to_email_address2']};{recipient['to_email_address3']};"
mail_item.cc = f"{recipient['cc_email_address1']};{recipient['cc_email_address2']};{recipient['cc_email_address3']};{config_default.get('cc')}"
mail_item.Subject = config_default.get('subject')
body = f"{recipient['department']}\n{config_default.get('text')}"
mail_item.Body = template.substitute(body=body, sign=config_default.get('sign'))
if attached is True:
file_path = rf"{attachments}"
mail_item.Attachments.Add(file_path)
# 確認
mail_item.Display(True)
if __name__ == "__main__":
args = get_args()
attached = False
if args.f:
csv_file_path = args.f
print(f'処理対象CSVファイル:{csv_file_path}')
if args.a:
attached = True
attachments = os.path.abspath(args.a)
print(f'添付ファイル:{os.path.abspath(args.a)}')
recipients = load_recipients(csv_file_path)
pprint(recipients)
send_emails(recipients)
メール送信を行う場合はSend()
メソッドを使用します。Display(True)
メソッドを使用することで、メール送信をする前に確認ができます。
特定のメールを抽出して返信
メールアイテムの中から、件名に特定の文字列が含まれているメールを抽出し、そのメールに返信する処理を行うこともできます。
以下は送信済みアイテムを基にキーワードで検索を行い特定のメールを抽出し、添付ファイルを付けて返信する例です。
import argparse
import configparser
import csv
from datetime import date, datetime
import os
import win32com.client
from ansi_escape_sequences import ColorText
def get_args() -> str:
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true', help='添付ファイル')
parser.add_argument('-f', type=str, help='設定ファイル')
parser.add_argument('-n', type=str, help='抽出する数', required=True)
parser.add_argument('-w', type=str, help='検索する件名')
return parser.parse_args()
def applying_configfile(conf_file: str):
config = configparser.ConfigParser()
config.read(conf_file, encoding='utf-8')
config_default = config['DEFAULT']
return config_default
def cehack_accounts() -> object:
outlook_app = win32com.client.Dispatch('Outlook.Application').GetNamespace("MAPI")
accounts = outlook_app.Folders
print("root (アカウント数=%d)" % accounts.Count)
for account in accounts:
print("└ ",account)
folders = account.Folders
for folder in folders:
print(" └ ",folder)
return accounts["hoge"]
def get_input_result(word: str) -> str:
ColorText.print_colored(f'{word} datetime', 'red')
datetime_str = input('Enter the date in YYYYMMDDHHMM format: ')
return datetime_str
def received_emails(account: str) -> list[str]:
folders = account.Folders
received_emails = folders["送信済みアイテム"]
# 全件取得して降順にソート
all_item = received_emails.Items
all_item.Sort("[ReceivedTime]", True)
# 日付でフィルターする
start_datetime_str = get_input_result('start')
start_datetime = datetime.strptime(start_datetime_str, "%Y%m%d%H%M")
end_datetime_str = get_input_result('end')
end_datetime = datetime.strptime(end_datetime_str, "%Y%m%d%H%M")
print(f"抽出期間:{start_datetime}:{end_datetime}\n")
restriction = f"[ReceivedTime] >= '{start_datetime.strftime('%Y-%m-%d %H:%M')}' AND [ReceivedTime] <= '{end_datetime.strftime('%Y-%m-%d %H:%M')}'"
all_item = all_item.Restrict(restriction)
# -wオプションの引数がある場合、キーワードを基に検索 ※大文字小文字を区別しない場合は、ci_phrasematchを設定
if search_title:
all_item = all_item.Restrict(f"@SQL=\"urn:schemas:httpmail:subject\" like '%{search_title}%'")
num = 0
result_list = []
for index, item in zip(range(num, int(max_num)), all_item):
try:
print(f"[No]:{index + 1} [Received time]:{item.receivedtime} [SenderName]{item.sendername} [Subject]:{item.subject}")
result_list.append([index + 1, str(item.receivedtime), item.sendername, item.subject, item.body])
# 全員に返信
replay_mail = item.ReplyAll()
# オリジナルメッセージを残す
original_body = replay_mail.body
try:
body = config_default.get('text')
except NameError:
body = ''
replay_mail.Body = f"{body}{original_body}"
# 添付ファイル
if attached is True:
current_folder = os.getcwd()
for filename in os.listdir():
file_path = os.path.join(current_folder, filename)
if os.path.isfile(file_path):
replay_mail.Attachments.Add(file_path)
# 確認
replay_mail.Display(True)
except AttributeError as e:
print(f"[No]:{index + 1} is {e}")
continue
except Exception as e:
raise BatchError('受信メール取得エラー')
return result_list
if __name__ == "__main__":
args = get_args()
attached = False
max_num = args.n
if args.a:
attached = True
if args.f:
conf_file = args.f
config_default = applying_configfile(conf_file)
if args.w:
search_title = args.w
target_account = cehack_accounts()
print(f"処理対象アカウント:{target_account}")
result_list = received_emails(target_account)
ReplyAll()
は元のメッセージから元のすべての受信者に対する返信を作成します。
予定表の抽出
Outlookの予定表を抽出したいと思ったことはありませんでしょうか。
以下はOutlookの予定表を抽出して出力する例です。オプションを指定すると、任意の日付の予定を抽出します。
import argparse
from datetime import date, datetime, timedelta
import os
import win32com.client
from ansi_escape_sequences import ColorText
def get_args() -> str:
parser = argparse.ArgumentParser()
parser.add_argument('-d', action='store_true', help='日付モード')
return parser.parse_args()
def get_today() -> datetime:
today = datetime.now()
start_date_yyyy, start_date_mm, start_date_dd = map(int, (today.strftime('%Y'), today.strftime('%m'), today.strftime('%d')))
today = today.today() + timedelta(days=1)
end_date_yyyy, end_date_mm, end_date_dd = map(int, (today.strftime('%Y'), today.strftime('%m'), today.strftime('%d')))
start_date = date(start_date_yyyy, start_date_mm, start_date_dd)
end_date = date(end_date_yyyy, end_date_mm, end_date_dd)
return start_date, end_date
def get_input_result() -> int:
ColorText.print_colored('start_date: YYYY MM DD', 'red')
start_date_yyyy, start_date_mm, start_date_dd = map(int, input().split())
ColorText.print_colored('end_date: YYYY MM DD', 'red')
end_date_yyyy, end_date_mm, end_date_dd = map(int, input().split())
start_date = date(start_date_yyyy, start_date_mm, start_date_dd)
end_date = date(end_date_yyyy, end_date_mm, end_date_dd)
return start_date, end_date
def get_meetinsStatus(OlMeetingStatus: int) -> str:
if OlMeetingStatus == 1:
status = 'olMeeting'
elif OlMeetingStatus == 3:
status = 'olMeetingReceived'
elif OlMeetingStatus == 5:
status = 'olMeetingCanceled'
elif OlMeetingStatus == 7:
status = 'olMeetingReceivedAndCanceled'
elif OlMeetingStatus == 0:
status = 'olNonMeeting'
return status
def get_calendar() -> str:
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
calender = outlook.GetDefaultFolder(9)
items = calender.Items
items.Sort("[Start]")
items.IncludeRecurrences = True
restriction = f"[Start] >= '{start_date:%Y-%m-%d}' AND [End] <= '{end_date:%Y-%m-%d}'"
select_items = []
select_items = items.Restrict(restriction)
for select_item in select_items:
ColorText.print_colored(f'タイトル:{select_item.subject}', 'underline')
print('場所 :', select_item.location)
print('開始時刻:', select_item.start)
print('終了時刻:', select_item.end)
print('状態:', get_meetinsStatus(select_item.MeetingStatus))
print("本文:", select_item.body)
print('')
if __name__ == "__main__":
os.system('CLS')
args = get_args()
if args.d:
start_date, end_date = get_input_result()
else:
start_date, end_date = get_today()
get_calendar()
会議の状態は、OlMeetingStatus
プロパティで確認できます。
予定の追加
予定の追加もできます。
以下は参加者のメールアドレスが記載されたテキストファイルを基に、一予定を追加する例です。
import argparse
from datetime import date, datetime, timedelta
import win32com.client
def get_args() -> str:
parser = argparse.ArgumentParser()
parser.add_argument('-f', type=str, help='ファイル名')
return parser.parse_args()
def get_input_result(word: str) -> str:
ColorText.print_colored(f'{word} datetime', 'red')
datetime_str = input('Enter the date in YYYYMMDDHHMM format: ')
return datetime_str
def get_input_title() -> str:
ColorText.print_colored(f'title', 'red')
title = input('Enter the title: ')
return title
def add_recipients(appointment: object, filename: str):
with open(filename, 'r') as file:
for line in file:
email_address = line.strip()
if email_address:
recipient = appointment.Recipients.Add(email_address)
recipient.Type = 1
def add_calendar() -> str:
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
calender = outlook.GetDefaultFolder(9)
items = calender.Items
appointment = items.Add()
start_datetime_str = get_input_result('start')
start_datetime = datetime.strptime(start_datetime_str, "%Y%m%d%H%M")
end_datetime_str = get_input_result('end')
end_datetime = datetime.strptime(end_datetime_str, "%Y%m%d%H%M")
# 件名
appointment.Subject = get_input_title()
# 日時
appointment.Start = start_datetime + timedelta(hours=9)
appointment.End = end_datetime + timedelta(hours=9)
# 参加者
if file_path:
add_recipients(appointment, file_path)
if not appointment.Recipients.ResolveAll():
print("error")
return
# 保存
appointment.Save()
if __name__ == "__main__":
args = get_args()
if args.f:
file_path = args.f
add_calendar()
その他
SeleniumでEdgeを操作する
SeleniumでEdgeを操作するために、ブラウザオブジェクトを作成する例について以下に記載します。
また、webdriver_managerのライブラリを用いてドライバーのバージョンアップを自動化します。webdriver_managerについては以前書いた「Seleniumで使用するドライバのバージョンアップを自動化する」をご参照ください。
from selenium import webdriver
from selenium.webdriver.edge.service import Service as EdgeService
from webdriver_manager.microsoft import EdgeChromiumDriverManager
driver = EdgeChromiumDriverManager().install()
service = EdgeService(executable_path=driver)
options = webdriver.EdgeOptions()
browser = webdriver.Edge(service=service, options=options)
browser.maximize_window()
Pyinstallerによるexe化
日々行っているルーチンワークをツール化して他の人にも使用してもらうためには、作成したプログラムを配布する必要があります。
そのため、プログラムを利用する側の環境が整ってない場合、プログラムの実行がハードルになりやすいです。
従って環境依存を防ぎ、簡単にプログラムを実行させたい場合、Windows環境におけるexe化は一つの解決策です。
以下Pyinstallerというライブラリを使用することで、実現できます。
実行方法も簡単です。exe化したいプログラムを引数にして実行するだけです。
> pyinstaller /path/to/yourscript.py
exe化してもargparseライブラリの引数は同様に使用できます。
batファイルからPythonを実行
作成したPythonのプログラムをWindowsのbatファイルから呼び出す場合、ダブルクリックで実行できるため、ちょっと動かしたいときなどは便利だと思います。
以下はcall
コマンドを用いて、仮想環境を有効化するコマンドが書かれたバッチファイルを呼び出した後に、プログラムを実行する例です。また、バッチファイルに引数を付与して、argparseの処理を制御しています。
- activate.bat
@echo off
rem 仮想環境のアクティベート
C:\Users\hoge\AppData\Local\Programs\Python\.venv\Scripts\activate
- sample.bat
echo processing begins.
set option=-w
call activate.bat
cd ..\sample
if "%1"=="" (
echo No options.
python sample.py
goto end
)
if %1==%option% (
echo Select options.
python sample.py -w
goto end
) else (
echo [error]Problem with setting options.
)
:end
echo processing completed.
テキストの装飾
ANSIエスケープシーケンスを使用することで、ターミナルのテキストに装飾を施すことができます。
しかし、Windowsでは、コマンドプロンプトやパワーシェルがデフォルトでANSIエスケープシーケンスに対応していないため、装飾を行うにはWindows環境でこれらのシーケンスを有効にする必要があります。
以下はモジュール化する例です。
import ctypes
import os
class ColorText:
colors = {
'reset': '\033[0m', #リセット
'black': '\033[30m',
'red': '\033[31m',
'green': '\033[32m',
'yellow': '\033[33m',
'bule': '\033[34m',
'magenta': '\033[35m',
'cyan': '\033[36m',
'white': '\033[37m',
'bold': '\033[1m', #大文字
'underline': '\033[4m', #アンダーライン
}
ansi_escape_sequences = False
@classmethod
def _enabled_virtual_terminal_processing(cls):
if os.name == 'nt' and not cls.ansi_escape_sequences:
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
MODE = ENABLE_PROCESSED_OUTPUT + ENABLE_WRAP_AT_EOL_OUTPUT + ENABLE_VIRTUAL_TERMINAL_PROCESSING
kernel32 = ctypes.windll.kernel32
handle = kernel32.GetStdHandle(-11)
kernel32.SetConsoleMode(handle, MODE)
cls.ansi_escape_sequences = True
@classmethod
def color_text(cls, text, color):
cls._enabled_virtual_terminal_processing()
color_code = cls.colors.get(color)
if color_code:
return f'{color_code}{text}{cls.colors['reset']}'
else:
return text
@classmethod
def print_colored(cls, text, color):
print(cls.color_text(text, color))
if __name__ == '__main__':
ColorText.print_colored('This is black text', 'black')
ColorText.print_colored('This is red text', 'red')
ColorText.print_colored('This is green text', 'green')
ColorText.print_colored('This is yellow text', 'yellow')
ColorText.print_colored('This is bule text', 'bule')
ColorText.print_colored('This is magenta text', 'magenta')
ColorText.print_colored('This is cyan text', 'cyan')
ColorText.print_colored('This is white text', 'white')
ColorText.print_colored('This is bold text', 'bold')
ColorText.print_colored('This is underlined text', 'underline')
おわりに
最近のWindows環境では、Power Automateを活用することで、プログラミング知識がなくてもルーティンワークなどの自動化が容易になっています。
しかし、より高度で細かな処理を実現するためには、プログラミングが必要です。