Python＋Selenium＋BeautifulSoupでSBIベネフィットシステムズの資産状況データを解析しCSVに書き出す

PythonとSeleniumとBeautifulSoupを使って、SBIベネフィットシステムズのサイトにログインして資産状況データを取得（CSVダウンロード）する方法を試してみました。

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

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

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


## はじめに
ログインまでの部分については下記記事を参考にしてみてください。

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

なお、上記記事ではインポートしていないライブラリがいくつかあります。以下もインポート必要になりますので追加しておきましょう。

```python
import os
import csv
from bs4 import BeautifulSoup
```

### CSVダウンロード機能がない！

これまで、複数の金融機関サイトで明細データをダウンロードするサンプルコードを書いてきました。大抵はCSVダウンロード機能を提供しているので、Seleniumでダウンロードボタンのクリック操作を再現させるだけで簡単にダウンロードできます。

しかし、残念ながらSBIベネフィットシステムズのサイトでは、ダウンロードする機能がありませんでした。。

### BeautifulSoupでHTMLタグを解析する

そこで、BeautifulSoupというHTMLを解析することができるスクレイピング用のライブラリを使って欲しいデータを取得することにします。

## 対象画面まで遷移する
まず、SBIベネフィットシステムズのログイン後TOP画面から、資産状況画面まで遷移させてみましょう。

◆TOP画面

|![20210427-002-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/a7013f74-60dd-f14b-49b0-c413606ec21b.png)|
| :--|

画面上部にあるメニューの中から「資産状況」リンクをクリックさせることで遷移できます。

```python
# 資産状況画面遷移
browser.find_element_by_id("D_Header1_uscD_CategoryMenu1_rptFunctionMenuTable__ctl2_btnMenuData").click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "palsyouhinbetu"))
)
print("資産状況画面遷移成功")
```

◆資産状況画面

遷移すると、資産状況欄に保有している商品の一覧が表示されます。商品一覧はtableタグで構築されているので、今回はこのtable要素を解析してデータを取得します。

|![20210427-001-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459893/95cf9757-a358-d361-b079-3fd797b1ead1.png)|
| :--|

## table要素の解析方法

### table要素を抜き出す

BeautifulSoupを使って要素解析するには、HTMLソースを取得する必要があります。BeautifulSoupの参考書等ではRequestsというライブラリを使って取得するのが一般的のようですが、今回は既にSeleniumを使って画面遷移していますので、SeleniumのWebDriverから取得します。WebDriverのpage_source（）メソッドを使えばHTMLソースを取得できます。

```python
# 資産一覧テーブル取得
html = browser.page_source
soup = BeautifulSoup(html, "html.parser")
```

HTMLソースを取得したら、今度はお目当てのtable要素を取得します。tableタグにはidが割り当てられているので、BeautifulSoupのfind()メソッドの引数にidを指定することで取得可能です。

```python
table = soup.find(id="grdSyouhinzangaku")
tr_list = table.find_all("tr")
```
また、商品は各行（trタグ）に格納されているので、trタグの一覧をfind_all()メソッドを使って取得しておきます。

### tableの構成を確認する

ここでひとまず、取得したtable要素の全体像を確認しておきましょう。
（※下記サンプルの金額はテキトーに加工した値です）

```html
<table cellspacing="0" cellpadding="4" rules="all" bordercolor="#CCCCCC" border="2" id="grdSyouhinzangaku" style="border-color:#CCCCCC;border-width:2px;border-style:solid;border-collapse:collapse;">
<tbody>

    <tr class="tableHeader" align="center" valign="middle">
      <td>商品タイプ</td>
      <td><span class="prod-formal">運用商品名</span><span class="f">（略称）</span></td>
      <td>時価単価<br><span class="unit-txt">(1万口当り)</span></td>
      <td>残高数量</td>
      <td>資産残高</td>
      <td>購入金額</td>
      <td>損益<br>損益率</td>
      <td>&nbsp;</td>
    </tr>

    <tr class="even-row">
      <td align="center" style="width:100px;">
        <label class="balanceIcon" style="BACKGROUND-COLOR:#ff99cc">
          国内株式
        </label>
      </td>
      <td align="left" style="width:225px;">
         <a href="javascript:subSyohinNew('../Knowledge/JP_D_Knowledge_InvestTrust_Inquiry.aspx?SCD=9200109227&amp;CCD=01')">
         <span class="prod-formal">ｅＭＡＸＩＳ　Ｓｌｉｍ　国内株式（ＴＯＰＩＸ）</span>
         <span class="prod-abbrv">（eMAXIS Slim 国内株式(TOPIX)）</span>
         </a>
      </td>
      <td align="right" style="width:79px;">13,576<span class="unit">円</span></td>
      <td align="right" style="width:89px;">129,790<span class="unit">口</span></td>
      <td align="right" style="width:79px;">176,202<span class="unit">円</span></td>
      <td align="right" style="width:79px;">132,538<span class="unit">円</span></td>
      <td align="right" style="width:79px;">43,664<span class="unit">円</span><br>32.9<span class="unit">％</span></td>
      <td align="right" style="width:79px;"></td>
    </tr>

    <tr class="odd-row">
      <td align="center" style="width:100px;">
        <label class="balanceIcon" style="BACKGROUND-COLOR:#ff00ff">内外株式</label>
      </td>
      <td align="left" style="width:225px;">
         <a href="javascript:subSyohinNew('../Knowledge/JP_D_Knowledge_InvestTrust_Inquiry.aspx?SCD=9200196387&amp;CCD=01')">
         <span class="prod-formal">ひふみ年金</span>
         <span class="prod-abbrv">（ひふみ年金）</span>
         </a>
      </td>
      <td align="right" style="width:79px;">18,729<span class="unit">円</span></td>
      <td align="right" style="width:89px;">96,108<span class="unit">口</span></td>
      <td align="right" style="width:79px;">180,000<span class="unit">円</span></td>
      <td align="right" style="width:79px;">132,538<span class="unit">円</span></td>
      <td align="right" style="width:79px;">47,462<span class="unit">円</span><br>35.8<span class="unit">％</span></td>
      <td align="right" style="width:79px;"></td>
    </tr>

    ＜＜＜　以下略　＞＞＞

</tbody>
</table>
```

解析にあたってポイントとなる点を列挙しました。

- 「商品名」と「略称」がひとつのtdタグ内に存在している　→　「商品名」だけ抜き出したい
- タイトル行のうち、「損益」と「損益率」がひとつのtdタグ内に存在している　→　列を分割して保存したい
- 金額や口数は単位がついている　→　単位は削除したい（損益率だけは逆に残しておいたほうがわかりやすそう）

このように、各データを微妙に加工しながら取り込む必要があります。

### 1行ずつ解析しながらCSV出力用データを作成

では、先ほど最後に取得したtr_listを１行ずつ読み込みながらCSV出力用のデータを作っていきましょう。

```python
# CSV出力用データ
data = []

for i, tr in enumerate(tr_list):
    row_data = []

    if i == 0:
        # ヘッダー行の処理
        # 損益と損益率を列分割するので自前でヘッダー行を用意する
        row_data.extend(["商品タイプ", "運用商品名", "時価単価", "残高数量", "資産残高", "購入金額", "損益", "損益率（％)"])
        data.append(row_data)
        continue

    # td要素取得
    td_list = tr.find_all("td")

    # 商品タイプ
    row_data.append(td_list[0].text.strip())

    # 運用商品名（略称は削除）
    row_data.append(td_list[1].find(class_="prod-formal").text.strip())

    # 時価単価（単位は削除）
    td_txt = td_list[2].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 残高数量（単位は削除）
    td_txt = td_list[3].text.strip()
    row_data.append(td_txt[:td_txt.find('口')])

    # 資産残高（単位は削除）
    td_txt = td_list[4].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 購入金額（単位は削除）
    td_txt = td_list[5].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 損益（単位は削除）、損益率
    td_txt = td_list[6].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])  # 円の前
    row_data.append(td_txt[td_txt.find('円') + 1:])  # 円の後

    data.append(row_data)
```

ヘッダー行のみ、if文で分岐させてデータ作成しています。

あとはtrの要素からtd要素を取得しtd_listに格納します。そして添字で1要素ずつ指定しながら必要な加工・修正を加えていきます。

## CSVファイルに書き出し

最後に、作成したデータをCSVファイルに書き出します。書き出したら忘れずにclose()しておきましょう。

```python
# ダウンロード先＆ファイル名
# パス、ファイル名は任意な値に変更してください
output_file_name = "../dl-data/result_sbi.csv"

if os.path.exists(output_file_name):
    # ファイルが存在する時は削除
    os.remove(output_file_name)

# CSVファイルに書き出し
with open(output_file_name, 'w') as file:
    w = csv.writer(file, lineterminator='\n')
    w.writerows(data)
    file.close()
```

以上、Python＋Selenium＋BeautifulSoupを使ってSBIベネフィットシステムズの資産状況データを解析しCSVに書き出す方法でした！

## サンプルコード全文

```python
#
# SBIベネフィットシステムズサイトへログインして資産状況を取得
#

import os
import csv
import json

from bs4 import BeautifulSoup
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 = "ideco_sbi"

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

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

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

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

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

# 入力
e = browser.find_element_by_id("txtUserID")
e.clear()
e.send_keys(USER)
e = browser.find_element_by_id("txtPassword")
e.clear()
e.send_keys(PASS)

# ログイン
browser.find_element_by_id("btnLogin").click()

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

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

# 資産状況画面遷移
browser.find_element_by_id("D_Header1_uscD_CategoryMenu1_rptFunctionMenuTable__ctl2_btnMenuData").click()

# ページロード完了まで待機
WebDriverWait(browser, 10).until(
    ec.presence_of_element_located((By.ID, "palsyouhinbetu"))
)
print("資産状況画面遷移成功")

# 資産一覧テーブル取得
html = browser.page_source
soup = BeautifulSoup(html, "html.parser")

table = soup.find(id="grdSyouhinzangaku")
tr_list = table.find_all("tr")

# CSV出力用データ
data = []

for i, tr in enumerate(tr_list):
    row_data = []

    if i == 0:
        # ヘッダー行の処理
        # 損益と損益率を列分割するので自前でヘッダー行を用意する
        row_data.extend(["商品タイプ", "運用商品名", "時価単価", "残高数量", "資産残高", "購入金額", "損益", "損益率（％)"])
        data.append(row_data)
        continue

    # td要素取得
    td_list = tr.find_all("td")

    # 商品タイプ
    row_data.append(td_list[0].text.strip())

    # 運用商品名（略称は削除）
    row_data.append(td_list[1].find(class_="prod-formal").text.strip())

    # 時価単価（単位は削除）
    td_txt = td_list[2].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 残高数量（単位は削除）
    td_txt = td_list[3].text.strip()
    row_data.append(td_txt[:td_txt.find('口')])

    # 資産残高（単位は削除）
    td_txt = td_list[4].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 購入金額（単位は削除）
    td_txt = td_list[5].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])

    # 損益（単位は削除）、損益率
    td_txt = td_list[6].text.strip()
    row_data.append(td_txt[:td_txt.find('円')])  # 円の前
    row_data.append(td_txt[td_txt.find('円') + 1:])  # 円の後

    data.append(row_data)

# ダウンロード先＆ファイル名
output_file_name = "../dl-data/result_sbi.csv"

if os.path.exists(output_file_name):
    # ファイルが存在する時は削除
    os.remove(output_file_name)

# CSVファイルに書き出し
with open(output_file_name, 'w') as file:
    w = csv.writer(file, lineterminator='\n')
    w.writerows(data)
    file.close()

# 処理終了
browser.quit()

print("=== All done! ===")

```