wide and deep

【Selenium+Python+CentOS7/Win10】MoneyForwardから資産推移CSVを自動取得

はじめに

表題のことをやりたかったです.

このツイートをしたときはWindowsで動かせていたのですが,レンタルサーバのCentOS7上で定期的に取得したかったので,対応しました.
本記事はそれらの説明です.
将来的には,このデータをいい感じにプロットして閲覧できるWEBページを作りたいと思います. (もしくは画像化して配信)

成果物はこちら ↓.
github.com

環境構築について

seleniumとpandasをインストールしたり,ここは簡単

pip install selenium, pandas

web driverまわり

Windowsの場合はここからバイナリを落として来てそのまま使えます.かんたん.
https://sites.google.com/chromium.org/driver/

CentOS7でのweb driverを使用したアクセスが面倒だったのでメモを残します.
なぜCentOSでは面倒なのかというと,CLIから,つまりDisplayがないとMoneyForwardはアクセスを弾いちゃうらしいです.
Javascriptによるレンダリングがされていると,クライアントによってはレンダリングを実施しない場合があるらしいです.
そのため,chrome本体/chrome driver/仮想ディスプレイを入れる必要があります.

このあたりは下記記事を参考にさせていただきました.
CentOS7でSelenium+Pythonを動かすまで - Qiita
CentOS7とSeleniumとPythonとChromeで定期実行処理を作ってみた - Qiita

Chromeのインストール

# vim /etc/yum.repos.d/google.chrome.repo
[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub
# yum update
# yum -y install google-chrome-stable
# google-chrome --version
Google Chrome 94.0.4606.61
# yum -y install ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts
# google-chrome --headless --no-sandbox --dump-dom https://www.google.com/

Chromeドライバのインストール(バージョンをChromeに合わせる形で)

# cd /usr/local/bin
# wget https://chromedriver.storage.googleapis.com/94.0.4606.61/chromedriver_linux64.zip
# unzip chromedriver_linux64.zip
# chmod 755 chromedriver
# rm chromedriver_linux64.zip 

仮想ディスプレイインストール&設定

# yum install xorg-x11-server-Xvfb
# vim /usr/lib/systemd/system/Xvfb.service
[Unit]
Description=Virtual Framebuffer X server for X Version 11

[Service]
Type=simple
EnvironmentFile=-/etc/sysconfig/Xvfb
ExecStart=/usr/bin/Xvfb $OPTION
ExecReload=/bin/kill -HUP ${MAINPID}

[Install]
WantedBy=multi-user.target
# vim /etc/sysconfig/Xvfb
# Xvfb Enviroment File
OPTION=":1 -screen 0 1366x768x24"
# systemctl enable Xvfb
# systemctl start Xvfb
# export DISPLAY=localhost:1.0;

開発の流れ

GUIではどうやってCSVをダウンロードするかを確認

seleniumでの記述に落とし込む

落としてきたcsvをいい感じに整形

開発

GUIではどうやってCSVをダウンロードするかを確認

  1. URLアクセス f:id:catdance124:20210928205939p:plain
  2. [メールアドレスでログイン]を押す
  3. 遷移した先でメールアドレスを入力しsubmit f:id:catdance124:20210928210036p:plain
  4. 遷移した先でパスワードを入力しsubmit f:id:catdance124:20210928210200p:plain
  5. ログイン完了 f:id:catdance124:20210928210348p:plain
  6. [資産推移]ページに移動(URLアクセス) f:id:catdance124:20210928210447p:plain
  7. ページ下部リンクから当月CSVをダウンロード f:id:catdance124:20210928210552p:plain
  8. 各月のページに飛び,CSVをダウンロード f:id:catdance124:20210928210752p:plain

こんな感じですかね.
勘のいい人は7,8の画像左下のリンクを見て,CSV取得はURL決め打ちでできるとわかったかと思います.

seleniumでの記述に落とし込む

細かい実装はリポジトリを見てもらうとして,上の記述をコードで追っていきます.

ログイン

ここの処理を実装します.

1. URLアクセス
2. [メールアドレスでログイン]を押す
3. 遷移した先でメールアドレスを入力しsubmit
4. 遷移した先でパスワードを入力しsubmit
5. ログイン完了
    def login(self, email, password):
        login_url = "https://moneyforward.com/sign_in"
        self.driver.get(login_url)
        self.driver.find_element_by_link_text("メールアドレスでログイン").click()
        elem = self.driver.find_element_by_name("mfid_user[email]")
        elem.clear()
        elem.send_keys(email)
        elem.submit()
        elem = self.driver.find_element_by_name("mfid_user[password]")
        elem.clear()
        elem.send_keys(password)
        elem.submit()

ここはHTMLソースを見ながら要素のinnerHTMLやname, classなどが使えないかを見ながら,流れ通り実装していきます.

資産推移CSVダウンロード

6. [資産推移]ページに移動(URLアクセス)
7. ページ下部リンクから当月CSVをダウンロード
8. 各月のページに飛び,CSVをダウンロード
    def download_history(self):
        # 6. [資産推移]ページに移動(URLアクセス)
        history_url = "https://moneyforward.com/bs/history"
        self.driver.get(history_url)
        elems = self.driver.find_elements_by_xpath('//*[@id="bs-history"]/*/table/tbody/tr/td/a')
        # download previous month csv
        # 8. 各月のページに飛び,CSVをダウンロード
        for elem in elems:
            href = elem.get_attribute("href")
            if "monthly" in href:
                month = re.search(r'\d{4}-\d{2}-\d{2}', href).group()
                save_path = Path(self.csv_dir/f"{month}.csv")
                if not save_path.exists():
                    month_csv = f"https://moneyforward.com/bs/history/list/{month}/monthly/csv"
                    self.driver.get(month_csv)
                    self._rename_latest_file(save_path)
        # download this month csv
        # 7. ページ下部リンクから当月CSVをダウンロード
        this_month_csv = "https://moneyforward.com/bs/history/csv"
        save_path = Path(self.csv_dir/"this_month.csv")
        if save_path.exists():
            save_path.unlink()
        self.driver.get(this_month_csv)
        self._rename_latest_file(save_path)

大体は流れ通りなのですが,各月CSVをダイレクトにURLからダウンロードする場合,どの月のCSVが存在するかを判定しなければいけません.
そこは,資産推移ページに存在する各月のリンクhrefから情報を取得しています.href = elem.get_attribute("href")のところ

あとはダウンロードしたファイルの名前が日本語だったりで扱いづらいので,YYYY-MM-dd.csvにリネームしたりしています.
seleniumでは名前を付けて保存ができないので,一度ダウンロードしてからself._rename_latest_file(save_path) のところで最新日時ファイルをリネームするという処理をしています.

    def _rename_latest_file(self, new_path):
        time.sleep(2)
        csv_list = self.csv_dir.glob('*[!all].csv')
        latest_csv = max(csv_list, key=lambda p: p.stat().st_ctime)
        latest_csv.rename(new_path)

落としてきたCSVをまとめる

各月で別のCSVになっているので,pandasでまとめます.

    def _concat_csv(self):
        csv_list = sorted(self.csv_dir.glob('*[!all].csv'))
        df_list = []
        for csv_path in csv_list:
            df = pd.read_csv(csv_path, encoding="shift-jis", sep=',')
            df_list.append(df)
        df_concat = pd.concat(df_list)
        df_concat.drop_duplicates(subset='日付', inplace=True)
        df_concat.set_index('日付', inplace=True)
        df_concat.sort_index(inplace=True)
        df_concat.fillna(0, inplace=True)
        df_concat.to_csv(Path(self.csv_dir/'all.csv'), encoding="shift-jis")

all.csv以外を読んで,concatして,日付でソート・重複削除後 all.csvとして保存するって感じです.

おわりに

MoneyForwardで資産推移グラフを見るためだけにお金を払いたくないというモチベーションのみで,着想からここまで3日でできました.
データを使用してプロットするプログラムは別リポジトリで作業しようと思っています.完走できるといいなあ github.com

.

参考

qiita.com qiita.com qiita.com stackoverflow.com qiita.com

qiita.com