こんにちは。TATです。
今日のテーマは「【Python】SeleniumでSBI証券から日本株の損益履歴を収集する方法」です。
PythonとSeleniumを使って、SBI証券のサイトから日本株の損益を収集する操作を自動化してみます。
過去の記事では約定履歴を収集する方法について解説しました。
今回の記事では損益の収集です。
このデータを集計すれば、月ごとの勝率や平均利益率、損切り回数などいろいろな分析に活用することができます。
目次
SBI証券へのログイン
事前準備として、PythonとSeleniumでSBI証券にログインしておく必要があります。
Seleniumの準備やSBI証券へログインする方法についてはこちらの記事で解説しています。
ログインが完了したら、本記事で紹介するコードを実行することができます。
-  

 【Python】SeleniumでSBI証券に自動ログインする方法【自動売買への道】
続きを見る
日本株の損益履歴(譲渡益税)を収集するまでの流れ
次にPythonとSeleniumでSBI証券のサイトから日本株の損益履歴を収集するまでの流れについて確認します。
ざっくりこんな感じかと思います。
日本株を売買するまでの流れ
- (SeleniumでSBI証券にログイン)
 - 「口座管理」→「取引履歴」→「譲渡益税明細」ページへ遷移する
 - 条件を選択する
 - 「照会」ボタンをクリックする
 - 結果を収集する
 
1についてはこちらの記事で解説している内容になるので、()書きにしました。
本記事では1はスキップして、2〜5を実装する方法を解説していきます。
PythonとSeleniumでSBI証券サイトから日本株の損益履歴収集を実装する
それでは2~5の流れを順番に解説していきます。
「口座管理」→「取引履歴」→「譲渡益税明細」ページへ遷移する
まずは譲渡益税のページに遷移します。
SBI証券にログインしてから、「口座管理」→「取引履歴」→ 「譲渡益税明細」とクリックすればOKです。

順番にHTMLの要素を確認してみます。
「口座管理」についてはtitle="口座管理"となっています。

「取引履歴」については、titleやaltなどの要素はありませんが、aタグで文字列が"取引履歴"となっています。

最後に、「譲渡益税明細」です。こちらもaタグで文字列が"譲渡益税明細"となっています。

これらの要素を利用してSeleniumでページ遷移していきます。
# 口座管理ページへ遷移
driver.find_element_by_css_selector("img[title=口座管理]").click()
driver.implicitly_wait(60)
# 取引履歴ページへ遷移
driver.find_element_by_link_text("取引履歴").click()
driver.implicitly_wait(60)
# 譲渡益税明細ページへ遷移
driver.find_element_by_link_text("譲渡益税明細").click()
driver.implicitly_wait(60)
データの読み込み時間を考慮して、driver.implicitly_wait(60)を加えました。
これは次の処理を行うにあたり、対象となる要素が見つかるまで待機するという命令になります。
これがないとページが読み込まれる前に値を入力しようしたり次の処理を行おうとしてエラーが発生する場合があるので、処理に時間がかかる箇所に追加しておくとプログラムの安定感が増します。
これで譲渡益税のページへ遷移することができます。

条件を選択する
次に条件の選択です。

譲渡益税明細のページでは次の条件を指定することができます。
ポイント
- 受渡日
 - 1ページあたりの表示件数
 
全てドロップダウンメニューになっていますね。
それぞれの要素をチェックすると、全てにnameが付与されていることが確認できます。
こちらは受渡日です。

1ページあたりの表示件数についてもname="max_cnt"となっています。

これらのnameを利用してドロップダウンの値を選択していきます。
やり方は全て同じですので、ここでは1ページあたりの表示件数を例にとって解説していきます。
Seleniumでドロップダウンメニューを選択するには、selenium.webdriver.support.select.Selectを使います。
# 表示件数
max_count_div = driver.find_element_by_name("max_cnt")
max_count_options = Select(max_count_div)
# 値の取得 ついでにintに変換
max_count_value_list = [int(c.get_attribute("value")) for c in max_count_options.options]
こんな感じでSelectを使います。対象の要素を取得してSelectに引き渡せばOKです。
ここから選択できる値を取得することもできます。文字列になっているのでついでにintに変換しました。

値を選択する際には、値を直接指定することもできますし、indexで指定することもできます。
値を直接指定する場合は該当する値がない場合はエラーになってしまうのでindexで選択する方が無難です。
先ほど作成したmax_count_value_listから任意の値を検索してそのindexを利用することもできます。
この場合intで指定できるので使い勝手が良くなります。
次のコードでは全て100件を選択しています。
# indexで指定
max_count_options.select_by_index(1)
# 値で指定
max_count_options.select_by_value("100")
# max_count_value_listを利用してindexを指定
max_count_options.select_by_index(max_count_value_list.index(100))
どれを使っても結果は変わらないのでお好きなものを選んでいただければOKです。

受渡日についても同様のやり方で設定できます。
全ての項目を網羅したコードは最後に共有します。
ここでは一旦次に進みます。
「照会」ボタンをクリックする
条件の設定が完了したら「照会」ボタンをクリックします。
要素を確認すると、name="ACT_search"となっているのでこれを利用します。

Seleniumで実装すると次のようになります。
# 「照会」をクリック
driver.find_element_by_name("ACT_search").click()
これで譲渡益税明細が表示されます。

僕の損益情報がダダ漏れですが、こんな感じで表示されていたらOKです。
結果を収集する
最後に表示結果を収集します。
データの収集
テーブル情報を取得して、各行のデータを収集していきます。
今回の場合、海外株の情報も混じっています。
よって、一旦全て収集してから最終的に日本株のデータだけに絞っていきます。
そしてこのプロセスを全て解説していくと長くなってしまうので、ここではコードをまとめて紹介します。
なるべく多くのコメントをつけました。
さらに、データが多いと複数ページになります。
「次へ→」というボタンがあると次のページがあることを意味するので、これを利用して次ページがなくなるまで繰り返しデータを収集するようなプログラムにしました。
from bs4 import BeautifulSoup
import unicodedata
import re
import pandas as pd
# 各行のデータを格納する変数を定義
data = []
# 次ページが存在するかをチェックするための変数を定義
is_next_page_available = True
# is_next_page_availableがTrueである限り処理を繰り返す
while is_next_page_available:
    
    # htmlを取得
    html = BeautifulSoup(driver.page_source, "html.parser")
    
    # tableを取得
    table = html.find("td", text=re.compile("銘柄")).findParent("table")
    # 全てのtr要素をチェック 一行目はカラム名なのでスキップする
    for tr in table.findAll("tr"):
        
        # 背景色とカラムの連結で銘柄情報が含まれている行かどうかを判断する
        if tr.find("td").get("bgcolor") == "#ffffff" and tr.find("td").get("colspan") == "2":
            
            # 行データを格納するリストを定義
            row = []
            
            # 全てのセルデータを収集、不要な文字を排除して、unicodedataで正規化する
            for td in tr.findAll("td"):
                if td.getText().strip() != "":
                    text = td.getText().strip().replace(",", "").replace("+", "")
                    row.append(unicodedata.normalize("NFKC", text))
            # 全てのセルデータを収集、不要な文字を排除して、unicodedataで正規化する
            for td in tr.findNext("tr").findAll("td"):
                if td.getText().strip() != "":
                    text = td.getText().strip().replace(",", "").replace("+", "")
                    row.append(unicodedata.normalize("NFKC", text))
        
            # dataに追加
            data.append(row)
    
    # 次ページがあるかチェック あればクリック、なければ終了
    if html.find("u", text=re.compile("次へ→")):
        driver.find_element_by_link_text("次へ→").click()
        driver.implicitly_wait(60)
    else:
        is_next_page_available = False
        
# カラム名を定義
columns = ["銘柄", "取引", "売却/決済金額(費用)", "取得/新規年月日", "取得/新規金額", "損益金額", "約定日", "数量", "受渡日"]
# DataFrameに変換
df = pd.DataFrame(data, columns=columns)
これでデータが収集できます。
また、今回は損益情報の取得が目的なので、税徴収額については収集していません。
収集したデータがこちらです。

いい感じですよね。
データの整形
次に修正したデータを少し整形していきます。
これは何をやるかによってどこまで綺麗に整形するべきかは変わってくると思うのですが、ここでは次のような作業をしました。
データ整形
- 全カラムに対して、"--"とNaNに変換
 - 「銘柄」から証券コードを抽出 → 証券コードがある銘柄が日本株と判断して日本株データにもに絞る
 - 「数量」をintに変換
 - 「約定日」、「受渡日」をdatetimeに変換
 - 「取得/新規金額」、「損益金額」をfloatに変換
 - 「取引」から株式配当金を除外
 
これらを実装したコードがこちらです。
import numpy as np
import datetime
import re
# "--"とNaNに変換
df.replace("--", np.nan, inplace=True)
# 証券コードを抽出
df["証券コード"] = df["銘柄"].apply(
    lambda x: re.findall(r"[0-9]{4}", x)[0] if len(re.findall(r"[0-9]{4}", x))>0 else np.nan)
# 証券コードがあるデータのみ抽出
df = df.dropna(subset=["証券コード"]).reset_index(drop=True)
# 日付の型変換
df["約定日"] = df["約定日"].apply(lambda x: datetime.datetime.strptime(x, "%y/%m/%d"))
df["受渡日"] = df["受渡日"].apply(lambda x: datetime.datetime.strptime(x, "%y/%m/%d"))
# floatに変換
df["取得/新規金額"] = df["取得/新規金額"].apply(lambda x: float(x))
df["損益金額"] = df["損益金額"].apply(lambda x: float(x))
# intに変換
df["数量"] = df["数量"].apply(lambda x: int(x.replace("株", "")))
# 配当金を除外
df = df[df["取引"]!="株式配当金"]
これで結構データはきれいになります。

これで全てのプロセスが完了です。
あとは必要に応じてカラムを追加したり集計したりすればいろいろな分析や用途に活用できるかと思います。
最後にコードをまとめてどうぞ!
最後に、これまでのコードをまとめたものを共有します。
解説で割愛した部分も全て網羅しています。
なるべくコメントも多くつけました。
コピペ用にどうぞ。
from selenium import webdriver 
from selenium.webdriver.support.select import Select
from bs4 import BeautifulSoup
import unicodedata
import re
import pandas as pd
"""
ページ遷移
"""
# 口座管理ページへ遷移
driver.find_element_by_css_selector("img[title=口座管理]").click()
driver.implicitly_wait(60)
# 取引履歴ページへ遷移
driver.find_element_by_link_text("取引履歴").click()
driver.implicitly_wait(60)
# 譲渡益税明細ページへ遷移
driver.find_element_by_link_text("譲渡益税明細").click()
driver.implicitly_wait(60)
"""
条件選択
"""
# from 年
from_year_div = driver.find_element_by_name("ref_from_yyyy")
from_year_options = Select(from_year_div)
from_year_value_list = [int(y.get_attribute("value")) for y in from_year_options.options]
# from 月
from_month_div = driver.find_element_by_name("ref_from_mm")
from_month_options = Select(from_month_div)
from_month_value_list = [int(m.get_attribute("value")) for m in from_month_options.options]
# from 日
from_day_div = driver.find_element_by_name("ref_from_dd")
from_day_options = Select(from_day_div)
from_day_value_list = [int(d.get_attribute("value")) for d in from_day_options.options]
# to 年
to_year_div = driver.find_element_by_name("ref_to_yyyy")
to_year_options = Select(to_year_div)
to_year_value_list = [int(y.get_attribute("value")) for y in to_year_options.options]
# to 月
to_month_div = driver.find_element_by_name("ref_to_mm")
to_month_options = Select(to_month_div)
to_month_value_list = [int(m.get_attribute("value")) for m in to_month_options.options]
# to 日
to_day_div = driver.find_element_by_name("ref_to_dd")
to_day_options = Select(to_day_div)
to_day_value_list = [int(d.get_attribute("value")) for d in to_day_options.options]
# 表示件数
max_count_div = driver.find_element_by_name("max_cnt")
max_count_options = Select(max_count_div)
max_count_value_list = [int(c.get_attribute("value")) for c in max_count_options.options]
# 受渡日を設定
from_year_options.select_by_index(from_year_value_list.index(2022))
from_month_options.select_by_index(from_month_value_list.index(1))
from_day_options.select_by_index(from_day_value_list.index(1))
to_year_options.select_by_index(to_year_value_list.index(2023))
to_month_options.select_by_index(to_month_value_list.index(3))
to_day_options.select_by_index(to_day_value_list.index(31))
# 表示件数を設定
max_count_options.select_by_index(max_count_value_list.index(100))
# 「照会」をクリック
driver.find_element_by_name("ACT_search").click()
driver.implicitly_wait(60)
"""
データ収集
"""
# 各行のデータを格納する変数を定義
data = []
# 次ページが存在するかをチェックするための変数を定義
is_next_page_available = True
# is_next_page_availableがTrueである限り処理を繰り返す
while is_next_page_available:
    
    # htmlを取得
    html = BeautifulSoup(driver.page_source, "html.parser")
    
    # tableを取得
    table = html.find("td", text=re.compile("銘柄")).findParent("table")
    # 全てのtr要素をチェック 一行目はカラム名なのでスキップする
    for tr in table.findAll("tr"):
        
        # 背景色とカラムの連結で銘柄情報が含まれている行かどうかを判断する
        if tr.find("td").get("bgcolor") == "#ffffff" and tr.find("td").get("colspan") == "2":
            
            # 行データを格納するリストを定義
            row = []
            
            # 全てのセルデータを収集、不要な文字を排除して、unicodedataで正規化する
            for td in tr.findAll("td"):
                if td.getText().strip() != "":
                    text = td.getText().strip().replace(",", "").replace("+", "")
                    row.append(unicodedata.normalize("NFKC", text))
            # 全てのセルデータを収集、不要な文字を排除して、unicodedataで正規化する
            for td in tr.findNext("tr").findAll("td"):
                if td.getText().strip() != "":
                    text = td.getText().strip().replace(",", "").replace("+", "")
                    row.append(unicodedata.normalize("NFKC", text))
        
            # dataに追加
            data.append(row)
    
    # 次ページがあるかチェック あればクリック、なければ終了
    if html.find("u", text=re.compile("次へ→")):
        driver.find_element_by_link_text("次へ→").click()
        driver.implicitly_wait(60)
    else:
        is_next_page_available = False
        
# カラム名を定義
columns = ["銘柄", "取引", "売却/決済金額(費用)", "取得/新規年月日", "取得/新規金額", "損益金額", "約定日", "数量", "受渡日"]
# DataFrameに変換
df = pd.DataFrame(data, columns=columns)
"""
データ整形
"""
# "--"とNaNに変換
df.replace("--", np.nan, inplace=True)
# 証券コードを抽出
df["証券コード"] = df["銘柄"].apply(
    lambda x: re.findall(r"[0-9]{4}", x)[0] if len(re.findall(r"[0-9]{4}", x))>0 else np.nan)
# 証券コードがあるデータのみ抽出
df = df.dropna(subset=["証券コード"]).reset_index(drop=True)
# 日付の型変換
df["約定日"] = df["約定日"].apply(lambda x: datetime.datetime.strptime(x, "%y/%m/%d"))
df["受渡日"] = df["受渡日"].apply(lambda x: datetime.datetime.strptime(x, "%y/%m/%d"))
# floatに変換
df["取得/新規金額"] = df["取得/新規金額"].apply(lambda x: float(x))
df["損益金額"] = df["損益金額"].apply(lambda x: float(x))
# intに変換
df["数量"] = df["数量"].apply(lambda x: int(x.replace("株", "")))
# 配当金を除外 df = df[df["取引"]!="株式配当金"]
これで、PythonとSeleniumを使って、SBI証券のサイトから日本株の損益履歴を収集することができるようになりました。
まとめ
本記事では「【Python】SeleniumでSBI証券から日本株の損益履歴を収集する方法」について解説しました。
PythonとSeleniumを使えば、損益履歴の収集を自動化することができました。
参照時の条件選択も全てSeleniumを通じて自由に操作することができます。
このデータを集計すれば、月ごとの勝率や平均利益率、損切り回数のカウントなど、いろいろな分析に活用することができます。
最後まで読んでくださり、ありがとうございました。
 









