マネーフォワードのポートフォリオ(資産ページ)は資産の内訳が一覧できて便利なのだが、銘柄コードがあるものは全部株、という感じでざっくりしている。商品の区分も業種の区分もわからないので、もうちょい細かく見たいときに困る。というか自分のポートフォリオが実際どうなっているのかよく分かってない。これでは良くないということできちんと集計しようと考えた。
しかし証券会社のwebサイトの出力はしょぼいし、マネーフォワードのポートフォリオはそもそも出力できないしユーザーが使えるAPIも公開されてない、ということでマネーフォワードをスクレイピングすることにした。
rvest
Rでちょっとスクレイピングするならrvestパッケージが手軽だ。
今回rvestは少し使うだけなのであまり説明しないが、HTMLにアクセスさえできれば、read_table()
(Parse an html table into a data frame — html_table • rvest)とかでシュッとテーブルをデータフレームにしたりできる。スゴイカンタン。
セッションも扱えるので、ログインが必要なページも割といける。だがjavascriptで動的にページが作成されていたりするとダメ。具体的にはマネーフォワードのログイン画面がダメ。
RSelenium
というわけでSeleniumを使う。RでSeleniumを使う方法としてRSeleniumパッケージがある。
Seleniumを使うためにはSelenium Serverを動かす必要があって、RSeleniumではDockerの利用を推奨している。が、M1 macだからなのか手元では動かなかった。
Dockerを使わない方法としてrsDriver()
関数が用意されている。rsDriver()
はかなり気軽に使えて、rsDriver(browser = "firefox")
とかやれば適当にWebDriverを落としてきてくれてFirefoxが起動する(※ブラウザとJavaを別途準備しておく必要はある)。
具体的には次のようにしてremoteDriverクラスのオブジェクトを作成し、このオブジェクトを通じてブラウザを操作する*1。
rD <- rsDriver(browser = "firefox") remDr <- rD$client
無事に動けばFirefoxが別ウィンドウで開くので、様子を見ながら操作を書いていけば良い。基本的には次のように操作を進めていく。
remDr$navigate()
で目的のページに移動する。remDr$findElement()
で要素を探してオブジェクトにする。- 要素を指定する方法はいくつかあるが、以下ではブラウザの開発者ツールを開いて取得した要素のXPathを利用している。
- 要素オブジェクトを通じて、フォームへの入力やクリックといった操作を行う。
使い終わったらブラウザを閉じてサーバーを止めておく必要がある。
remDr$close() rD$server$stop()
もし忘れてしまっても、オブジェクトを削除してgc()
すれば止まる。
rm(rD) gc()
マネーフォワードからデータ取得
# rvestとRSeleniumを使う library(rvest) library(RSelenium)
## マネーフォワードのIDログイン画面にアクセス remDr$navigate("https://id.moneyforward.com/sign_in/email") Sys.sleep(5) # ページ読み込み待機 ## メールアドレスを入力して画面遷移 webElem <- remDr$findElement( using = "xpath", value = "/html/body/main/div/div/div/div/div[1]/section/form/div[2]/div/input" ) webElem$sendKeysToElement(list("自分のIDを入力する")) webElem <- remDr$findElement( using = "xpath", value = "/html/body/main/div/div/div/div/div[1]/section/form/div[2]/div/div[3]/input" ) webElem$clickElement() Sys.sleep(5) # ページ読み込み待機 ## パスワードを入力して画面遷移 webElem <- remDr$findElement( using = "xpath", value = "/html/body/main/div/div/div/div/div[1]/section/form/div[2]/div/input[2]" ) webElem$sendKeysToElement(list("自分のパスワードを入力する")) webElem <- remDr$findElement( using = "xpath", value = "/html/body/main/div/div/div/div/div[1]/section/form/div[2]/div/div[3]/input" ) webElem$clickElement()
ログインできたらマネーフォワードMEに移動し、ポートフォリオを表示する。
## ログイン状態でマネーフォワードMEに移動するとアカウント選択を求められるのでアカウントを選ぶ remDr$navigate("https://moneyforward.com/sign_in") Sys.sleep(5) webElem <- remDr$findElement( using = "xpath", value = "/html/body/main/div/div/div/div/div[1]/section/form/div[2]/div/div[2]/input" ) webElem$clickElement() Sys.sleep(5) ## ポートフォリオ画面に移動 remDr$navigate("https://moneyforward.com/bs/portfolio")
ここまでの操作はページ遷移→要素の選択→要素に対する操作の繰り返しなので、この3つの操作だけ覚えておけば多少サイトのレイアウトが変わっても対応できるはずだ。
remDr$getPageSource()
でページのHTMLを取得したら、後はrvestの力を借りてテーブルをデータフレームとして頂くだけ。
remDr$getPageSource()[[1]] %>% read_html() %>% html_table() -> df
中身はデータフレームのリストになっているので、あとは煮るなり焼くなり好きにできる。
煮たり焼いたりする
テーブルはそのまま読み込まれているので、多少の前処理は必要になる*3。
## 現金 df[[2]] %>% mutate(残高 = as.numeric(gsub("円|,", "", 残高))) -> money ## 株 df[[3]] %>% mutate_at(vars(保有数:評価額), ~as.numeric(gsub("円|,", "", .))) %>% select(1:6) -> stock ## 投資信託 df[[4]] %>% mutate_at(vars(保有数:評価額), ~as.numeric(gsub("円|,", "", .))) %>% select(1:5) -> inv_trusts ## ポイント・マイル df[[5]] %>% mutate(現在の価値 = as.numeric(gsub("円|,", "", 現在の価値))) -> point
個別株の情報をどうにかするには市場区分や業種区分のデータがあると良さそうだが、これはJPXのウェブサイトに良いものがある。
銘柄コードや商品区分、業種コードが一覧表になっている。Excelだが余計な装飾は無いのでほぼそのまま使える。
web上にあるリソースを読み込むならrioパッケージが便利だ。
rioパッケージは大概のリソースをどうにかできるimport()
という強力な関数を持っている。上記のデータは次のようにするだけでデータフレームになる。Excelファイルを手元に落とす必要はない。
## その他統計資料 | 日本取引所グループ - https://www.jpx.co.jp/markets/statistics-equities/misc/01.html url <- "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls" stock_catalog <- rio::import(url)
あとはもう何でもできる。好きにやろう。
例として17業種区分でバープロットを作成した際のコードを載せておく。
library(ggplot2) stock_catalog %>% mutate_at( vars(ends_with("業種区分")), ~if_else(. == "-", `市場・商品区分`, .) ) -> stock_catalog_mod stock %>% select(銘柄コード, 銘柄名, 評価額) %>% left_join(stock_catalog_mod %>% select(-1, -銘柄名), by = c("銘柄コード" = "コード")) %>% mutate_if(is.character, ~if_else(grepl("REIT", .), "REIT", .)) %>% group_by(`17業種区分`) %>% summarise(評価額 = sum(評価額)) %>% mutate(比率 = 評価額 / sum(評価額)) %>% ggplot(aes(y = `17業種区分`, x = 比率)) + geom_bar(stat = "identity") + scale_x_continuous(labels = scales::percent) + labs(x = "", y = "")