SBI証券から保有資産を取得する為Pythonでスクレイピングしてみる

プログラミング

SBI証券の保有資産をデータとして使いたいなと思うところがありまして、ちゃちゃっとpythonで取得コードを書いてみました。

しかし、私はノンプログラマな上に、プログラミングなんて数年ぶりなので小一時間程今の環境を調査することに。

結果以下のことがわかりました。

  • HTTPの処理をするのに、今時のPythonはrequestsモジュールを使うのがシャレオツ
  • HTMLを解析(パース)するにはBeautifulSoup4を使うのが楽

というわけで、Pythonの使い方を思い出しながら探り探り作ってみた。リストの操作すら忘れてた人間が突貫工事で書いてます…笑

# -*- coding: utf-8 -*-
"""
SBI証券から保有資産をとってきますん(∩´∀`)∩
"""

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from collections import defaultdict
import json
import io


class SbiSec:

    def __init__(self, user_id, user_password):
        self.url_list = {
                'base_url': "https://site2.sbisec.co.jp/",
                'login_url': "https://site2.sbisec.co.jp/ETGate/"
        }
        self.login(user_id, user_password)

    def login(self, user_id, user_password):
        # 以下内容はログインページのhtmlから直接把握
        payload = {
                'JS_FLG': "0",
                'BW_FLG': "0",
                "_ControlID": "WPLETlgR001Control",
                "_DataStoreID": "DSWPLETlgR001Control",
                "_PageID": "WPLETlgR001Rlgn20",
                "_ActionID": "login",
                "getFlg": "on",
                "allPrmFlg": "on",
                "_ReturnPageInfo": "WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control",
                "user_id": user_id,
                "user_password": user_password
        }
        self.session = requests.session()
        res = self.session.post(self.url_list['login_url'], data=payload)
        res.raise_for_status()  # エラーの時に例外を発生させる

    def logout(self):
        self.session.close()

    def get_kouzakanri_url(self):
        """口座管理画面のURLはユーザーによらず一意だとは思いますが、それをしっかり確認していないので、
        念のため、ログイン後のトップ画面右上の「口座管理」ボタンからURLを取得することにしました。
        """
        if 'kouzakanri_url' in self.url_list:
            kouzakanri_url = self.url_list['kouzakanri_url']
        else:
            res = self.session.get(self.url_list['base_url'])
            soup = BeautifulSoup(res.text, "html.parser")

            # SBI証券トップページの右上の「口座管理」ボタンから相対URLを取得
            relative_path = soup.select_one("#link02M > ul > li:nth-of-type(3) > a").attrs["href"]
            kouzakanri_url = urljoin(self.url_list['base_url'], relative_path)
            self.url_list['kouzakanri_url'] = kouzakanri_url

        return kouzakanri_url

    def get_kouzakanri_html(self):
        res = self.session.get(self.get_kouzakanri_url())
        res.encoding = "cp932"  # 僕のPCはWindowsヽ(´エ`)ノ
        return res.text

    def get_kouzakanri_data(self):
        html = self.get_kouzakanri_html()
        soup = BeautifulSoup(html, "html.parser")

        tables = soup.find_all('table', width="300")

        # SBI証券の口座管理画面の各テーブル名を表す
        SBI_ACCOUNT_TYPE = dict(
            KABUSHIKI_TOKUTEI = 3,  # 株式(現物/特定預り)の箇所のテーブル
            KABUSHIKI_NISA = 4,     # 株式(現物/NISA預り
            TOUSHIN_TOKIUTEI = 5,   # 投資信託(金額/特定預り)
            TOUSHIN_NISA = 6        # 投資信託(金額/NISA預り)
            # 国内債券等は、他とテーブル構造が違っていた上、私は国債しか買っておらず興味がないので無視。
        )
        portfolio = defaultdict(lambda: defaultdict(dict))  # 3次元辞書を操作しやすくする為
        # Pythonで加工しやすくする為、一先ずリストに格納した
        for i in SBI_ACCOUNT_TYPE.values():
            rows = tables[i].find_all('tr')
            data = []
            for row in rows:
                cols = row.find_all('td')
                cols = [ele.text.strip() for ele in cols]
                data.append([ele for ele in cols if ele])

            """
            dataは以下のような二重リスト。SBI証券のポートフォリオのtalbe構造と対応している。
                [[株式(現物/特定預り)],                           0行目:口座種別(1列)
                 ['保有株数', '取得単価', '現在値', '評価損益'],  1行目:ヘッダー(4列)
                 ['ISエマージング株', '現買 現売'],                 2行目(偶数行目):投資商品名(2列)
                 [100, 4663, 5000, 33700]                       3行目(奇数行目):データ (4列)
                 ['別商品', ....],                              別商品があれば偶数/奇数行目を以下繰り返し
                 [200, ....]]
            """

            table_name = data[0][0]
            header_row = data[1]
            body_rows = data[2:]
            for num in range(int(len(body_rows)/2)):
                toshi_item = body_rows[num*2][0]
                toshi_data_row = body_rows[num*2 + 1]
                portfolio[table_name][num] = dict(zip(header_row, toshi_data_row))
                portfolio[table_name][num]['投資商品'] = toshi_item

        return portfolio


if __name__ == '__main__':
    sbiobj = SbiSec(user_id="********", user_password="************")
    data = sbiobj.get_kouzakanri_data()
    sbiobj.logout()

    with io.open('filename.json', 'w', encoding='utf-8') as json_file:
        json.dump(data, json_file, indent=2, ensure_ascii=False)

書いているうちに長くなりすぎたので、色々と関数化することに。

関数化してるうちに色々やりたくなって、sessionを関数で渡すの面倒くさいなってなって、self.sessionでセッション確保したかったというだけの理由でクラス化してます笑

SBI証券にログインする為に必要な情報

ログインの際に何をPOSTとして送ればよいのか調べるため、SBI証券のソースコードを読んでみました。結果、パスワード入力の箇所は以下のようなフォームになっておりました。

<form class="margin-4" action="/ETGate/" method="post" name="form_login">
<input id="JS_FLG" name="JS_FLG" type="hidden" value="0" />
<input id="BW_FLG" name="BW_FLG" type="hidden" value="0" />
<input name="_ControlID" type="hidden" value="WPLETlgR001Control" />
<input name="_DataStoreID" type="hidden" value="DSWPLETlgR001Control" />
<input name="_PageID" type="hidden" value="WPLETlgR001Rlgn20" />
<input name="_ActionID" type="hidden" value="login" />
<input name="getFlg" type="hidden" value="on" />
<input name="allPrmFlg" type="hidden" value="on" />
<input name="_ReturnPageInfo" type="hidden" value="WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control" />
(省略)
</form>

この情報をCookieとして送信すればよいはず…ですね!

inputタグがhiddenとなっていて画面に表示はされていませんが、諸々送っているみたいですね!

そして、ログインID/パスワードの箇所は以下のコードになっていました。

<dl>
 	<dt>ユーザーネーム</dt>
</dl>

<dl>
 	<dd>
<div id="user_input"><input name="user_id" type="text" value="" /></div></dd>
</dl>

<dl>
 	<dt>パスワード</dt>
</dl>

<dl>
 	<dd>
<div id="password_input"><input maxlength="10" name="user_password" type="password" value="" /></div></dd>
</dl>

name="user_id"にログインIDが入り、name="user_password"にパスワードが入るみたいですね。

 

そのため、pythonで以下の通りPOSTの為のデータを設定しました。

payload = {
                'JS_FLG': "0",
                'BW_FLG': "0",
                "_ControlID": "WPLETlgR001Control",
                "_DataStoreID": "DSWPLETlgR001Control",
                "_PageID": "WPLETlgR001Rlgn20",
                "_ActionID": "login",
                "getFlg": "on",
                "allPrmFlg": "on",
                "_ReturnPageInfo": "WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control",
                "user_id": user_id,
                "user_password": user_password
        }

user_iduser_passwordは、当然、ご自身のユーザーIDとパスワードですね!

もう少ししっかりHTMLソースを読むと、色々とjavascriptがごちゃごちゃと書かれておりまして、JS_FLGはjavascriptを入れてたら1になる感じでした。初期値0のままでいいかな。

BW_FLGはjavascriptでnavigator.userAgentを取ってきて、どのブラウザからアクセスしたのか取得していました。これも初期値で0のままで行きます。

試しに、ChromeのディベロッパーツールからRequestした情報を追ってみたところ(ネットワーク機能から[Form Data]を参照した)、JS_FLGは1, BW_FLGは”chrome, 55″が入っていました… が、まぁどうでもいいです笑

そして、これらのデータをどこにPOSTすればよいのかですが、

と記載がある通り、formの内容は/ETGate/に送信されているみたいでした。

 

というわけで、これら情報を/ETGate/に送信したところ… 無事ログイン成功!

get_kouzakanri_url() : 口座管理画面のURLの取得

口座管理画面のURLをプログラムから取得することにしました。ココです!

普通にURLを直接貼って問題ないと思うのですが、URLがやたら複雑で「あれ?人によってURL違う…?」なんて懸念が頭をよぎり、一応、スクレイピングでaタグのhref属性をとることにしました。URLはただひたすらにページをクエリストリングでつないでいるように見えるので、さすがに皆同じURLだとは思うんですけどね。

Chromeのディベロッパーツールで取得したCSSセレクタを直接Beautifulsoupに書いてます。

しかし、BeautifulSoupはnth-childが使えないらしいので、そこをnth-of-typeに修正だけした。

relative_path = soup.select_one("#link02M > ul > li:nth-of-type(3) > a").attrs["href"]

get_kouzakanri_data() : 保有資産データの取得

SBI証券のこの右側のデータを引っ張ってきてます。

この右側のテーブルはwidth=”300″で指定すれば引っ張ってこられそうだったので、それをBeautifulsoupでとってきました。

一応モザイクかけてみたけど、モザイクかけすぎて株式の評価損益がなんか真っ青にみえますね笑(SBI証券の画面は、青が赤字、赤が黒字です笑)

table[3]以降にデータが入ってる感じ。

各行はこんな感じのデータが入ってます。

ひふみプラスを買ったことは以前公開しましたので、ひふみプラスをキャプチャ(ちなみに、今年の損益通算の調整用に少し売ろうかなと考え中)。

0行0列目に投資した商品の名前が入ってる。0行1列目は「買付 売却」という意味のないデータが入ってる(この行はtdタグが2つしかなく、データが2列しかない)。

そして1行目にはデータが4列分ちゃんと入ってる。

という構造。

レイアウト目的にテーブルを使っているのでやりにくいですね。

プログラムちまちま変えるかもしれません

突貫工事で作ったので色々アレなプログラムなので時間のある時に変えるかも。

SBI証券のテーブルも人によって違うはずなので、関数の中にテーブル定義を埋め込むよりは、自分で欲しいテーブルを指定するようにしたほうが使い勝手が良さそうですしね…!

あと、せっかくなのでpythonのmatplotlibを使ってアセットアロケーションのグラフを描きたいなと思ったので、CSV等、もう少し扱いやすいデータ形式に変えると思いまする。

全ては時間があるときに…。あぁ… 久々にプログラム書くと、プログラミングを復習したくなってきちゃいますね。


最上部へ戻る