MacでSelenum+PyautoGUIを使ってZoomに自動出席

# 自動でZoom出席する
半年ほど前にSeleniumでZoomに自動で出席できないかトライしてみた([オンライン授業にPython+Seleniumで自動出席したい！](https://qiita.com/LemniscaterN/items/80b2f8ca99c0693d42ff))のですが、Seleniumのみではシステムダイアログをクリック出来ず、Zoom会議に参加することはできませんでした。

その後、@hima_zin331さんの「[PythonでZoomミーティングに自動入室しよう](https://qiita.com/hima_zin331/items/97fc5c9093057bb06572)」という記事を拝見しました。
この記事ではWindowsにてSelenium+Pywinautoを用いてZoomにログインし、指定時間画面録画後に自動でZoomを退出しています。

当時の私はSeleniumで遊ぶことが目的だったのでSeleniumのみで出席することを考えていたのですが、本記事ではSelenium以外のライブラリを利用して、MacでもZoomに自動出席ができないかを検討します。
なお、**本プログラムはあくまで知的好奇心を満たす為のものです。本プログラムを利用して発生したあらゆる損失や不利益について当方は一切の責任を負いません**。

## 環境
macOS Big Sur 11.2.2
Python 3.8.6
PyautoGUI 0.9.52
Google Chrome 88.0.4324.192（Official Build） （x86_64）

## PyautoGUIについて
今回改めて自動出席プログラムに挑戦しようと考えた最大の理由が**PyautoGUI**の存在です。
PyAutoGUIは、Python のモジュールの一つで、マウスやキーボード操作を自動化することが出来ます。イメージマッチングが簡単に利用でき、クロスプラットフォームなのでMac、Windows、Linuxで利用出来ます。

>参考

>* [pyautoguiのあれこれ](https://qiita.com/hideto1198/items/a2e822892a27af03fe5a)

>* [PyAutoGuiで繰り返し作業をPythonにやらせよう](https://qiita.com/hirohiro77/items/78e26a59c2e45a0fe4e3)

## 事前準備
* Zoomをインストール、ログインの後に以下の様に設定を変更する。
    * [x] ミーティング接続時に、自動的にコンピュータオーディオに接続
    * [x] ミーティングの参加時にマイクをミュートに設定
* command+Shift+5を押して、Macの画面撮影録画バーを表示する。「画面全体を収録」か「選択部分を収録」をクリックし、録画はせずに閉じる。（macOS Mojave以降の機能です。）録音には「なし」か「内臓マイク」を選択できます。
Zoomの音声を録音したい場合はBackground Musicなどを利用します。ただし、OSbigSurは非対応の為、スナップショットバージョンを利用します。
詳細は[公式ページ](https://github.com/kyleneideck/BackgroundMusic)を参照して下さい。

```
brew tap homebrew/cask-versions
brew install --cask background-music-pre
```

* ターミナルから```crontab -e```を入力して、Zoomに入室したい時間にautozoom.pyを起動するようにcronを設定します。
次の例では3月4日の21時に実行し、ログを書き出します。
ただし、/usr/sbinにPATHを通さないとPyautoGUIがエラーを吐いて実行できないので注意が必要です。  
cronでPyautoGUIを使うには何かと工夫が必要だったので、[MacでのPyAutoGUIの注意点](https://qiita.com/LemniscaterN/items/103019bbfeab36c9fab4)にまとめておきます。
pythonのフルパスは```which python```で取得出来ます。
cronが利用できない場合はtime.sleep()で軌道までの時間を無理矢理時間を潰すのもアリかもしれないです。

```
PATH=/usr/sbin:/usr/bin:/bin
0 21 3 4 * cd autozoom.pyのフルパス && pythonのフルパス autozoom.pyのフルパス ログ書き出したいファイルのフルパス 2>&1
```
* システム環境設定から「セキュリティとプライバシー」タブをクリックして「プライバシー」を開いて下記の項目を設定し、ターミナルを再起動する。
    * アクセシビリティに「AEServer」（OSのバージョンによっては名称が異なるかも）、「/usr/sbin/cron」を追加
    * フルディスクアクセスに「ターミナル」、「/usr/sbin/cron」を追加
    * 画面収録に「ターミナル」、「/usr/sbin/cron」を追加
* 以下のような写真を撮影する。（cmd+shift+4で任意範囲のスクリーンショットが撮影できます。）ファイル名をopenzoom.pngに変更し、後述のautozoom.pyと同じフォルダに入れておく。
![img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661162/98759e0b-0581-e7d4-251c-fd17f1ff124d.png)

* 必要なライブラリのインストール（ターミナルに入力する）

```
#ブラウザ操作関連
pip3 install selenium
pip3 install chromedriver-binary

#PyautoGUI関連
#OSによってインストール方法が異なりますが、Macの場合は以下の様にインストール出来ます。
pip3 install pyobjc-core
pip3 install opencv_python
pip3 install pyobjc
pip3 install pyscreeze
pip3 install pyautogui
```

## コード
```python:autozoom.py

import datetime
import os
import pyautogui
import chromedriver_binary
from  selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--disable-dev-shm-usage')
import time
import pyscreeze
from pyscreeze import ImageNotFoundException
pyscreeze.USE_IMAGE_NOT_FOUND_EXCEPTION = True

browser = webdriver.Chrome()
browser.implicitly_wait(3)
browser.get("入室するZoomのURL")
time.sleep(5)

nowTime = 0
maxTime = 15
p=None
while True:
    time.sleep(1)
    nowTime+=1
    print(nowTime)
    if(nowTime>maxTime):
            print(nowTime,maxTime)
            break
    try:
        p = pyautogui.locateCenterOnScreen('openzoom.png',grayscale=False,confidence=0.7)
        pyautogui.moveTo(p.x/2,p.y/2)
        if(p is not None):
            sc = pyautogui.screenshot()
            break
    except ImageNotFoundException:
        print("Image not found")
   
if(p is None):
    print("None and exit")
    sc = pyautogui.screenshot()
    name = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
    sc.save(name)
    exit()
else:
    pyautogui.click(p.x/2, p.y/2)

pyautogui.hotkey('command','shift','5')
time.sleep(3)
pyautogui.hotkey('enter')

time.sleep(600)

pyautogui.hotkey('command','ctrl','esc')
time.sleep(3)
pyautogui.hotkey('enter')

pyautogui.hotkey('command','q') 
time.sleep(3)
pyautogui.hotkey('enter')
```

### コードの説明詳細

#### 各種ライブラリ類のインポート
``` autozoom.py
import datetime
import os
import pyautogui
import chromedriver_binary
from  selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--disable-dev-shm-usage')
import time
import pyscreeze
from pyscreeze import ImageNotFoundException
pyscreeze.USE_IMAGE_NOT_FOUND_EXCEPTION = True
```

seleniumのオプションで--disable-dev-shm-usageを指定するとメモリ不足のクラッシュを防げるらしいので利用します。
また、pyscreezeのImageNotFoundExceptionを利用することで、target.pngが見つからなかったときにImageNotFoundExceptionが発生するようにしています。

#### ブラウザ起動、「zoom.usを開く」をクリックしてZoomに入室

``` autozoom.py
browser = webdriver.Chrome()
browser.implicitly_wait(3)
browser.get("入室するZoomのURL")
time.sleep(5)

nowTime = 0
maxTime = 15
p=None
while True:
    time.sleep(1)
    nowTime+=1
    print(nowTime)
    if(nowTime>maxTime):
            print(nowTime,maxTime)
            break
    try:
        p = pyautogui.locateCenterOnScreen('openzoom.png',grayscale=False,confidence=0.7)
        if(p is not None):
            break
    except ImageNotFoundException:
        print("Image not found")
   
if(p is None):
    print("None and exit")
    sc = pyautogui.screenshot()
    name = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
    sc.save(name)
    exit()
else:
    pyautogui.moveTo(p.x/2,p.y/2)
    pyautogui.click(p.x/2, p.y/2)
```

ブラウザーを起動して、ZoomURLを開きます。1秒ごとに画面内にtarget.pngがあるかどうかを判定し、発見した場合はwhileをbreak、lauchボタンにカーソルを動かしてクリックして、Zoomに入室します。
入室後は設定通りにオーディオに自動接続し、ミュート状態になります。
PyautoGUIには一部Macで座標系が2倍になる不具合があり、p.x/2、p.y/2としていますが、座標系が正しい場合は/2は不要です。
ちなみに、カーソルを動かさずとも(pyautogui.moveTo(p.x/2,p.y/2))クリックできるが、アクセシビリティでターミナル、cron、AEServerなどが許可されていないとカーソルが動かないことを利用して、正しく動作しているか確認します。
maxTime秒(=15秒)以内に発見できなかった場合はスクリーンショットを撮影して、プログラムを終了します。撮影されたスクリーンショットがデフォルトのデスクトップの画像の場合は、画面収録にcronやターミナルが追加されていないので環境設定を確認してください。

#### 画面録画、待機、退室処理

```
pyautogui.hotkey('command','shift','5')
time.sleep(3)
pyautogui.hotkey('enter')

time.sleep(600)

pyautogui.hotkey('command','ctrl','esc')
time.sleep(3)
pyautogui.hotkey('enter')

pyautogui.hotkey('command','q') 
time.sleep(3)
pyautogui.hotkey('enter')
```

Zoom入室後はMacのデフォルトの画面録画開始のショートカット(command+shift+5)で画面録画のツールバーを呼び出し、enterキーで画面録画を開始します。(Mac以外のOSや、Mojave以前のOSを利用する場合は画面録画ツールにショートカット設定すれば同様に利用できると思います。)
time.sleep()で録画する間待機（サンプルでは600[秒]=10[分])し、画面録画終了のショートカット（command+ctrl+esc）で録画を終了します。
最後にcommand+qで退室ボタンを表示させ、enteキーで退室します。Zoomの設定で退室の確認通知は設定でoffにすることもできます。

## 終わりに
PyautoGUIのイメージマッチング機能が便利すぎて感動しました。
locateCenterOnScreenの1行でイメージマッチングして座標を返すなんて便利すぎないかい、PyautoGUIくん？笑
そして、実は先日までOSがHighSierraでした。画面収録が機能がなかったので、いっちょBigSurアプデするかーにと思った結果、なんだかんだでOSをクリーンインストールしたのですが、セキュリティとプライバシーの機能変更にかなり戸惑いました。HighSierraにはフルディスクアクセスとか無かったんですよね。
思ったように動くようになってよかったです。