Code

pythonでRPAやってみた

code-icon

category:code:python_RPA

pythonロボで自動メール送信するよ!!!

※本記事は「RPA Community Advent Calendar 2021」24日目の記事として書かせていただいております。
最近pythonの勉強を初めて、少しずつ業務自動化に活用をすすめているものです。
uipathでRPAやりたいが、職場ではライセンスの問題があるな。。。というわけで無償で利用出来るPythonで挑んでいたら 結構いろんなことが出来て楽しくなってきているところです。 最近作ったものの中で、実用性に手ごたえのあったものを紹介させていただきます。


プログラム概要

主な使用ライブラリ:smtplib,pyautogui,pyinstaller他
やりたかったこと:特定の条件を満たすと自動的にメールを送る仕組みを実装したい。

gmail送信をPythonで実装出来たので、これを利用した業務自動化が出来ないかと模索してみたものです。 職場のメッセンジャーアプリ受信の検知や、ロボのエラーログ検出などでメール送信させる仕組みを作ってみました。

まずはgmail送信を実装

googleのアプリパスワードを取得してconfig.csvを作成。

config.csv
key value
ID googleアカウント名
pass アプリパスワード
send_to 送信先アドレス

pythonでのGmail送信コード

    def gmail_send():
        import pandas as pd
        import smtplib
        from email.mime.multipart import MIMEMultipart
        from email.mime.text import MIMEText
        from email.mime.application import MIMEApplication
        import os
        data_file = pd.read_csv("config.csv")
        arg_df = data_file[["key", "value"]].values
        id, password, send_to = (arg_df[0][1], arg_df[1][1], arg_df[2][1])
        g_mail_address = f"{id}@gmail.com"
        g_mail_password = password
        stmp_server = "smtp.gmail.com"
        stmp_port = "587"
        subject = "件名"
        body = "メール本文"
        msg = MIMEMultipart()
        msg["Subject"] = subject
        msg["To"] = send_to
        msg["From"] = g_mail_address
        msg.attach(MIMEText(body, "html"))
        #添付ファイル(テキストに変換して添付されるらしい)
        filepath = "fileパス/hoge.png"
        filename = os.path.basename(filepath)
        with open(filepath, "rb") as f:
            mb = MIMEApplication(f.read())
        mb.add_header("Content-Disposition", "attachment", filename=filename)
        msg.attach(mb)
        s = smtplib.SMTP(stmp_server, stmp_port)
        s.starttls()
        s.login(g_mail_address,g_mail_password)
        s.sendmail(g_mail_address, send_to, msg.as_string())
        s.quit()

pythonファイルの直下に置いたconfig.csvからアカウント情報を呼び出してメール送信。


活用1:常時画面を見張って、特定の画像を検出したらメール送信

メール送信が出来たので、これで何が出来たら嬉しいかな~といろいろ考えてみた。
常時起動しておいて、特定の画像を検出したら知らせるパトロールアプリを作ってみる。
今回は社内用PCでメッセンジャーアプリの受信を検知したら通知を送信。

message_demo

↑こういうメッセージを、離席時や休みの日に送られてもかなり放置になってしまったりするので、

message_kenti

↑ロボに見張らせて、こんな感じでスクショをつけて通知してもらおう!


target.png(メッセンジャーのダイアログの左上部分切り抜き)

target

whileループで、常にこの画像が出ていないかを検証し続ける。
pyautoguiのlocateCenterOnScreenで画面上に一致画像があるか検証してくれる。

     import pyautogui as pg
     while True:
        print("stand by")
        if pg.locateCenterOnScreen("target.png"):
            print("検出したよ~~~!!!")
            break

これだけで見張りプログラムは完成。後は「検出したよ~」の部分を上記のメール送信に変更すればOK

     import pyautogui as pg
     from time import sleep
     while True:
        print("stand by")
        if pg.locateCenterOnScreen("target.png"):
            pg.screenshot("fileパス/hoge.png")#スクリーンショット作成
            sleep(1)
            gmail_send()#上述の送信プログラム。
            break

ただし、このままでは検出時無限にGmailを送り続けるウイルスと化してしまうため修正
今回はtarget.pngの位置へ移動して右へ190pxの位置にある最小化ボタンを押すことで終了。
このあたりはさせたい動作によって細かい微調整が必要。

 #ipmsg_patroller.py全文
    import pyautogui as pg
    from time import sleep
    import pandas as pd
    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    from email.mime.application import MIMEApplication
    import os

    def ipmsg_receive_loop():
        while True:
            print("stand by")
            if pg.locateCenterOnScreen("target.png"):
                pg.screenshot("/hoge.png")#スクリーンショット作成
                sleep(1)
                pg.moveTo(pg.locateCenterOnScreen("target.png"))
                pg.move(190, 0)
                pg.click()#ダイアログの最小化ボタンを押す。
                gmail_send()#送信。
                break

    def gmail_send():
        data_file = pd.read_csv("config.csv")
        arg_df = data_file[["key", "value"]].values
        id, password, send_to = (arg_df[0][1], arg_df[1][1], arg_df[2][1])
        g_mail_address = f"{id}@gmail.com"
        g_mail_password = password
        stmp_server = "smtp.gmail.com"
        stmp_port = "587"
        subject = "ipmsg検知通知です。"
        body = "<p>IPメッセンジャー受信を検知しました。スクリーンショットを添付します。</p>"
        msg = MIMEMultipart()
        msg["Subject"] = subject
        msg["To"] = send_to
        msg["From"] = g_mail_address
        msg.attach(MIMEText(body, "html"))
        #添付ファイル
        filepath = "/hoge.png"
        filename = os.path.basename(filepath)
        with open(filepath, "rb") as f:
            mb = MIMEApplication(f.read())
        mb.add_header("Content-Disposition", "attachment", filename=filename)
        msg.attach(mb)
        s = smtplib.SMTP(stmp_server, stmp_port)
        s.starttls()
        s.login(g_mail_address,g_mail_password)
        s.sendmail(g_mail_address, send_to, msg.as_string())
        s.quit()
        print("reported")

    if __name__ == '__main__':
        print("見張りを開始します。")
        while True:
            ipmsg_receive_loop()

pyinstallerでexe化して必要時に起動しています。(後述)

patoroller

起動するとずっとこの状態で回り続ける。targetの画像が小さいほどループは早く回る。大きい画像だと1ループに数秒かかるため極力小さくしたいところ。


活用2:毎日特定の時刻に起動してエラーログがあればメール送信

毎朝専用の端末で起動させているPythonロボのエラーログフォルダを定時監視して、フォルダが空で無ければ知らせる。

     import glob
   files = glob.glob("errorlogs/*")

単純にこれだけで直下のフォルダのファイル名を取得できるので、後は空かどうかで分岐。

 #errorlog_checker.py全文
    import glob
    import os
    import pandas as pd
    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText

    def filecheck():
        files = glob.glob("errorlogs/*")
        if len(files) == 0:
            #空だったら終了
            pass
        else:
            #ファイル名を改行区切りで取得
            errors = ""
            for file in files:
                errors = errors + os.path.basename(file) + "<br>"
            gmail_send(errors)#メール送信

    def gmail_send(body_text):
        data_file = pd.read_csv("config.csv")
        arg_df = data_file[["key", "value"]].values
        id, password, send_to = (arg_df[0][1], arg_df[1][1], arg_df[2][1])
        g_mail_address = f"{id}@gmail.com"
        g_mail_password = password
        stmp_server = "smtp.gmail.com"
        stmp_port = "587"
        subject = "ロボがやらかした通知です"
        body = f"<p>下記のエラーを検出しました。<br>{body_text}</p>"
        msg = MIMEMultipart()
        msg["Subject"] = subject
        msg["To"] = send_to
        msg["From"] = g_mail_address
        msg.attach(MIMEText(body, "html"))
        s = smtplib.SMTP(stmp_server, stmp_port)
        s.starttls()
        s.login(g_mail_address, g_mail_password)
        s.sendmail(g_mail_address, send_to, msg.as_string())
        s.quit()
        print("reported")

    if __name__ == '__main__':
        filecheck()

かなり使いまわしが効きそう。あとはタスクスケジューラ様へおまかせ。

errorcheck

活用3:pingを飛ばして死活確認。落ちていたらメール通知

いや待てよ、そもそもロボの端末落ちてたらどうすんのよ。というわけでpingロボ作成。  
(pingって?→wikipedia)

subprocessモジュールでサクッと行けそう。

    import subprocess
    ping = subprocess.run(["ping", "-w", "1", "-n", "1", ipアドレス ], stdout=subprocess.PIPE)
    print(ping.returncode) #ping通れば「0」を。ダメなら「1」を返す

これで死活確認出来るので、後はメールと組み合わせ。確認しながら設置したいのでテストモードも作ってみた。

config.csv
key value
ID googleアカウント名
pass アプリパスワード
send_to 送信先アドレス
testmode 1(1ならテストモード)
target_host 目当てのIPアドレス
  #ping_robo.py全文
    import subprocess
    import pandas as pd
    import PySimpleGUI as sg
    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText

    # macでは-nを-cに変更
    def ping_windows(host, testmode):
        ping = subprocess.run(["ping", "-w", "1", "-n", "1", host], stdout=subprocess.PIPE)
        if ping.returncode == 0:
            if testmode == "1":
                #ダイアログ表示
                sg.popup(f"{host}OK!!", title="test")
            else:
                pass
        else:
            if testmode == "1":
                #ダイアログ表示
                sg.popup(f"{host}NO!!", title="test")
            else:
                #メール送信
                data_file = pd.read_csv("config.csv")
                arg_df = data_file[["key", "value"]].values
                id, password, send_to = (arg_df[0][1], arg_df[1][1], arg_df[2][1])
                g_mail_address = f"{id}@gmail.com"
                g_mail_password = password
                stmp_server = "smtp.gmail.com"
                stmp_port = "587"
                subject = "pingエラー通知"
                body = f"<p>{host}へのpingは通りませんでした。</p>"
                msg = MIMEMultipart()
                msg["Subject"] = subject
                msg["To"] = send_to
                msg["From"] = g_mail_address
                msg.attach(MIMEText(body, "html"))
                s = smtplib.SMTP(stmp_server, stmp_port)
                s.starttls()
                s.login(g_mail_address, g_mail_password)
                s.sendmail(g_mail_address, send_to, msg.as_string())
                s.quit()
                print("reported")

    if __name__ == '__main__':
        data_file = pd.read_csv("config.csv")
        arg_df = data_file[["key", "value"]].values
        test_flag, target_host = (arg_df[3][1], arg_df[4][1])
        ping_windows(target_host, test_flag)

pingが通らなかった場合のみ送信する仕様。

テストモードで起動すると、pysimpleguiのダイアログでのみ通知。

dialog

これも地味に使い勝手が良い。



パッケージ化してタスクスケジューラで自動起動

pyinstallerというライブラリを利用することで、exe化してpythonの入っていない端末でも利用が可能。

特に今回は複数の端末に組み込みたかったし、出来が良ければ配布したいという思いもありパッケージ化してみました。

 $ pyinstaller ○○.py --onefile

これで上のプログラムはすべて問題なくexe化できた。
目的の.pyファイルのあるディレクトリに移動してから実行しないとエラーになる。

タスクスケジューラで起動するときひとつ躓きポイントが、、、

タスクスケジューラに直接exeファイルを指定してやると関連ファイル(config.csv等)を読み込んでもらえなくてエラーとなるのである。 ダブルクリックで実行すると上手くいくのに何故!?とだいぶハマった。

調べたところ、タスクスケジューラでのプログラム起動では相対パス指定を受け付けてもらえないようで、、

 @echo off
 ping_robo.exe

同一ディレクトリにこんな感じのbatファイルを作成してこちらを起動してやるとちゃんと直下のファイルを読み込んでもらえた。

タスクスケジューラへの登録はこんな感じ。

tasks

開始オプションにディレクトリ名を指定してやらないと動かなかった。(環境による模様)


まとめ

pythonでRPA的なものを作る場合はまずpyautoguiが思いつくが、他のライブラリを有効に組み合わせることで ロボの堅牢性を上げる事が出来ると感じた。無償でこれだけ色々実装できるのはやはり魅力的。
特に常時起動して見張らせるという仕組みは色々応用して見たいと思います。