ルーチンワークはPythonにやらせよう　Seleniumで勤怠処理を自動化する

# はじめに
本記事は勤怠管理システムにおける日々の勤怠入力を自動化し、ルーチンワークを自動化するための方法について記載しています。

勤怠入力などのルーチンワークを自動化することで、日々の勤怠入力にかかる時間を削減します。
また入力漏れや入力不備を防止し、入力不備によって発生する対応がなくなることで、余計なストレスを低減できます。

## 概要
本記事で紹介している勤怠処理の自動化を行うための環境は以下の通りです。

![勤怠処理自動化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/966f337e-18c2-aebb-9148-9dca1ab3c34d.png)

OSはmacOSですが、Windowsの場合はタスクスケジューラを使用することで、同様の仕組みが構築できます。


| 項目 | バージョン等 | 
|:-----------|:------------|
| OS         |macOS        |
| Python     |3.8          |
|Selenium    |4.4          |
|ブラウザ      |Chrome       |

勤怠処理の自動化は、以下の仕組みによって実現しています。
在宅勤務と出社では勤怠処理が異なるため、基本的にフルリモート前提で在宅勤務の場合に処理を行うよう設定しています。

1. cronによってシェルスクリプトを起動
1. シェルスクリプトで在宅勤務の判定を行う
1. 在宅勤務の場合、Pythonのプログラムを起動
1. Seleniumで勤怠入力

## 実装
Pythonのプログラムは、仮想環境を作成して実行しています。

### 仮想環境の作成及びSeleniumの導入
Pythonの仮想環境は**venv**を使用して作成します。

仮想環境を作成するためには、以下のコマンドを実行します。
`＜Dir＞`には任意のディレクトリ名を指定します。

`$ python3 -m venv ＜Dir＞`
`$ source ＜Dir＞/bin/activate`

仮想環境構築後、**selenium**をインストールします。
仮想環境が有効な場合はプロンプトの表示が変わります。
例えば、`kintai`ディレクトリが有効の場合は以下のような出力になります。

`(kintai) $ pip3 install selenium`

:::note warn
pipのバージョンが古いことによって、以下のようなエラーが出力される場合は、pipのアップグレードを行います。
:::

`$ pip3 install --upgrade pip`

- エラーの出力例
```console
Failed to build cryptography
ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly
WARNING: You are using pip version 19.2.3, however version 22.2.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
```

### ドライバのダウンロード
Seleniumをインストールしただけは使用することができません。
ブラウザに対応しているドライバをインストールする必要があります。

Chromeを起動し、`chrome://settings/help`にアクセスしてバージョンを確認します。

![スクリーンショット 2022-09-01 19.36.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/93faaa03-6582-8f57-7e38-8c70610b4417.png)


バージョンを確認後、[Downloads](https://chromedriver.chromium.org/downloads)から同じバージョンのChromeDriverのダウンロードを行い、任意のディレクトリに配備します。

::: note warn
ブラウザのバージョンをあげた場合は、互換性を保つためにドライバのバージョンもあげる必要があります。
:::

以下はブラウザのバージョンと、ドライバのバージョンの違いによって、発生したエラーの出力例です。


- エラーの出力例
```console
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 103
Current browser version is 105.0.5195.52 with binary path /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
```

### Selenium
以下はSeleniumを用いて、Webサイトにログインする処理のスニペットです。
URLや、ブラウザを操作するために取得する要素は利用する勤怠システムによって異なるため、一例です。

```python
#! /usr/bin/env python3
import datetime
import time
import traceback

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.select import Select

USERID= os.environ['USERID']
PASS = os.environ['PASS']

driver = '/opt/chrome/chromedriver'
options = webdriver.chrome.options.Options()
browser = webdriver.Chrome(executable_path=driver, chrome_options=options)

base_url = 'https://＜勤怠システムのURL＞/'

# USERIDとPASSを入力して、ログインボタンを押す場合の例
try:
    browser.get(base_url + '＜ログインページを指定＞')

    e = browser.find_element(By.ID, '＜IDの要素を指定＞')
    e.clear()
    e.send_keys(USERID)

    e = browser.find_element(By.ID, '＜パスワードの要素を指定＞')
    e.clear()
    e.send_keys(PASS)

    e.find_element(By.XPATH, '＜XPATHを指定＞').click()
    time.sleep(3)

except Exception as e:
    traceback.print_exc()
```

:::note
勤怠管理システムにログインするためのID及びパスワードのクレデンシャル情報は、ハードコーディングを避けるために、環境変数として取り込んでいます。環境変数の設定については後述します。
:::

#### Seleniumのバージョン
2021年の10月13日に**Selenium 4**が発表されています。
本記事執筆時点では4.4.xが最新です。 

- [Announcing Selenium 4](https://www.selenium.dev/blog/2021/announcing-selenium-4/)
- [Selenium 4.4.0 Released!](https://www.selenium.dev/blog/2022/selenium-4-4-0-released/)

Selenium 4に関する主な変更点は[Upgrade to Selenium 4](https://www.selenium.dev/documentation/webdriver/getting_started/upgrade_to_selenium_4/)から確認できます。

例えば、ID属性を指定して要素を取得する場合、従来は`find_element_by_id`メソッドを使用していました。
Selenium 4では、以下の書き方に変更されています。

```python
# Selenium 3
find_element_by_id('＜要素名＞')
# Selenium 4
find_element(By.ID, '＜要素名＞')
```

:::note warn
古い書籍やブログなどでは、従来のSelenium 3などの書き方に関する情報が多いため、参考にする場合は注意が必要です。
:::

#### Seleniumの使い方
Seleniumでブラウザを操作する際の基本的な使い方について、以下に記載しています。
ブラウザ操作の詳細は、Documentationの[Browser](https://www.selenium.dev/documentation/webdriver/browser/)から確認できます。

本記事では以下のブラウザ操作の組み合わせによって、自動化を実現しています。

- ブラウザの起動

```
base_url = "＜勤怠システムのURL＞"
browser.get(base_url) 
```

- ブラウザの起動（ヘッドレスモード[^1]）

```python
options = webdriver.chrome.options.Options()
options.add_argument('--headless')

browser = webdriver.Chrome(executable_path="/opt/chrome/chromedriver", chrome_options=options)
```

[^1]: ※ブラウザを非表示にして起動する

- 要素を取得して入力

```python
e = browser.find_element(By.ID, 'USER_ID')
e.send_keys(USERID)
```

- ボタンをクリック

```python
browser.find_element(By.XPATH, '＜XPATHを指定＞').click()
```

- クエリーパラメータの指定

```python
import datetime

today_tmp = datetime.date.today()
YEAR = today_tmp.strftime('%Y') 
MONTH = today_tmp.strftime('%m') 
DAY = today_tmp.strftime('%d') 

# 今日の日付にアクセス
base_url = 'https://＜勤怠システムのURL＞'
e = browser.get(base_url + f'<特定のページ>.php?D_YEAR={YEAR}&D_MONTH={MONTH}&D_DAY={DAY}')
```

- セレクトボックスを指定

```python
e = browser.find_element(By.XPATH, '＜XPATHを指定＞')
select_object = Select(e)
select_object.select_by_index(3)
```

::: note
セレクトボックスから0から始まるため、例として4個目を取得したい場合、indexは3を選択します。
:::

- ブラウザの終了

```python
browser.quit()
```

## 自動化
OSの機能を活用して自動化の仕組みを構築します。

### シェルスクリプト
cronによって勤務終了後の時間になったら、シェルスクリプトが起動します。

勤怠管理システムにログインするためのID及びパスワードのクレデンシャル情報は、環境変数として`export`します。

勤怠システムのパスワードは、`security find-generic-password`コマンド（`-a`はアカウント名、`-s`はキーチェーンの項目名を指定）で、macのキーチェーンに登録したパスワードを取得しています。

在宅勤務の判定を行うために、`networksetup`コマンドを実行して接続しているWi-FiのSSIDを取得し、SSIDが自宅の場合はPythonのプログラムを実行します。
`networksetup`コマンドはシステム環境設定のネットワークを設定するためのコマンドです。

```bash
#!/bin/bash

export USERID=********
export PASS=$(security find-generic-password -a "${USERID}" -s kintai -w)

network=$(networksetup -getairportnetwork en0 | awk '{print $4}')
home_ssid=********

DIR=$(cd $(dirname $0) && pwd)

if [ "${network}" = "${home_ssid}" ] ; then
        echo "勤怠処理開始"
        source "${DIR}"/bin/activate
        python3 "${DIR}"/kintai.py
fi
```

スクリプト終了後に`say`コマンドで音声を付けてみるのも良いと思います。
`say`コマンドは色々なvoiceが設定できます。

`$ say -v Samantha -r 50 "thank you for your hard work"`

### キーチェーンの登録
キーチェーンは事前に以下の手順で登録します。

キーチェーンアクセスを起動し、新規に項目を作成します。

![スクリーンショット 2022-08-25 20.15.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/a9ddd3c5-a40b-8604-b0db-49fb4d0af55b.png)

勤怠システムで使用するIDと、パスワードを入力して追加を押します。

![スクリーンショット 2022-08-25 20.11.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/87c368ee-e875-7785-d741-fdea1d84e1de.png)

パスワードを入力して、「キーチェーンを変更」を押します。

![スクリーンショット 2022-08-25 20.11.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/984f60c9-5a91-a12d-b695-d5b8646bd04f.png)

作成した項目を選択し、ダブルクリックまたは右クリックで「情報を見る」を押します。

![スクリーンショット 2022-08-25 20.14.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/d8951cea-3d71-64ee-58cf-973c2117671c.png)

シェルスクリプトからアクセスを許可するために、アクセス制御の「この項目の使用をすべてのアプリケーションに許可」にチェックを入れて、「変更内容を保存」を押します。

![スクリーンショット 2022-08-25 20.13.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/0231f84f-e0e5-64ec-5b48-0bab15e0556d.png)

:::note
キーチェーンアクセスからパスワードを取得し、環境変数としてインポートすることで、パスワードをハードコーティングしないようにしています。パスワードを変更した際はキーチェーンアクセスの値を変更すれば良いので、コードを修正することもありません。
:::

### cron
cronの自動起動の設定と、アクセス許可の設定を行います。

#### crontab
基本は平日の月曜から金曜までしか仕事しないため、19時に勤怠を入力する場合は`crontab`で以下のように設定します。
cron実行時のPATHは限られているため、最低限必要なPATHを追加しています。

```bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
00 19 * * 1-5 /Users/workman/Documents/kintai/shell.sh
```

#### cronに対するアクセス許可の設定
またcronによって起動されたシェルスクリプトに対する、アクセス許可の設定が必要です。
アクセス許可がない場合は以下のようなエラーが出力されます。

```console
/bin/bash: /Users/workman/Documents/kintai/hell.sh: Operation not permitted
```

「システム環境設定」を起動し、「セキュリティとプライバシー」の「プライバシー」の中の「フルディスクアクセス」を選択して、「+」を押します。

![スクリーンショット 2022-08-30 19.49.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/b6ce5486-06a9-b7b2-fb88-6a1dfff6bc94.png)

cronを追加する場合は`command+shift+G`でフォルダの場所選択ダイアログを開き、`/usr/sbin/cron`を入力して移動を押します。

![スクリーンショット 2022-08-25 19.54.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/6faeb030-70d3-afc4-b7d5-97d7b430906b.png)

cronを選択して「開く」を押します。

![スクリーンショット 2022-08-25 19.54.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/23fd1966-970f-e72a-dd28-9e8dae17898f.png)

cronが選択されます。

![スクリーンショット 2022-08-25 20.00.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/5a23da5d-1695-f347-1342-945226a87382.png)

cronが実行される度に、コンソールで以下のように出力されます。

```console
You have mail in /var/mail/＜user name＞
```

上記出力より、cronの結果は以下のファイルを参照して確認できます。

`cat /var/mail/＜user name＞`

- cronの実行結果の例

```console
勤怠処理開始
/Users/workman/Documents/kintai/kintai.py:19: DeprecationWarning: executable_path has been deprecated, please pass in a Service object
  browser = webdriver.Chrome(executable_path=driver, chrome_options=options)
/Users/workman/Documents/kintai/kintai.py:19: DeprecationWarning: use options instead of chrome_options
  browser = webdriver.Chrome(executable_path=driver, chrome_options=options)
勤怠処理終了
```

::: note warn
Pythonの場合はtry/exceptを設定して、エラーハンドリングを行うようにすることで、プログラムが異常終了した際の例外をキャッチできます。
:::

# おわりに
ルーチンワークの自動化によって、生み出される時間の恩恵は計り知れません。
自動化しているので勤怠入力という行為を行うことは無くなりました。
また勤怠入力漏れなどによる、煩わしい通知も来なくなったので一石二鳥です。

運用上の留意点としては、cronが動く前に仕事を終えた場合はリカバリーの仕組みがないため、オンデマンドで実行できる仕組みも別で用意しておくと良いと思います。

シェルを直接実行したり、aliasでコマンドを設定するなどでも実行できますが、Macの場合はAutomatorと組み合わせて、アプリケーションにしてポチッと押すだけの仕組みにするのが楽だと思います。

Automatorについては以前書いた[Macでディスプレイの配置を簡単に変更するためのLifehack](https://qiita.com/Brutus/items/509e5e5163c2c11acf39#automator)を参照。

## 追記
本記事執筆時点で、Seleniumに関する以下の課題がありました。

- ドライバのバージョンアップ
- executable_pathと、chrome_optionsの警告

これらの対応方法については、[Seleniumで使用するドライバのバージョンアップを自動化する](https://qiita.com/Brutus/items/1998249c0a38c334be8e)に記載しています。

## 参考
- [venv](https://docs.python.org/ja/3/library/venv.html)
- [Selenium](https://www.selenium.dev/)
- [ChromeDriver](https://sites.google.com/chromium.org/driver/)