AutoZoomをPyInstallerで実行ファイル化

上記の記事で作成したAutoZoomを実行ファイル化し、Python環境がなくても実行可能にすることを考える。

PyInstaller

PyInstallerは、Pythonスクリプトを実行可能なバイナリファイル(Windowsでは*.exe)に変換するツールであり、これによってPythonがインストールされていない環境でも、アプリケーションの実行が可能となる。PyInstallerは依存関係を自動でパッケージングするため、アプリケーションの配布がより簡単になる。

インストール

インストールは、以下のように行う。

pip install pyinstaller

外部ファイルの扱い

Pythonスクリプト間の依存関係は、自動で判定してくれるため、指定する必要はないが、外部ファイルとなる画像やデータファイルは、--add-data="ソースパス:ターゲットパス"で指定する必要がある。
よって、画像ファイルimag.pngをスクリプト内で読み込んで使用している場合、

pyinstaller --onefile --add-data "imag.png:." main.py

とする。
これによって、読み込みファイルは実行ファイルと同一化する。
ただし、PyInstallerで作成された実行ファイルは、実行ファイルが物理的に存在しているディレクトリ内で実行はされないので、コードの中で読み込むファイルのパスについて以下のように変更する必要がある。

if getattr(sys, 'frozen', False): # executes as PyInstaller exec file
        base_path = sys._MEIPASS 
    else:                         # executes as Python script
        base_path = os.path.dirname(os.path.abspath(__file__))
imag = os.path.join(base_path, "imag.png")

スクリプト実行時には、imag.pngは同じディレクトリに存在しているとしている。

SSL証明書


同様に、Pythonが通信で使用するシステムのSSL証明書を実行ファイルは、デフォルトで実行ファイルには組み込まれない。よって、実行時や他環境で、HTTPSリクエストなどを行う際に証明書が見つからず、SSLErrorが発生することがある。
この証明書は、cacert.pemのファイル名でPythonのライブラリ下に置かれていることが多い。実行ファイルにするスクリプトの環境下で、

import certifi
print(certifi.where())

とし、これを画像ファイルと同様に、--add-data='/path/to/cacert.pem'で実行ファイルに組み込み、実行ファイルで起動した時に、chromedriver_autoinstaller.install()が、この証明ファイルを通してダウンロードできるように以下のコードをソースコードを入れる。

if getattr(sys, 'frozen', False):
    # Get the path to the bundled certificate file
    cert_path = os.path.join(os.path.dirname(__file__), 'certifi', 'cacert.pem')
    print(cert_path)
    # Set the ssl certificate path
    os.environ['SSL_CERT_FILE'] = cert_path
    os.environ['REQUESTS_CA_BUNDLE'] = cert_path

また、実行ファイル化するにあたり、コードの最初の部分の、必要なモジュールのインストールは必要ない。

PyZoom.pyとmain.py

以上の変更を入れたPyZoom.pyのコードは以下のようになる。

import subprocess
import importlib
import sys
import os

if getattr(sys, 'frozen', False):
    # Get the path to the bundled certificate file
    cert_path = os.path.join(os.path.dirname(__file__), 'certifi', 'cacert.pem')
    print(cert_path)
    # Set the ssl certificate path
    os.environ['SSL_CERT_FILE'] = cert_path
    os.environ['REQUESTS_CA_BUNDLE'] = cert_path
else:
    Required_Modules = {
        'selenium': 'selenium',
        'chromedriver_autoinstaller': 'chromedriver_autoinstaller',
        'pyscreeze': 'pyscreeze',
        'pyautogui': 'pyautogui',
        'cv2': 'opencv-python',
        'PIL': 'Pillow',
    }
    for module, pip_name in Required_Modules.items():
        try:
            importlib.import_module(module)
        except ImportError:
            print(f'Installing {pip_name}')
            subprocess.check_call([sys.executable, '-m', 'pip', 'install', pip_name])

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoAlertPresentException
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

import chromedriver_autoinstaller
import pyscreeze
from pyscreeze import ImageNotFoundException
pyscreeze.USE_IMAGE_NOT_FOUND_EXCEPTION = True
import time
import datetime
import pyautogui as pa
from sys import exit


chromedriver_autoinstaller.install()

pa.useImageNotFoundException()

def StartZOOM(MeetingID,url):

    chrome_options = Options()
    chrome_options.add_argument("--start-maximized")  # Open browser in maximized mode
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")  # Disable automation control detection
    chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")

    #driver = webdriver.Chrome()
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(url)
    wait = WebDriverWait(driver,60)
    time.sleep(3)
    try:
        WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH,'//button[@id="onetrust-accept-btn-handler"]'))).click()
        print("accepted cookies")
    except Exception as e:
        print('no cookie button')

    target = driver.find_element(By.ID,"join-confno")
    target.send_keys(MeetingID)
    btn = driver.find_element(By.ID,"btnSubmit").click()
    return driver

def FindLoc(pngfile):
    while True:
        try:
            p= pa.locateCenterOnScreen(pngfile,grayscale=False,confidence=0.7)
            if(p is not None):
                print(f'{pngfile} location:',p)
                break
        except pa.ImageNotFoundException:
            print(f"{pngfile} image not found, will exit")
            sc = pa.screenshot()
            name = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
            sc.save(name)
            exit()
    return p

def MeetingMinute(MeetingDuration):
    minutes =0
    second = 0
    if MeetingDuration[-1]=='h' :
        minutes=60*float(MeetingDuration[:-1])
    elif MeetingDuration[-1]=='m':
        minutes= float(MeetingDuration[:-1])
    return 60*minutes

def AutoZoom(MeetingID,MeetingDuration,url='http://zoom.us/join',Passcode=None):

    if getattr(sys, 'frozen', False):
        base_path = sys._MEIPASS
    else:
        base_path = os.path.dirname(os.path.abspath(__file__))

    driver=StartZOOM(MeetingID,url)
    time.sleep(3)
    zoom_us = os.path.join(base_path, "zoom_us.png")
    p_zu=FindLoc(zoom_us)
    pa.moveTo(p_zu.x/2,p_zu.y/2)
    pa.click(p_zu.x/2, p_zu.y/2,clicks=2, interval=1)
    time.sleep(3)

    if Passcode =='None': Passcode=None
    if Passcode is not None:
        time.sleep(5)
        zoom_pc = os.path.join(base_path, "zoom_pc.png")
        p_pc=FindLoc(zoom_pc)
        pa.moveTo(p_pc.x/2,p_pc.y/2)
        pa.write(Passcode)
        pa.press('enter')
    else:
        time.sleep(10)
        zoom_join = os.path.join(base_path, "zoom_join.png")
        p_join=FindLoc(zoom_join)
        pa.moveTo(p_join.x/2,p_join.y/2)
        pa.click(p_join.x/2, p_join.y/2,clicks=2, interval=1)

    time.sleep(10)
    # mic is muted
    pa.hotkey('command','shift','A')
    time.sleep(3)
    pa.hotkey('enter')

    time.sleep(MeetingMinute(MeetingDuration))

    #Leaving prompt
    pa.hotkey('command','q')
    time.sleep(3)
    pa.hotkey('enter')
    driver.close()

    return

読み込みファイルは、zoom_us.png, zoom_pc.png, zoom_join.pngの3画像ファイルと、SSL証明書でであるから、Zoomを立ち上げるmain_zoom.pyをMainZoomという名の実行ファイルにするには、以下のコマンドで行う。これにより、distディレクトリ内にMainZoomの実行ファイルができる。

% pyinstaller --onefile --add-data="zoom_join.png:." --add-data="zoom_pc.png:." --add-data="zoom_us.png:." --add-data="/Path/to/ssl_certfi/directory/certifi/cacert.pem:certifi" --name MainZoom main_zoom.py
% ls -l dist 
total 129032
-rwxr-xr-x  1 ayamaguc  staff  66058016 10 Oct 17:21 MainZoom

同様に、crontabに命令を書き込むmain.pyを、MainZoomに対応するように書き換える。

def DateToCron(MeetingDateTime):
    import datetime
    MeetingDT = datetime.datetime.strptime(MeetingDateTime, '%d/%m/%y %H:%M')
    sunday_as_zero = (MeetingDT.weekday() + 1) % 7
    return f'{MeetingDT.minute} {MeetingDT.hour} {MeetingDT.day} {MeetingDT.month} {sunday_as_zero} '


if __name__ == '__main__':

    import argparse
    import pyautogui as pa
    import os
    import sys
    import time
    import subprocess
    from pathlib import Path

    parser = argparse.ArgumentParser(description="Pass meeting details as arguments.")
    # Adding arguments
    parser.add_argument("--MeetingID", required=True, help="Meeting ID")
    parser.add_argument("--Passcode", default=None, help="Passcode for the meeting")
    parser.add_argument("--MeetingTime", required=True, help="Time of the meeting")
    parser.add_argument("--MeetingSchedule", required=True, help="Date and time of the meeting")
    args = parser.parse_args()
    strArgs=f"--MeetingID='{args.MeetingID}' --MeetingTime='{args.MeetingTime}' --Passcode='{args.Passcode}'"

    # Parse the arguments

    strCron=DateToCron(args.MeetingSchedule)
    
    # Modification for PyInstaller package
    compile=""
    if getattr(sys, 'frozen', False):
        base_path = sys._MEIPASS
        whereiam = os.getcwd()
        main_file=os.path.join(whereiam,'MainZoom')
    else:
        base_path = os.path.dirname(os.path.abspath(__file__))
        main_file=os.path.join(base_path,'main_zoom.py')
        compile='python'
    home_directory = Path.home()

    strCron=strCron+f'source {home_directory}/.zshrc; cd {whereiam}; {compile} {main_file}  {strArgs} >> /tmp/test.txt 2>&1'

    subprocess.run(["open", "-a", "Terminal"])
    pa.write('crontab -e')
    pa.press('enter')

    time.sleep(2)
    pa.press('i')
    pa.write('PATH=/usr/sbin:/usr/bin:/bin:/usr/local/bin \nSHELL=/bin/zsh \n')
    pa.write(strCron)
    pa.write('\n')

    pa.press('esc')            # Confirm save (if asked)
    pa.write(':wq')   # Exit nano
    pa.press('enter')

これを実行ファイル化する。

 % pyinstaller --onefile --name CronZoom main.py
 % cd dist
 % ls
CronZoom	MainZoom

MacOS 設定

MainZoom, python3, Terminal,zoomの以下のアクセスを許可しておく。

  • System Preference > Privacy & Security > Screen & System Audio Recording

  • System Preference > Privacy & Secutiry > Accessibility

さらに、
System Preference > Privacy & Security > Full Disk Accessで、cron, us.zoom, Terminalのアクセスをonにしておく。

Usage

./CronZoom --MeetingID='XXX XXXX XXXX' --Passcode='XXXXXx' --MeetingTime='30m' --MeetingSchedule='DD/MM/YY HH:mm'


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