Python＋Seleniumで楽天カードの明細をCSVダウンロードする

PythonとSeleniumを使って、楽天カード（楽天e-NAVI）サイトにログインして明細データを取得（CSVダウンロード）する方法を試してみました。

サンプルコード全文も記事の後半に載せているので参考になれば幸いです。そしてもっと上手いやり方ご存知の方いらっしゃったらぜひ教えてください＾＾

※2021/04/15時点の情報です。

## 環境
- Windows 10
- Python 3.8
- Selenium 3.141.0


## はじめに
ログインまでの部分や、webdriverのプロファイル設定に関する部分については、以下記事をご確認ください。

https://qiita.com/yoshi2045/items/856def50927c12dd4701

↑ログインするところまでのサンプルです。（本記事のサンプルコードにもログイン部分あります）

https://qiita.com/yoshi2045/items/be739680bceb2f2df6a2

↑サンプルのwebdriverは、Firefoxを使っています。CSVダウンロードするにあたって、Firefoxのプロファイル設定が必要なので、そのあたりを解説しています。

## 対象画面まで遷移させる
まず、楽天カード（楽天e-NAVI）のログイン後TOP画面から明細ダウンロード画面までどういう経路で遷移できるかを確認してみます。

＜画面フロー＞

1. TOP画面
2. お支払い（ご利用明細）画面 ← ここでCSVダウンロード

上記フローで目的の画面まで遷移できることが分かりました。では続いて、具体的に各画面ごとの遷移処理を見ていきましょう。

◆ログイン後のTOP画面

|![20210414-003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/4b4d5b69-80ac-34c5-a8f1-1cbbc01b48e4.png)|
|:--|

TOP画面からご利用明細画面への遷移は、画面上部にある「お支払い（ご利用明細）」メニューの中にあるリンクをクリックします。

あるいは、画面下部の「サービス一覧」エリアにも、ご利用明細画面へのリンクがあります。

|![20210414-002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/3be2dd56-0bce-f7be-774c-7eca40d6db89.png)|
|:--|
URLを調べてみるとどちらも同じで、静的HTMLへのリンクになっていたので、今回はget()メソッドを使って遷移させてみました。

```python
# お支払い（ご利用明細）画面まで遷移
browser.get("https://www.rakuten-card.co.jp/e-navi/members/statement/index.xhtml")

# browser.find_element_by_xpath("//dt[contains(.,'ご利用明細')]").click()
# 上記でも遷移可能だが、URL指定で遷移できる場合は、そっちのほうが確実
```


◆お支払い（ご利用明細）画面

get()メソッドを実行すると、お支払い（ご利用明細）画面に遷移します。

|![20210414-004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/f05c9d22-7216-28bc-70eb-e960915fec4b.png)|
|:--|

## 期間を指定する

続いて、CSVダウンロードしたい対象の期間を指定します。期間を指定するには、「ほかの月の明細を見る」ボタンをクリックします。

|![20210414-005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/8db4b22a-c512-0e42-d67e-63eab0ffd5bf.png)|
|:--|

クリックすると、下記のような月を選択する小窓が開きます。

|![20210414-006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/85742697-b263-cebd-ba3b-39143a9b64d4.png)|
|:--|

この小窓で対象月をクリックすると、明細が表示されます。

|![20210414-007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/29f7615e-adef-fcea-e8f9-51024b7b3caa.png)|
|:--|

ここまでの操作のコードは以下のようになります。

```python
# ダウンロード対象期間指定
# ほかの月を見る
# 一度期間指定したあとに、別の月を見たい時はこのボタンクリックから再開
browser.find_element_by_id("js-payment-calendar-btn").click()

# 対象月指定
# リンクテキストは操作年の月と同じ
browser.find_element_by_link_text("3").click()
```

ちなみに、小窓を表示させたあとに年を切り替えたい時は次のようなコードになります。

```python
# 前年のカレンダー表示させる場合
browser.find_element_by_id("js-payment-calendar-prev").click()
# 翌年のカレンダー表示させる場合
browser.find_element_by_id("js-payment-calendar-next").click()
```

## CSVダウンロードを実行

明細行が表示されたら、「明細コピー用 Excel(CSV)」ボタンをクリックすると、CSVファイルをダウンロードすることができます。

|![20210414-008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/33663cc5-8778-ffea-5de1-c4c0a0a21646.png)|
|:--|

### オーバーレイ広告に注意！

ただ、ここで1点ハマったところがありました。

それは、Seleniumでヘッドレスモードではなく、ブラウザ起動した状態では上手くダウンロードできるのに、ヘッドレスモードにした場合だけ、なぜか次のようなエラーが発生していました。

```python
ElementClickInterceptedException: Message: 
Element <a class="～中略～"> is not clickable at point (954,707) 
because another element <img src="～中略～"> obscures it
```

色々調べてみた結果、ヘッドレスモードとブラウザ起動した時とではブラウザのウィンドウサイズが異なっていて、そのせいで挙動が異なる場合があるようです。

参考：
https://yuki.world/selenium-headless-chrome-not-interactable/

今回の楽天カードの例でいうと、、、

|![20210414-009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/d1b09e09-77ec-8428-3528-cba3ef2a195b.png)|
|:--|

どうやらヘッドレスモードで実行した時のウィンドウサイズが小さく、上記のような画面最下部に表示される広告が上に被さってしまった結果、ダウンロードボタンを指摘できずにエラーとなってしまったようです。

もう一度エラー内容を見てみましょう。

```python
ElementClickInterceptedException: Message: 
Element <a class="～中略～"> is not clickable at point (954,707) 
because another element <img src="～中略～"> obscures it
```

対象のaタグエレメントがクリックできないよ、なぜならimgタグが覆い隠しているから。という意味ですね。

このような事象を回避するには2つ方法があります。

1. ヘッドレスモードのウィンドウサイズを拡張する
2. 画面スクロールさせる

今回は2の画面スクロールさせる方法で回避しました。そのコードがこちらです。

```python
# ウィンドウスクロール
# ヘッドレスモードだと、CSVダウンロードボタンがオーバーレイ広告と重なってしまい
# エラーが発生するため、ウィンドスクロールさせて表示位置をずらす
browser.execute_script("window.scrollTo(0, 300);")

# CSVダウンロード実行
browser.find_element_by_link_text("CSV").click()
```

これで無事、CSVダウンロードできると思います。

以上、Python＋Seleniumで楽天カードの明細をCSVダウンロードする方法でした！

## サンプルコード全文
```python
#
# 楽天カードサイトへログイン
#

import time
import json
from selenium import webdriver
from selenium.webdriver import Firefox, FirefoxOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec

# 設定ファイルを取得
login_info = json.load(open("login_info.json", "r", encoding="utf-8"))

# ログインサイト名
site_name = "card_rakuten"

# ログイン画面URL
url_login = login_info[site_name]["url"]

# ユーザー名とパスワードの指定
USER = login_info[site_name]["id"]
PASS = login_info[site_name]["pass"]
PASS2 = login_info[site_name]["pass2"]

# ダウンロードフォルダ
# 相対パス指定はできない模様
# dl_folder = r"..\dl-data"
# TODO: ダウンロードフォルダのパスは適宜変更してください
dl_folder = r"ダウンロードフォルダのパスを指定"

# オプション設定
options = FirefoxOptions()

# ヘッドレスモードを有効にする
options.add_argument('--headless')

# プロファイル設定
fp = webdriver.FirefoxProfile()

# ダウンロードフォルダ指定
# 0: デスクトップ、1:システム規定のフォルダ、2:任意の指定フォルダ
fp.set_preference("browser.download.folderList", 2)
fp.set_preference("browser.download.dir", dl_folder)

# ダウンロードマネージャウィンドウを表示させない
fp.set_preference("browser.download.manager.showWhenStarting", False)

# MIMEタイプを設定
fp.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream;text/csv")

# Firefoxを起動する
browser = Firefox(options=options, firefox_profile=fp)

# ログイン画面取得
browser.get(url_login)

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "loginButton"))
)
print("ログイン画面遷移成功")

# 入力
e = browser.find_element_by_id("u")
e.clear()
e.send_keys(USER)
e = browser.find_element_by_id("p")
e.clear()
e.send_keys(PASS)

# ログイン
button = browser.find_element_by_id("loginButton")
button.click()

# 第2パスワード入力画面
# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "indexForm:password"))
)
print("第2パスワード入力画面遷移成功")

# 第2パスワード入力
e = browser.find_element_by_id("indexForm:password")
e.clear()
e.send_keys(PASS2)

# ログイン
button = browser.find_element_by_name("indexForm:j_idt78")
button.click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.CLASS_NAME, "rf-font-bold"))
)
print("TOP画面遷移成功")

# ログインできたか確認（画面キャプチャ取る）
# browser.save_screenshot("../screenshots/card_rakuten/home.png")

# お支払い（ご利用明細）画面まで遷移
# browser.find_element_by_xpath("//dt[contains(.,'ご利用明細')]").click()
# URL指定で遷移できる場合は、そっちのほうが確実
browser.get("https://www.rakuten-card.co.jp/e-navi/members/statement/index.xhtml")

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "js-payment-calendar-btn"))
)
print("お支払い（ご利用明細）画面遷移成功")

# ダウンロード対象期間指定
# ほかの月を見る
# 一度期間指定したあとに、別の月を見たい時はこのボタンクリックから再開
browser.find_element_by_id("js-payment-calendar-btn").click()

# 前年のカレンダー表示させる場合
# browser.find_element_by_id("js-payment-calendar-prev").click()
# 翌年のカレンダー表示させる場合
# browser.find_element_by_id("js-payment-calendar-next").click()

# 念の為スリープ
time.sleep(1)

# 対象月指定
# リンクテキストは操作年の月と同じ
browser.find_element_by_link_text("3").click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.CLASS_NAME, "payment-amount-other-ttl"))
)
print("明細表示成功")

# ウィンドウスクロール
# ヘッドレスモードだと、CSVダウンロードボタンがオーバーレイ広告と重なってしまい
# エラーが発生するため、ウィンドスクロールさせて表示位置をずらす
browser.execute_script("window.scrollTo(0, 300);")

# CSVダウンロード実行
browser.find_element_by_link_text("CSV").click()

# ログアウト画面へ遷移
browser.find_element_by_link_text("ログアウト").click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.XPATH, "//img[@alt='ログアウト']"))
)
print("ログアウト画面遷移成功")

# ログアウト
browser.find_element_by_xpath("//img[@alt='ログアウト']").click()

# 処理終了
browser.quit()
```