Python＋SeleniumでNEOBANK（住信SBIネット銀行）の明細取得（CSVダウンロード）

PythonとSeleniumを使って、NEOBANK（住信SBIネット銀行）サイトにログインして明細データを取得（CSVダウンロード）する方法を試してみました。

ただ、結論を先に言うとあまりうまくいきませんでした・・・。一応ダウンロードはできたんですが、イマイチな動きをしています。

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

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

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


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

https://qiita.com/yoshi2045/items/299858da6f0e23de056b

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

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

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


## NEOBANKの画面は特殊・・・
これまで楽天銀行や三菱UFJ銀行のCSVダウンロードを試してきましたが、それと比べるとNEOBANKのサイトは特殊です。。。

ちなみにサイト構築にはAngularを使ってるっぽいんですが、そのせいなのかはよく分かってません。

以下、つまづいた点と回避策です。

### IDやクラス名などで特定することがほぼできない

単純に使い勝手が独特過ぎて分かりにくいのと、タグの要素も単純にはいきませんでした。例えば他サイトならタグ要素を確定するためにIDやクラス名などで特定できることが多いのですが、NEOBANKの場合ほとんどそれが不可能で、代わりにXPATHかCSSセレクター指定で特定せざるを得ませんでした。

XPATHやCSSセレクターの場合、単純にコードが読みにくくなるのとサイトのちょっとしたデザイン変更が発生した場合うまく動作しなくなるリスクが高くなるんですよね。。

### WebDriverWait()による画面表示待機がうまくいかない時がある

```python
# ページロード完了まで待機
# ID指定で要素特定する例
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "IDの名前を指定"))
)
```

そのため強制的にtime.sleep()でスリープさせることでエラー回避しました。

```python
time.sleep(1)
```

## 対象画面まで遷移させる


まず、NEOBANK（住信SBIネット銀行）のログイン後のTOP画面から明細ダウンロード画面までどういう経路で遷移できるかを確認してみます。

＜画面フロー＞

1. TOP画面
2. 入出金明細画面 ← ここでCSVダウンロード

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


◆ログイン後のTOP画面

![20210413-001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/d87b3007-6e84-877e-804b-3a8dff79f441.png)

TOP画面から入出金明細画面への遷移は、画面上部にある「入出金明細」をクリックします。こういったヘッダーのメニュー要素でもIDや使えそうなclass指定がなかったので、XPATH指定で要素を特定しました。

```python
# 入出金明細画面へ遷移
browser.find_element_by_css_selector(".m-icon-ps_details").click()
```

◆入出金明細画面
![20210413-002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/d252628f-ccb4-8731-236c-b5c89125ae23.png)

入出金明細画面の中央部分に、条件を指定するエリアがあるので、ここをSeleniumを使って値を設定していきます。

なお、私の口座の場合「代表口座」と「SBIハイブリッド預金」の2つを登録していますが、ここは皆さんの利用状況次第で変わる部分だと思います。

![20210413-003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/32265b70-afee-847c-608f-3fd499280539.png)

表示口座の指定方法はつぎのとおりです。

```python
# 表示口座切り替え（代表口座の場合）
browser.find_element_by_css_selector(".ng-tns-c4-6 .ui-selectmenu-text").click()
browser.find_element_by_xpath("//li[@value='0']").click()


# 表示口座切り替え（ハイブリッド口座）
browser.find_element_by_css_selector(".ng-tns-c4-6 .ui-selectmenu-text").click()
browser.find_element_by_xpath("//li[@value='1']").click()
```

find_element_by_xpath()の引数部分で、valueの値が違っています。

## 期間を指定する

![20210413-004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/3be44450-5d6e-4e8e-02a5-dffee8743123.png)

続いて期間を指定してきます。今回は開始日と終了日を特定したかったのでラジオボタンで「期間指定」を選択し、日付も設定してきます。

```python
# 期間を設定
browser.find_element_by_xpath("//li[5]/label").click()

# 開始年月日
browser.find_element_by_xpath("//p[1]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'2021年')]").click()
browser.find_element_by_xpath("//p[2]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'2月')]").click()
browser.find_element_by_xpath("//p[3]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'1日')]").click()

# 終了年
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p/nb-simple-select/span/span[2]").click()
# li要素の9個目が、操作中の年（最新の年）とみなすしかなさそう
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p/nb-simple-select/div/ul/li[9]").click()

# 終了月
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[2]/nb-simple-select/span/span[2]").click()
# li[1]は"--月"で、li[2]が1月、li[3]が2月・・・
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[2]/nb-simple-select/div/ul/li[3]").click()

# 終了日
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[3]/nb-simple-select/span/span[2]").click()

# li[1]は"--月日で、li[2]が1日、li[3]が2日・・・
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[3]/nb-simple-select/div/ul/li[2]").click()

```

コードを見ていただくと分かるように、開始年月日と終了年月日の指定方法が違っています。なぜか同じ方法だと上手くいきませんでした。試行錯誤の結果、原因特定できなかったため上記コードのような形になっています。また、xpath指定だと設定している年月日が直感的に分かりにくいですね。

### 表示ボタンをクリックし、CSVダウンロードボタンで実行

期間を設定したら、「表示」ボタンをクリックして明細を表示させます。

```python
# 明細表示
browser.find_element_by_link_text("表示").click()
```
ここではリンクテキストを元に指定してみました。

「表示ボタン」をクリックすると、明細行が表示されます。

![20210413-005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/eb77404f-f072-9b11-0d27-6e73c1ab20c7.png)

あとは「CSVダウンロード」ボタンをクリックすればダウンロード可能になりますが、仮に明細行がない場合は以下のようなエラーメッセージが表示され、「CSVダウンロード」ボタンは表示されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/c033e28a-12b1-d2b2-74ec-e3690353a35a.png)

このような場合、CSVダウンロードできるかの判断をしてからダウンロード実行するべきなのですが、今回は手を抜いてtry-exceptで囲んで対処しました。

```python
# CSVダウンロード
try:
    browser.find_element_by_link_text("CSVでダウンロード").click()
except Exception as e:
    print("代表口座CSVダウンロードで例外発生")
```

以上、PythonとSeleniumを使ってNEOBANK（住信SBIネット銀行）の明細取得（CSVダウンロード）する方法でした！いやー大変だった・・・

## サンプルコード全文

```python
#
# NEOBANK（住信SBIネット銀行）サイトへログイン
#

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 = "bank_neo"

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

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

# ダウンロードフォルダ
# 相対パス指定はできない模様
# 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)

# 入力
e = browser.find_element_by_id("userName")
e.clear()
e.send_keys(USER)
e = browser.find_element_by_id("loginPwdSet")
e.clear()
e.send_keys(PASS)

# ログイン
button = browser.find_element_by_css_selector(".m-btnEm-l > span")
button.click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.CLASS_NAME, "top-name"))
)

# 上記だけだと上手く行かないケースがあるので、念の為スリープ入れる
time.sleep(3)

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

# 入出金明細画面へ遷移
browser.find_element_by_css_selector(".m-icon-ps_details").click()

time.sleep(1)

# 表示口座切り替え（代表口座）
browser.find_element_by_css_selector(".ng-tns-c4-6 .ui-selectmenu-text").click()
browser.find_element_by_xpath("//li[@value='0']").click()

# 期間を設定
browser.find_element_by_xpath("//li[5]/label").click()

# 開始年月日
browser.find_element_by_xpath("//p[1]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'2021年')]").click()
browser.find_element_by_xpath("//p[2]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'2月')]").click()
browser.find_element_by_xpath("//p[3]/nb-simple-select/span/span[2]").click()
browser.find_element_by_xpath("//li[contains(.,'1日')]").click()

# 終了年
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p/nb-simple-select/span/span[2]").click()
# li要素の9個目が、操作中の年（最新の年）とみなすしかなさそう
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p/nb-simple-select/div/ul/li[9]").click()

# 終了月
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[2]/nb-simple-select/span/span[2]").click()
# li[1]は"--月"で、li[2]が1月、li[3]が2月・・・
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[2]/nb-simple-select/div/ul/li[3]").click()

# 終了日
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[3]/nb-simple-select/span/span[2]").click()

# li[1]は"--月日で、li[2]が1日、li[3]が2日・・・
browser.find_element_by_xpath("//div[2]/div[2]/nb-select-ymd/div[2]/div/p[3]/nb-simple-select/div/ul/li[2]").click()

# 明細表示
browser.find_element_by_link_text("表示").click()

time.sleep(1)

# CSVダウンロード
try:
    browser.find_element_by_link_text("CSVでダウンロード").click()
except Exception as e:
    print("代表口座CSVダウンロードで例外発生")

# 表示口座切り替え（ハイブリッド口座）
browser.find_element_by_css_selector(".ng-tns-c4-6 .ui-selectmenu-text").click()
browser.find_element_by_xpath("//li[@value='1']").click()

# 明細表示
browser.find_element_by_link_text("表示").click()

# CSVダウンロード
try:
    browser.find_element_by_link_text("CSVでダウンロード").click()
except Exception as e:
    print("ハイブリッド口座CSVダウンロードで例外発生")

# ログアウト
# デバッグモードではなく通常実行モードだとなぜかエラーになる
# browser.find_element_by_class_name("m-icon-cm_logout").click()

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