Pywin – Pythonowa automatyzacja API Windows
Windows i jego aplikacje – jakie są każdy widzi. Niby niezastąpione, niby w wielu przypadkach jedynie słuszne, a jednak częstokroć dające się we znaki użytkownikom.
Zjawisko, którego najbardziej nie lubię w pracy jest wielokrotne wykonywanie powtarzalnych czynności. Szczególnie frustrujące są zadania, które wykonuje się regularnie (codziennie?), wymagają powtórzenia n razy tej samej czynności ale…. każda kolejna czynność różni się drobiazgiem i wymaga minimum uwagi użytkownika.
Świetny przykład (z życia wzięty) opisywanego zjawiska stanowi przetwarzanie dziennych raportów sprzedażowych. Każdego dnia roboczego na skrzynkę pocztową wpływają raporty z 2 kanałów sprzedaży, które należy pobrać, zapisać w korespondującym katalogu zależnym od źródła raportu oraz odpowiednio opisać plik. Nic trudnego, za to bardzo irytujące.
Jako osoba wygodnicka dość szybko zacząłem się denerwować manualnym wertowaniem Outlooka i postanowiłem zautomatyzować działania budując odpowiedni skrypt. Wybór padł oczywiście na Pythonową bibliotekę Pywin.
Pywin
Python posiada olbrzymia liczbę bibliotek. Pośród nich znajduje się pywin32, który pozwala w prosty sposób odwoływać się do API Windowsa. Przy wykorzystaniu wspomnianej biblioteki zyskujemy ogrom możliwości jak m. in. odwołania bezpośrednio do komponentów i metod Win API, złożonych operacji na strukturze plików/katalogów, manipulacji Windowsowym GUI i wielu innych. Dobrym źródłem wiedzy na temat Pywin jest strona http://timgolden.me.uk/pywin32-docs/index.html
Budujemy skrypt
W moim przypadku potrzebuję odwołać się do Outlooka. Aby to osiągnąć trzeba będzie posłużyć się modelem COM, który pozwala swobodnie manipulować obiektami Windowsa – pominiemy tutaj definicje i technikalia ; -).
Budowę skryptu zaczniemy od zastanowienia się, co chcemy zrobić. Wiemy, że codziennie na mail’a przychodzą wygenerowane raporty. Ich rodzaje związane są z kanałem sprzedaży, którego dane są raportowane. W naszym przypadku łatwo rozpoznać czego dotyczy raport po tytule wiadomości, która opiera się o następujący schemat „DD-MM-YYYY RAPORT <kanał> sprzedaż”. Znając już wiadomość trzeba pobrać załącznik i zapisać go do odpowiedniego katalogu, a następnie nadać zapisanym plikom nazwę (wszystkie pliki nazywają się raport.xlsx), która powinna zawierać datę raportu.
Zaczynamy od zaimportowania potrzebnych bibliotek. Naturalnie przyda nam się pywin do obsługi maila, ale ponieważ zamierzamy analizować nazwy plików oraz badać daty posłużymy się bibliotekami służącymi do obsługi tego rodzaju danych.
import win32com.client as pywin
import re
from datetime import datetime
Następnie przygotujemy klasę, która wykona za nas całą pracę.
class Zbieracz:
def __init__(self,data_start,data_stop,mail_address,path_pro,path_com, option="Skrzynka odbiorcza"):
self._podziel_date(data_start,data_stop)
self.pro = path_pro
self.com = path_com
self.mail = mail_address
self.handler = pywin.Dispatch('Outlook.Application').GetNamespace("MAPI")
self.box = self.handler.Folders(mail_address).Folders(option)
self.messeges = self.box.Items
def _podziel_date(self,data_start,data_stop):
self.start = datetime.strptime(data_start, '%Y-%m-%d')
self.stop = datetime.strptime(data_stop, '%Y-%m-%d')
def retreive_reports(self):
for mail in self.messeges:
temp_received_var = str(mail.ReceivedTime)
temp_received_var = temp_received_var[:10]
temp_received_var_date = datetime.strptime(temp_received_var, '%Y-%m-%d')
if temp_received_var_date >= self.start and temp_received_var_date <= self.stop:
if re.search("^.*Raport sprzedaży.*", mail.Subject):
if re.search("^.*E-Commerce.*", mail.Subject):
for attachment in mail.Attachments:
print(temp_received_var, attachment.FileName)
ev = self.com + str(temp_received_var)[:10] + " " + attachment.FileName
attachment.SaveASFile(ev)
if re.search("^.*Professional.*", mail.Subject):
for attachment in mail.Attachments:
print(temp_received_var, attachment.FileName)
ev = self.pro + str(temp_received_var)[:10] + " " + attachment.FileName
attachment.SaveASFile(ev)
Zastanówmy się teraz co tutaj się wydarzyło. Tworzymy klasę, której w konstruktorze przekazujemy kilka parametrów. Pierwsze dwa z nich dotyczą dat granicznych, dla których chcemy pobrać raporty. Możemy wszakże chcieć wygenerować zrzut jedynie dla konkretnego miesiąca lub kwartału. Kolejny parametr to adres mailowy, z którego pobierane będą wiadomości. Tutaj warto wspomnieć, że aby całość zadziałała nie wystarczy sam adres e-mail. W Outlooku musi zostać utworzony profil skojarzony z daną skrzynką. Kolejne dwa parametry zawierają ścieżki do katalogów, w których chcemy zapisywać nasze raporty. W przykładzie są to 2 rodzaje, ale oczywiście może być ich dowolna liczba. Ostatni argument określa nazwę katalogu w obrębie konta, w którym skrypt ma szukać maili.
Wykonanie konstruktora, oprócz przypisania do atrybutów parametrów wykonuje kilka istotnych operacji, którym warto poświęcić kilka słów.
pywin.Dispatch(’Outlook.Application’).GetNamespace(„MAPI”) – cała magia zaczyna się od poinformowania Pythona, że potrzebujemy utworzyć interfejs do komunikacji z Outlookiem. Odpowiada za to metoda Dispatch, która jako parametr przyjmuje w tym przypadku nazwę aplikacji. Następnie, pobieramy przestrzenie nazw, które pełnią funkcję swoistej sesji.
self.box = self.handler.Folders(mail_address).Folders(option) – skoro już utworzyliśmy instancję, w ramach której możemy manipulować aplikacją warto by przejść do działania. Używając odwołań do struktury kont w obrębie aplikacji wskazujemy, które konto chcemy użyć (do tego potrzebny był nam parametr z adresem e-mail) oraz który katalog zawiera poszukiwane przez nas maile.
self.messeges = self.box.Items – ostatnim krokiem konfigurowania komunikacji pomiędzy skryptem, a obiektami działającymi pod dyktando COM jest wskazanie, że interesują nas elementy w uprzednio wybranej lokalizacji.
Dodatkowo używając funkcji strptime przekazane przez nas wartości graniczne dat konwertujemy z łańcuchów tekstowych na obiekty daty, co pozwoli na wygodne porównanie granic czasowych.
Metoda retreive_reports – to tutaj dzieje się cała „magia” ;-). Metoda .Items użyta w konstruktorze tworzy obiekt, który jest iterowalny. Dzięki temu możemy przejść element po elemencie przez wiadomości znajdujące się w skrzynce odbiorczej i wyłuskać te, które nas interesują. Pywin pozwala nam odwoływać się w prosty sposób do elementów każdej wiadomości. Atrybut .ReceivedTime zawiera pełny timestamp wpłynięcia maila. Nas interesuje data, która posiada format YYYY-MM-DD*, więc wycinamy sobie ten fragment i przerabiamy na obiekt date.
Idąc przez skrzynkę analizujemy każdą wiadomość, która znajdzie się w ramach czasowych i jednocześnie zawiera poszukiwaną przez nas frazę w tytule, za który odpowiada atrybut .Subject. I tutaj przydaje nam się biblioteka do regexp’ów, dzięki którym możemy w łatwy sposób sprawdzić czy temat zawiera słowa-klucze**. Ostatnim krokiem jest przejście przez załączniki (nasze raporty mają tylko jeden) i zapisanie go pod wcześniej zdefiniowanymi adresami katalogów z pożądaną przez nas nazwą.
Podsumowanie
Przedstawiony skrypt nie jest idealny czego jestem świadomy, ale miał za zadanie szybko uwolnić mnie od irytującej pracy manualnej, a przy okazji posłużył za obiekt szkoleniowy dla czytelników bloga. Przedstawiony skrypt jest maleńkim wycinkiem z możliwości, jakie otwiera Pywin przed użytkownikami Pythona. Kolejne wpisy poświęcę prezentacji jak przy pomocy m. in. tej biblioteki z Excela zrobić multi-kombajn na sterydach 😉
* tak wiem, że rozwiązanie jest paskudne, hardcoded i wrażliwe na zmiany, ale potrzebowałem szybko zakodować do obsługi danych, które się nie zmienią do końca świata 😉
** jasne, poszukiwane słowa mogą znaleźć się w mailach z załącznikami nie będącymi raportami, ale co stoi na przeszkodzie dodać dodatkowe warunki ;)?