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'