Python
Selenium
スクレイピング
PyInstaller

TouchOnTimeのデータを自動印刷するソフトを作った

業務で勤怠管理ソフトのTouchOnTimeを使っており"1ヶ月分の日別データを手動で印刷する"というどう考えても21世紀を生きる人間がやるべきでない作業があったので自動化しました。
またPythonがインストールされていない環境でも利用できるように、exe化しました。

使うもの

  • Python3.6
  • Selenium
  • openpyxl
  • pywinauto
  • PyInstaller
  • GoogleChrome
  • Chromedriver

開発環境

  • Windows10(64bit)
  • Python3.6.4(32bit)

やりたいこと

日別の勤怠データ(勤務時間など)の取得

これに関しては標準機能のCSV出力で、全データを出力してExcelでピポットテーブルなどを使えばできるのですが、ついでに一緒に取得することにしました。
Selenium+openpyxlを使います。

日別の勤怠データ画面の印刷

印刷に関してはSeleniumのブラウザの処理から外れてしまうので、別途pywinautoというWindowsの自動化のライブラリを使用します。

各種ライブラリのインストール

基本的にはpipで大丈夫です。他に詳しく説明されている方が多数おられるので、詳しくはそちらを参照してください。
またSeleniumの基本的な使い方などについても、他の方が分かりやすく解説してくれている記事があるのでここでは省略します。

Selenium
PythonでSeleniumを使ってスクレイピング (基礎)

openpyxl
PythonからExcelファイルをいじるopenpyxl

pywinauto
pywinauto/pywinauto - Github

PyInstaller
Python3でGUI(WxPython)実行ファイル(pyInstaller)[Windows]

プログラム

取得する日や日数はその時によって変わります。また勤怠データを取得したいだけで、印刷はしたくない場合もあります。
なので一部の設定は別に設定ファイルを設けて、そこに記述します。

config.ini
[shop_settings]
username  = user
password  = password
shop_id   = 100000000

[crawl_settings]
start_day = 2018/01/01(yyyy/mm/dd)
days      = 5
printout  = on(on/off)

上記が設定ファイルです。
ログインIDとPWと所属する場所(店舗や支店や部署)などを[shop_settings]に記述します。
クロールを開始する日と日数、印刷するかしないかの設定を[crawl_settings]に記述します。

auto_print.py
# coding:utf-8
import sys
import os
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from time import sleep
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.action_chains import ActionChains
import openpyxl as px
import pywinauto
import configparser

#ユーザー設定
inifile = configparser.ConfigParser()
inifile.read('.\\config.ini', 'UTF-8')

username = inifile.get('shop_settings', 'username')
password = inifile.get('shop_settings', 'password')
shop_id = inifile.get('shop_settings', 'shop_id')
start_day = inifile.get('crawl_settings', 'start_day')
days = int(inifile.get('crawl_settings', 'days'))
printout = inifile.get('crawl_settings', 'printout')

#保存するExcelファイルの選択と有効化
wb = px.load_workbook('work_data.xlsx')
ws = wb.active

#chromedriverの指定
driver = webdriver.Chrome(executable_path='.\\chromedriver.exe')

def login(username,password):
    # 管理画面アクセス
    driver.get("https://touchontime.com/admin")
    #最大化
    driver.maximize_window()
    # ログイン処理
    driver.find_element_by_id("login_id").send_keys(username)
    driver.find_element_by_id("login_password").send_keys(password)
    driver.find_element_by_id("login_password").send_keys(Keys.ENTER)

def skip():
    #適当な座標をクリックしてチュートリアル画面スキップ
    actions = ActionChains(driver)
    actions.move_by_offset(100,100)
    actions.click()
    actions.perform()

def date_setting(start_day,shop_id):
    #日別集計画面選択
    driver.find_element_by_xpath("//*[@id='daily_working_link']").click()
    sleep(2)
    #店舗の設定
    shop_element = driver.find_element_by_name('selected_section_id')
    shop_select_element = Select(shop_element)
    shop_select_element.select_by_value(shop_id)

    #日付の設定
    for i in range(10):
        driver.find_element_by_id("parts_daily_select_picker").send_keys(Keys.BACKSPACE)
    driver.find_element_by_id("parts_daily_select_picker").send_keys(start_day)
    driver.find_element_by_id("parts_daily_select_picker").send_keys(Keys.ENTER)
    driver.find_element_by_id("display_button").click()
    sleep(2)

def page_print():
    #印刷ダイアログを開く
    pywinauto.keyboard.SendKeys("+^P")
    sleep(1)
    a_check = lambda:pywinauto.findwindows.find_windows(title=u'印刷', class_name='#32770')[0]
    dialog = pywinauto.timings.WaitUntilPasses(5, 1, a_check)
    pwa_app = pywinauto.Application()
    pwa_app.connect(handle=dialog)
    #Windowsの印刷設定画面
    window1 = pwa_app[u'印刷']
    window1.Wait('ready')
    sleep(1)
    button1 = window1[u'詳細設定(&R)']
    button1.Click()
    #プリンターの印刷設定画面
    window2 = pwa_app[u'印刷設定']
    window2.Wait('ready')
    window2.TabControl.Select(0)
    sleep(1)
    window2[u'横(&E)'].Click()
    #印刷
    sleep(1)
    window2[u'OK'].Click()
    window1[u'印刷(&P)'].Click()

def crawl():
    i=1
    while i<=days:
        #日付の取得
        element = driver.find_element_by_xpath("//*[@id='parts_daily_select_picker']")
        a = element.get_attribute("value")
        ws['%s%d' % ("A",i+1)].value = a

        #所定時間の取得
        element = driver.find_element_by_xpath("//*[@id='tab-1']/div/div[2]/div[1]/table/tfoot/tr/td[13]/p")
        ws['%s%d' % ("B",i+1)].value = element.text

        #深夜時間の取得
        element = driver.find_element_by_xpath("//*[@id='tab-1']/div/div[2]/div[1]/table/tfoot/tr/td[16]/p")
        ws['%s%d' % ("C",i+1)].value = element.text

        #深夜残業時間の取得
        element = driver.find_element_by_xpath("//*[@id='tab-1']/div/div[2]/div[1]/table/tfoot/tr/td[17]/p")
        ws['%s%d' % ("D",i+1)].value = element.text

        #印刷する
        page_print() if printout == "on" else None

        #次のページに遷移
        driver.find_element_by_xpath("//*[@id='button_next_day']").click()
        sleep(1)
        i+=1

if __name__ == '__main__':
    login(username,password)
    skip()
    date_setting(start_day,shop_id)
    crawl()
    #保存
    wb.save('work_data.xlsx')
    #終了
    print("3秒後に終了します")
    sleep(3)
    driver.quit()

こちらがプログラム本体です。
予めクロールしたデータを保存するためのExcelファイルwork_data.xlsxを作成しておきます。

関数の説明

login(username,passowrd)

ログイン用の関数です。
iniから取得した情報をもとに、ログイン処理を実行します。

skip()

チュートリアル画面スキップ用の関数です。
素直に次へボタンを押してもいいのですが、画面の遷移に時間がかかるので適当な位置を座標指定でクリックしています。

date_setting(start_day,shop_id)

日付などの日別データの表示に関係するデータをセットする関数です。
日別のデータを取得/印刷したいので、日別の集計画面を選択して、iniの情報をもとに所属店舗や日付の設定を行います。
日付の設定の時に、Seleniumのclear()を使用せずにBackSpaceを何回も押しているのはHTMLの属性がinput=dateになっており通常のフォームに対してのclear()が効かないからです。
フォーラムなどを見ると処理がめんどくさそうなので、妥協してBackSpaceを何回も押しています。

page_print()

ページ印刷用の関数です。
ここの処理はブラウザの操作から外れるため、Seleniumだけでは操作できません。
そこでWindowsを自動操作するための、pywinautoというライブラリを使用して操作します。
こちらの記事に大変お世話になりました。ありがとうございます。

pywinautoは日本語のドキュメントが少なくて実装するのに苦労しましたが、Seleniumと同じようにWindowsフォームコントロールの要素を指定することで操作ができます。
指定した要素が存在しないなどのエラーが発生した時に、指定した画面の要素の一覧がエラーメッセージと一緒に表示されるのでそれを頼りにしました。

crawl()

実際にクロール処理を行う部分です。
そのページに表示されている日付や勤務時間などのデータを取得して、予め作成しておいたExcelファイルに追記します。

設定ファイルで印刷がonになっていれば印刷、offになっていればスキップして次のページに遷移します。
これをiniで設定した日数分行います。

exe化して別の環境でも使えるようにする

このままだとPythonの環境(加えてライブラリなども)が揃っていないと実行できません。
今回はPyInstallerを使用してexe化しました。

PyInstallerは手軽にPythonプログラムをexe化できます。
使い方についてはこの記事で解説されているので省略します。

ハマったことろなど

今回このプログラムを作成するにあたり、ハマったところがいくつかあったので別記事にしました。

Windows10でpywinautoを使おうとしたらハマった
pywinautoを用いたプログラムをexe化する時にハマった

Githubにあげてみました

全然使ってなかったけど使ってみました。
ponkio-o/auto_print