ワンクリックで出退勤してくれるアプリ作ってみた

#初めに
私が勤めさせてもらっている会社の出退勤は会社のサーバーにログインして
出退勤のページまでポチポチが必要です。。。うーん、面倒だ。
Python で楽できないかなー

#まずは、chrome 立ち上げてみるか
色々ググって斜め読み。
とりあえず、これが楽ちんそうだ。

```LaunchChrome.py
#事前に以下を実行しライブラリをインストールしてね
#pip install webdriver_manager
#pip install selenium

#↓ python から ブラウザドライバに命令を出せるようになる
from selenium import webdriver
#↓ ブラウザドライバを勝手に取りに行ってくれる                
from webdriver_manager.chrome import ChromeDriverManager 

#貴方のChromeバージョンに適したドライバを勝手に取得しつつ、
#Chrome を制御することをココに宣言します!('-'*ゞ
driver = webdriver.Chrome(ChromeDriverManager().install())

#Chrome を開く
driver.get("https://www.google.co.jp")
```

解説はココが神。

https://qiita.com/disk131/items/3d6203d59dd9799910ab

#なんかググってみる
次は検索でもしてみるか。とりあえず以下をコピペで試してチョ

```SearchSomething.py
from selenium import webdriver                
from webdriver_manager.chrome import ChromeDriverManager 
#↓ 記述がシンプルになるオマジナイ
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get("https://www.google.co.jp")

search_box=driver.find_element_by_name("q")#検索ボックスをブラウザから探して search_box と宣言
serach_box.send_keys("amazon")             #検索ボックスにamazonって入れてみる
serach_box.send_keys("keys.RETURN")        #検索ボタンをクリックして GO!!

```

解説はココが神。

https://yutori-insotsu.net/pc/automatic-operation-of-chrome-with-python

https://netwiz.jp/python3-web-browser/

#社員専用ページに python でログインできた
たぶん、前述の知識を組み合わせると "Chrome を立ち上げ => 社員専用ページにログイン"まで行けると思う。

私の場合は、出退勤の画面を開くと新しいタブが開く。
Python で制御できるのは 1page だけ。
新しいタブで開いたページを継続して制御してほしい場合は、
今のページから新しいタブのページにスイッチが必要。

```switch_tab.py

handle_array = driver.window_handles              #今開いているウィンドウを handle_array に突っ込み、配列化
driver.switch_to.window(driver.window_handles[1]) #新規で開いたウィンドウを開く

```

ココが分かり易い。

https://tanuhack.com/selenium-change-window/

#つまいずいた rev1
社内システムでは、月単位で累計残業時間が 20時間を超えるとアラートが出てくる場合がある
ココを見てもらうとアラーとのイメージできるかも

https://www.sejuku.net/blog/29821

つまり、アラートを消さないと出勤画面に進めない。

```alertClick.py
#方針
#alert が表示される前提で、accept() でポップアップを消す
#alert が表示されない場合は、pass というなのスルーで次の処理に進む
try:
  alert = driver.switch_to.aleart()
  alert.accept()
except:
  pass
```

前述の記述についてはココが神

https://www.seleniumqref.com/api/python/window_set/Python_accept_dismiss.html

念のため、アラートを消した後、
今モニタしている画面が何処なのかを
以下のコマンドで確認しながら進むと余計な混乱を防げる

```checkCurrent.py
print(driver.current_url)
```

#つまいずいた rev2
やっとのことで辿り着いた出退勤画面。
Xpath を指定しても Error がでる。
そんな Xpath ありませんよ。。。ってね。
答えはコレでした。

https://hacknote.jp/archives/51263/

https://qiita.com/G-san/items/19dfae091923545f28fe


平たく言うと HTML のpage は分割されている事があり、
何処の Frame をアクセスするか指定しないといけない場合がある。
へー、勉強になりました。

#あとはポチポチ出勤ボタン、退勤ボタンを押すだけ
一応、時間を見て、"出勤、退勤、何もしない" 
から勝手に選択してもらうようにした。

以下を見て、適当に時間を抽出して if 分を書けば大丈夫。

https://note.nkmk.me/python-datetime-timedelta-conversion/

一応イメージをメモしておく。

```selectTable.py
#事前に import datetime を宣言
#dt_now = datetime.datetime.now() も宣言
#dt_now は今日の日付情報が詰まった箱をイメージしてね

if dt_now.hour == 8 and 0 <= dt_now.minute <= 45:#8:00 ~ 8:45 の間にアプリを起動すると出勤処理
   #出勤ボタンをクリック
elif dt_now.hour == 17 and 15 <= dt_now.minute or 18<=dt_now.hour:#17:15 以降の場合は退勤処理
   #退勤ボタンのクリック
else                                           #それ以外の時間帯は立ち上がった chrome を閉じる
   driver.quit()
   exit()
```


#上司に出退勤を知らせるメールも飛ぶようにしておく

まずは以下を軽く読んでみる

https://qiita.com/konitech913/items/51867dbe24a2a4272bb6

出退勤の処理後に、適当に書き換えた以下のプログラムを走らせた。

```mailSent.py
#事前に import datetime を宣言
#dt_now = datetime.datetime.now() も宣言
#dt_now は今日の日付情報が詰まった箱をイメージしてね

import win32com.client

outlook = win32com.client.Dispatch("Outlook.Application")
mail = outlook.CreateItem(0)

mail.to = 'jyoshi@com'#上司のメアドを入れる
#↓表題は今日の日付を勝手に取り込む記述に調整
#dt_now.month は月の情報,dt_now.dayは日の情報
#.format() を使ってテキスト内に変数(日付)をぶち込む
mail.subject = '{}/{} 在宅勤務を開始します'.format(dt_now.month,dt_now.day)
mail.bodyFormat = 1
mail.body = ''' 
'''

mail.Send()#メール送信

driver.quit()#出退勤画面を表示していた chrome を閉じる
```

やりたいことが出来た。うれぴー
一応、アプリの起動中は、ほかの作業してても処理は進むので
事実上、時間短縮が出来たといえる。
　　　　　　　　　　　　　-めでたし、めでたし-

#その他
・UA 偽装とかやってみたけど、chrome のままアクセスが速かった。
・その他、オプションを試すと 2,3秒早くなった。
・今回は chromdriver を使った。IEdriver は正直癖が多く、動かないこともあり使い勝手が悪かった。
  PC のセキュリティやレジストをいじらないとダメ。もっと手軽にやりたい。

https://bitwave.showcase-tv.com/selenium%e3%81%a7internet-explorer11%e3%82%92%e5%8b%95%e3%81%8b%e3%81%99%e6%96%b9%e6%b3%95/

・その他のブラウザは試していない
・最近、アルゴ本読んでるけど、今回あまり使わなかった。
　アルゴと結び付けながらコード書くってどんな作業になるんだ？？

https://www.amazon.co.jp/%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E5%AE%9F%E6%8A%80%E6%A4%9C%E5%AE%9A-%E5%85%AC%E5%BC%8F%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88-%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%83%BC-%E4%B8%AD%E7%B4%9A%E7%B7%A8-Compass-Books%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/483997277X/ref=sr_1_1?adgrpid=122732831441&dchild=1&hvadid=506499916002&hvdev=c&hvqmt=b&hvtargid=kwd-1077952456545&hydadcr=1789_10144706&jp-ad-ap=0&keywords=%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E5%AE%9F%E6%8A%80%E6%A4%9C%E5%AE%9A&qid=1630822012&sr=8-1

・自分のためではなく、みんなの為にコードを書く。
・"各々の環境が異なる中で一律に動くソフト作り"は難しいことだと感じた。
・でも逃げずに向き合えば新しい学びがあり、成長につながって嬉しかったし楽しかった。
・シェアした仲間には喜んで貰えた。
・やはりコミュニケーションは大切だ。
・関係ないけど、英語の勉強でトレーラーに字幕をつけてみた。
　outlast の最新トレーラーです。よかったらドウゾ(youtube上で日本語 字幕表示の設定をしてください)。

　https://youtu.be/xsgXYDtjuCM

#追加リクエスト1
処理を軽くしてスピードをあげたいとリクエストがあった。
ただ、自分が試している感触からすると、アクセスが集中する"出勤"の時間帯はどうしても遅くなる。
例えば 8:45までに出退勤する場合、8:40 ~ 45 は鬼のように遅い。

なので遅くとも 8:30 までにアプリを起動するように伝えたところ、
改善が見られた。

退勤はそもそも、各々の時間で切っているため、アクセスが集中せず
快適に動くようだ。

念のため試した option とかを貼っておく。

```options.py
options = webdriver.ChromeOptions()

#https://www.ugtop.com/spill.shtml で偽造したいブラウザの情報を確認
UA = 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
options.add_argument('--user-agent=' + UA)        #Chrome を IE として開く
options.add_argument('--disable-extensions')      #全ての拡張機能を無効化
options.add_argument('--proxy-server="direct://"') #Proxy経由ではなく直接接続する
options.add_argument('--proxy-bypass-list=*')     #すべてのホスト名
options.add_experimental_option("excludeSwitches", ["enable-automation"])#自動制御されてるウィンドウを消す
options.add_experimental_option('useAutomationExtension', False)#自動制御されてるウィンドウを消す
options.page_load_strategy = 'eager'              #page open のスタイルを normal => eager に変更
options.add_argument('--blink-settings=imagesEnabled=false')#画像を無視

#最新ドライバーの取得後にブラウザ制御を開始
driver = webdriver.Chrome(options=options,executable_path=ChromeDriverManager(path="C:/Users/1919/Pictures").install())
```

Chrome Driver Manager はドライバーの update を見つけると、
格納パスを指定することで勝手に、そこに保存してくれる。

残念だが headless は社内のコンプライアンスとして認められなかったので未検証。
以下の記述にも淡い期待を持ったが、ダメだった。

https://www.seleniumqref.com/api/java/about/About_expectedConditions.html

https://www.seleniumqref.com/api/python/conditions/Python_element_to_be_clickable.html

##結論
アクセスの時間帯をずらして、アプリを動かす事が何よりも対策となった(自分は)
Simple is best!!

#追加リクエスト2
退勤メールの本文に、その日のスケジュールを埋め込んで欲しいらしい。
とりあえず、outlook からスケジュールを引っ張る情報を探した。

https://qiita.com/konitech913/items/2ec831863ad84db23558

とりあえずコピペで動作確認しつつ、私好みに書き換えた。

```get_schedule.py
import win32com.client
import datetime
from dateutil.relativedelta import relativedelta
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
today = datetime.date.today()
TwoDaysAgo = today + relativedelta(days=-2)

def get_schedule(num):
    calender = outlook.GetDefaultFolder(9) # "9" はスケジュール
    items = calender.Items                 # スケジュールの中身を全部 items に突っ込む
    dic = {}
    
    #本家は ↓ が for event in items となっている。
    #この場合、items[0] から順に event に代入する。
    #実は items[0] はスケジュール最古の情報。最新が欲しい場合は items[-1] から順に回す必要あり
    #よって、reverse を使った。今日のスケジュールだけを取り出したかったので、これにより処理時間が格段に減った。
    #
    #実は items 内は時系列に情報を格納していない。大枠は時系列だが 1,2 日ごちゃまぜになっているケースがある
    #辞書で {key : 日付、データ : スケジュール情報} として管理。後程 key(時間) でソートする。
    #ソート後(時系列)に配列を組み直す。
    #それによりコメント最下部にある最終イメージで項目を時系列に箇条書きが出来る。
    for event in reversed(items):          
        if event.start.date()==today:                     #日付が今日!!
            key = event.start.time().strftime("%H:%M:%S") #日付型のデータは key となりえないので文字変換
            dic[key] = "・"+event.subject                 #格納データの文頭に "・" をくっつけたい
        elif event.start.date() <= TwoDaysAgo:            #2日以前のデータが来たら break 
            break                                         #余分に確認して"今日"の収集漏れを防ぐ
    
    dic = sorted(dic.items())                             #冒頭に述べたように前述の辞書を時間でソートする                
    num.append("■Schedule")                               #箇条書きの top に "■Schedule" と名付けたい
    for a,b in dic:                                       #辞書のデータだけを取り出してソート済みのデータを
        num.append(b)                                     #順番に num に格納していく。
    ##イメージ
    #["■Schedule","・会社用 PC に電源を入れる","・個人用 PC に電源を入れる","・メールをチェックする","・youtube をチェックする"
    #　　　　　　　↓
    #今後の記述でメール本文には以下のように表示させる(予定)
    #             ↓
    #■Schedule
    #・会社用 PC に電源を入れる
    #・個人用 PC に電源を入れる
    #・メールをチェックする
    #・youtube をチェックする
    #・...etc

```

#追加リクエスト3
退勤メールの本文に、その日の Zendesk の update を埋め込んで欲しいらしい。
一応 Zendesk の説明は以下が神。

https://qiita.com/fuji3zpg/items/9f50b796712b3f00b497

まぁ、技術サポートの問い合わせを取りまとめるシステムです。
顧客からのメールの受信と顧客へのメールの送信を一括管理することで、
技術サポート業務を個人のスペックに依存せずにシステムで一律管理する感じです。

んで、こちらから送信、or 顧客からメールを受信した場合に
outlook にメールが飛んできます。
これを退勤メールに貼ることで、今日一日のアクティビティが一目瞭然になる。

っというわけで、outlook の受信ボックスを読み込む知識が必要。

https://qiita.com/konitech913/items/8a285522b0c118d5f905

##しかし、つまずいた
メールアドレスが正しく読めない。
こんな感じになる。

**/O=Microsoft/OU=EXCHANGE ADMINISTRATIVE GROUP (XXXXXXXX)/CN=RECIPIENTS/CN=UserA**

ググったら、神が居た。

https://blog.litus.co.jp/2017/07/vstooutlook-smtp.html

https://www.fixes.pub/program/451650.html

ほぼ水平に近い斜め読み。
よし、動かしてみよう。。以下に一旦落ち着いた。

```get_zendesk_update.py
import win32com.client
import datetime
from dateutil.relativedelta import relativedelta

outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
today = datetime.date.today()#やはり受信データも時系列でデータを配列としてデフォルトで格納されていない
TwoDaysAgo = today + relativedelta(days=-2)#前述にあるように今日の日付けと 2 日前の日付けを用意する

def get_zendesk_update(num):
    inbox= outlook.GetDefaultFolder(6)    
    messages = inbox.Items
    dic = {}
    for message in reversed(messages):
        if message.receivedtime.date() <= TwoDaysAgo:
            break
        elif message.receivedtime.date() == today:
            if message.Class== 43:
                try:
                    if message.SenderEmailType== "SMTP" \
                        and message.SenderEmailAddress == "waiwai_support@zendesk.com":
                       print("========")
                       print("Subj: " + message.Subject)
                       print("Email Type: ", message.SenderEmailType)
                       print("Name: ", message.SenderName)
                       print("Email Address: ", message.SenderEmailAddress)
                       print("Date: ", message.ReceivedTime)
                       key = message.ReceivedTime.time().strftime("%H:%M:%S")
                       dic[key] = "・"+message.Subject
                except Exception as e:
                    print(e)
                    continue
    dic = sorted(dic.items())
    num.append("■Zendesk_update")
    for a,b in dic:
        num.append(b)
```

基本は追加リクエスト 2 に書いた補足をイメージ出来れば補足説明無くても読めると思います。
念のため、メールの起こし方もメモしておきます。

```mail.py
#冒頭に前述にある以下を宣言しておく
#def get_schedule(num):
#def get_zendesk_update(num)

TodaysActions = []                #今日のアクションを格納する配列
get_schedule(TodaysActions)       #スケジュールからアクティビティを取得
get_zendesk_update(TodaysActions) #受信ボックスからZendesk update を取得

dt_now = datetime.datetime.now()
outlook = win32com.client.Dispatch("Outlook.Application")
mail = outlook.CreateItem(0)
mail.subject = '{}/{} 在宅勤務を開始します'.format(dt_now.month,dt_now.day)
mail.to = 'jyoshi@com'
mail.bodyFormat = 1
TodaysActions = "\r\n".join(TodaysActions)# "\r\n" は改行してくれるオマジナイ
mail.body = "{}".format(TodaysActions)    #メールの本文に TodaysActions を挿入
mail.display(True)                        ##手動メール送付
```

周囲の反応(リアクションを)を見つつ、継続して最適化を目指していきます。m(_ _)m

#追加リクエスト4
local な話で申し訳ない。
Zendesk のサポートシステムは、どんなアップデートもメールで知らせてくれる。
例えば、社内の関係者間のメールでのコミュニケーション、顧客からの返信、こちらからの回答 .. etc
アクティビィとして全部それらをリストしてメールの本文を貼ると以下のようになる。

■Zendesk
・#21348 (弊社返信) [質問] アルティマウェポンの入手方法
・#20845 (弊社返信) 評価boardライセンス及び PCIe に関して
・#21348 (顧客返信) [質問] アルティマウェポンの入手方法
・#20845 (弊社返信) 評価boardライセンス及び PCIe に関して
・#9799 (旧環境に新規問い合わせがありました。) カスタムマテリアルの生成方法について
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (弊社返信) [質問] アルティマウェポンの入手方法
・#21012 (顧客返信) (質問)効率的なマテリアルの育成方法について
・#21443 (新規)(組織未登録の顧客からのチケットが発行されました) 【超級武人破斬の入手方法】

**受け取った方はどうだろう? 確かに一生懸命、仕事した感は伝わる。。
だが、重複してて見にくいのではないか？**

っというわけで最適化の依頼が来た。
個人的には、文字処理の追加は特に遅くなるのでは？それが懸念だった。
極力 簡略化して、早くやりたい。。
とりあえず、以下でやってみることにした。

**冒頭で引っ張ったメール情報 => 
エクセルに貼る =>
エクセルを python で制御(編集)=>
編集データをメールの本文に引っ張る**

なんとなく、以下の手順でやってみた

**Step1:すでに用意したエクセルファイルを開いて編集**

https://note.nkmk.me/python-openpyxl-usage/

split() を使って、番号と題名で区切った。
あとは、エクセルの特定の列(番号)を int に変換してpandas でソート。
そのあとは for 分で隣接する同一番号は、行単位で削除する。
文字で比較はしない。番号 + 本文 を文字として全部比較する処理は無駄の塊のような気がした。

**Step2:編集したエクセルをソート**

http://wheelchairtext.net/program/1667/

**Step3:ソートしたセルを python 用の配列に詰めなおす**

https://office54.net/python/excel/python-cell-none

基本的には空のセルにぶち当たるまで、ソート済みの表から
str(番号)、本文の両方を "".join でくっつけてから格納した。
検証前のデータも参考に、改変後の結果をどうぞ。

###改変<font color="red">前</font>
■Zendesk
・#21348 (弊社返信) [質問] アルティマウェポンの入手方法
・#20845 (弊社返信) 評価boardライセンス及び PCIe に関して
・#21348 (顧客返信) [質問] アルティマウェポンの入手方法
・#20845 (弊社返信) 評価boardライセンス及び PCIe に関して
・#9799 (旧環境に新規問い合わせがありました。) カスタムマテリアルの生成方法について
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21348 (弊社返信) [質問] アルティマウェポンの入手方法
・#21012 (顧客返信) (質問)効率的なマテリアルの育成方法について
・#21443 (新規)(組織未登録の顧客からのチケットが発行されました) 【超級武人破斬の入手方法】


###改変<font color="red">後</font>
■Zendesk
・#21443 (新規)(組織未登録の顧客からのチケットが発行されました) 【超級武人破斬の入手方法】
・#21348 (内部メモ) [質問] アルティマウェポンの入手方法
・#21012 (顧客返信) (質問)効率的なマテリアルの育成方法について
・#20845 (弊社返信) 評価boardライセンス及び PCIe に関して
・#9799 (旧環境に新規問い合わせがありました。) カスタムマテリアルの生成方法について

かなりスッキリした。
一応、追加リクエスト4 を全部実行した時の時間を測った。

##elapsed_time:<font color="Blue">0.09820389747619629[sec]</font>

追加リクエスト4 に応えるための上乗せ時間としては、まぁいいのかな。。
フィールドに展開して感想を募り、問題があれば別途検討します。。。

#最後に
昔から応援して下さった方、初めましての方々、LGTM、ストック、有難うございます。
とっても嬉しいです。
今後の予定では、今までの内容を網羅したコードを、
技術的に段階に分けて、git に挙げることを検討しています。
いつになるか分かりませんが、気長にお待ちください。