【Python】妻に真夜中のダンスレッスン予約を強要されたので、Webスクレイピングで回避を試みようとしている話 （１１．seleniumでカレンダーの日付選択など）

前回は、<a href="https://qiita.com/sibulabo/items/cdf970486ad2d82915f3">「１０．seleniumでログイン」</a>について投稿しました。今回はその続きで、予約画面に遷移して表示されたカレンダーの予約日をクリックする部分についてコーディングしたいと思います。

-------------------------------------------------------------------------------------------
# ログイン後の画面遷移
ログインに成功すると、レッスン予約画面に遷移します。そこで予約ボタンをクリックします。
![名称未設定-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915699/79e510c1-6f5a-8b3a-bf6d-66bc93085fd8.jpeg)

左にカレンダー、右側に該当日のレッスン一覧が確認できるレッスン一覧画面が表示されます。
デフォルトでは当日のレッスン一覧が表示されており、カレンダーの日付をクリックすると、そのクリックした日付のレッスンが右側に表示されます。また、カレンダー右上の"^"をクリックすることで次月のカレンダーが表示され、次月のレッスン予約が可能です。
![名称未設定-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915699/f94478cb-fd09-70da-90ee-ceffc51845ad.jpeg)

今回は、
- レッスン予約画面で予約ボタンをクリックする。
- カレンダー上のレッスン予約日をクリックする。

部分をコーディングします。

## カレンダーの日付をクリックする部分のコーディング

### 予約ボタンのタグについて
検証ツールで予約ボタンのタグを確認しました。要素を特定できるキーとしては、「リンクテキストが"予約"」という部分しか使えそうにありません。
```html
<a href="xxx">
    <span class="ui-btn-inner">
        <span class="ui-btn-text">予約</span>
    </span>
</a>
```
### 予約ボタンをクリックする。
seleniumの find_elementメソッドで、リンクテキストが"予約"の要素を検索してclickするようにコーディングしました。

```reserveLesson.py
 47:      #予約画面へ移動（予約リンクをクリック）
 48:      elm = driver.find_element(By.LINK_TEXT,"予約")
 49:      elm.click()
```

### カレンダーのタグについて
カレンダーでは、10/30～11/06までは薄い文字色が設定された選択できない日付になっています。そして、11/7～30までが予約可能な日付になっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915699/1f50a8f2-3ca9-b952-feec-ad36e5ee619b.jpeg" width="40%">
検証ツールを使用してカレンダーのHTMLタグを確認しました。選択できる日付の場合は、aタグが使用されていることがわかりました。また、右上の"^"の翌月のカレンダーを表示する要素についても同様で何れもリンクテキストが要素検索のキーになりそうです。
```html
/* カレンダーの"^"要素 */
<a class="ui-datepicker-next" onclick="XXX();" title="次>">
    <span>次&gt;</span>　/* &gtは、">"を表すコードです。(greater than）*/
</a>

/* 日付の選択ができない要素 （日付がグレーアウトの要素）*/
<td>
  <span class="ui-state-default">1</span>
</td>
/* 日付の選択ができる要素 */
<td onclick="selectDay(this);return false;">
  <a class="ui-state-default" href="#">25</a>
</td>
```
### カレンダーの日付をクリックする。
```reserveLesson.py
 51:      #レッスン日が次月の場合は、翌月のカレンダーを表示する。
 52:      if datetime.today().month+1 == rsvdic["予約日付"].month:
 53:          driver.find_element(By.LINK_TEXT,"次>").click()
 54:      #カレンダーのや予約日をクリック
 55:      cal_day = driver.find_elements(By.LINK_TEXT, rsvdic["予約日付"].strftime('%#d'))
 56:      if cal_day:
 57:          driver.find_element(By.LINK_TEXT, rsvdic["予約日付"].strftime('%#d')).click()
```
予約したい日付が翌月の場合は、カレンダー右上の"^"をクリックして、翌月のカレンダーを表示する必要があります。そのための処理が 51～53行目になります。Excelに設定された予約日付が 当月+1、つまり翌月（datetime.today().month+1）の場合は、ハイパーリンクテキストが "^" である要素をクリックしています。

55行目では、ハイパーリンクテキストが予約日付である要素を検索しています。find_elementsという語尾に"s"がつくメソッドを使用していますが、これを使用することでもし該当の日付要素が存在しなかった場合にエラーが発生しないように対策しています。"s"なしのメソッドの場合は、要素が見つからないとエラーが発生してしまうのですが、"s"付きのelementsでは、空の配列が返ってくるだけでエラーは発生しません。
そして、56、57行目で要素があった場合は、その要素つまり日付をクリックします。この場合（57行目）には要素の存在が確定しているので、find_elementメソッド、"s"なしのメソッドを使用しています。


## reserveLesson.pyの全容
```reserveLesson.py
  1:  #各コンポーネントをインポート
  2:  from selenium import webdriver
  3:  from selenium.webdriver.chrome import service
  4:  from selenium.webdriver.common.by import By
  5:  from webdriver_manager.chrome import ChromeDriverManager
  6:  from openpyxl import load_workbook
  7:  import os
  8:  from datetime import datetime, date, timedelta
  9:  
 10:  #--定数----------------
 11:  LOGIN_URL = "https://xxxxx/"
 12:  EXCEL_FILENAME = "reserveLessonList.xlsx"
 13:  
 14:  #******************************************************************
 15:  # main処理
 16:  #******************************************************************
 17:  def main():
 18:      #会員・予約情報(Excel)を辞書リスト形式で取得する。
 19:      rsvlst = getExcelDicList()
 20:  
 21:      #Webドライバーオブジェクトを取得する。
 22:      driver = openChromeBrowser()
 23:  
 24:      #予約件数分ループ
 25:      for rsvdic in rsvlst:
 26:          #レッスン予約処理
 27:          reserveLesson(driver, rsvdic)
 28:  
 29:      #chromeドライバー セッション終了
 30:      driver.quit()
 31:  
 32:  #---------------------
 33:  # SubRoutine
 34:  #---------------------
 35:  #--レッスンを予約する
 36:  def reserveLesson(driver, rsvdic):
 37:      #要素が見つかるまで、最大10秒間待機する(暗黙的な待機)
 38:      driver.implicitly_wait(10)
 39:  
 40:      #サイトを表示する。
 41:      driver.get(LOGIN_URL)
 42:      #ユーザー名、パスワードを設定してログイン
 43:      driver.find_element(By.ID, "member_id").send_keys(rsvdic["ユーザー名"])
 44:      driver.find_element(By.ID, "password").send_keys(rsvdic["パスワード"])
 45:      driver.find_element(By.CLASS_NAME, "login_btn").click()
 46:  
 47:      #予約画面へ移動（予約リンクをクリック）
 48:      elm = driver.find_element(By.LINK_TEXT,"予約")
 49:      elm.click()
 50:  
 51:      #レッスン日が次月の場合は、翌月のカレンダーを表示する。
 52:      if datetime.today().month+1 == rsvdic["予約日付"].month:
 53:          driver.find_element(By.LINK_TEXT,"次>").click()
 54:      #カレンダーのや予約日をクリック
 55:      cal_day = driver.find_elements(By.LINK_TEXT, rsvdic["予約日付"].strftime('%#d'))
 56:      if cal_day:
 57:          driver.find_element(By.LINK_TEXT, rsvdic["予約日付"].strftime('%#d')).click()
 58:  
 59:  
 60:  #--レッスン予約サイトを開く
 61:  def openChromeBrowser():
 62:      #Serviceオブジェクトを介して、execute_pathを指定する。
 63:      chrome_service =  service.Service(executable_path=ChromeDriverManager().install())
 64:      #chromeドライバー セッション開始（最新Ver自動インストール、およびオプション指定）
 65:      driver = webdriver.Chrome(service=chrome_service, options=getOptions())
 66:  
 67:      return driver
 68:  
 69:  #--Get ChromeDriver Options------
 70:  def getOptions():
 71:      options = webdriver.ChromeOptions()
 72:      options.headless = False    #ヘッドレスモード True:ブラウザ画面非表示、False:表示
 73:  
 74:      return options
 75:  
 76:  #--会員・予約情報を辞書リスト形式で取得する。---
 77:  def getExcelDicList():
 78:      #予約一覧Excelファイルパス取得
 79:      rsv_lst_path = os.path.join(os.getcwd(), EXCEL_FILENAME)
 80:      #予約一覧を読み込み専用で開く。
 81:      wb = load_workbook(rsv_lst_path, read_only=True)
 82:      #予約一覧シートを指定
 83:      ws = wb["予約一覧"]
 84:  
 85:      #**********************************
 86:      #* {列名：セル値}の辞書をリスト化する。（[{列名：セル値},{列名:セル値},...]）
 87:      #**********************************
 88:      header = ws[1]  #1行目の列名を取得
 89:      rsvlst =  []    #リスト初期化
 90:      #予約一覧データ部(2行目～)の行数分処理する。
 91:      for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
 92:          rsvinf = {}     #辞書初期化
 93:          #列名:セル値の辞書作成
 94:          for k, v in zip(header, row):
 95:              rsvinf[k.value] = v.value
 96:          #作成した辞書をリストに追加
 97:          rsvlst.append(rsvinf)
 98:  
 99:      #Close ExcelBook
100:      wb.close()
101:  
102:      return rsvlst
103:  
104:  #-------------------------------
105:  # スクリプト実行時に mainサブコール
106:  #-------------------------------
107:  if __name__ == '__main__':
108:      main()
```

今回は、カレンダーの日付をクリックしてレッスン一覧を表示するところまでコーディングできました。次回は、いよいよ予約場所を特定して予約するところまでコーディングできたらなぁと思います。:wink:

予約場所の特定とは、<a href="https://qiita.com/sibulabo/items/943b8b460700da85df15">「５．Excelで会員・予約情報の管理を考える」</a>で下図の例を投稿しましたが、予約できる空き場所と希望する場所とを比較して、優先順位が一番高い空き場所を決定することを指します。できるのかなぁ:weary:
![WS000004.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915699/03d1c4a0-b632-5046-fcba-54e56049d884.jpeg)