Money Forward Me で csv ダウンロードボタンを作成してみた

# Money Forward Me のcsvダウンロードをしたかった

けど有料オプションしかない．
そして1年しか見れないから，過去ログを保存できない．
CSVで月ごとにダウンロードして，まとめたいと思ったので，自作することにした．

# 実装

早速実装に向けて取り掛かろうと思う

## 必要なもの

- モダンブラウザ
    - Chrome (推奨)
    - Firefox (推奨)
    - Microsoft Edge (推奨)
    - Safari (推奨)
    - Opera Next
    - Dolphin Browser
- [Tampermonkey](https://www.tampermonkey.net/) (拡張機能)

https://www.tampermonkey.net/

ここではTampermonkeyの説明はしません．
自作の拡張機能を簡単に使用する為の拡張機能です．

## データのcsv化およびダウンロード

```js:handleDownload()
// セルの中の改行を消す為の正規表現
const removeN = /\n.*/g;
const removeComma = /,/g;
const regex = 'icon-check icon-large';

// 文字コードをBOM付きUTF-8に指定
var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

// データが入っている要素を取得
var table = document.getElementById('cf-detail-table');
const yearSlash = document.getElementsByTagName("h2")[1].innerText.slice(0,5); // <='2022/'

// ここに文字データとして値を格納していく
var data_csv="計算対象, 日付, 内容, 金額, 保有金融機関, 大項目, 中項目, メモ, 振替\n";

for(var i = 1; i < table.rows.length; i++){ // 行単位
    // 振替チェック
    var isTransfar
    if(table.rows[i].cells[3].innerText.indexOf('\n') != -1) isTransfar = true;
    else isTransfar = false;

    for(var j = 0; j < table.rows[i].cells.length - 1; j++){ // 列単位
        // data_cell の用意
        var data_cell = table.rows[i].cells[j];

        // data_cell の値を整形して data_csv に格納
        if(j == 0) {
            if((data_cell.innerHTML + '').indexOf(regex) != -1) data_csv += 'TRUE'; // data_cell のテキストがregexを含んでいたら(チェックボックスを入れるクラスがあれば)
            else data_csv += 'FALSE';
        } else if(j == 1) {
            // 年表記を入れ，曜日表記を消す
            var dataTemp = (yearSlash + data_cell.innerText.replace(removeN, "").replace(removeComma, ""));
            data_csv += dataTemp.slice(0,10);
        } else if(j == 8 && isTransfar) {
            // 8列目かつ振替がON
            data_csv += 'TRUE';
        } else {
            data_csv += data_cell.innerText.replace(removeN, "").replace(removeComma, "");
        }

        // 毎行の終わりに改行コードを追加
        if(j == table.rows[i].cells.length - 2) data_csv += "\n";

        // セル値の区切り文字として,を追加
        else data_csv += ",";
    }
}

// ダウンロードボタンの作成
// ダウンロードするファイル名の定義
var fileName = document.getElementsByTagName('h2')[1].innerText.slice(2,7).replace('/', '_');
fileName += '.csv'; // ex) 22_10.csv <=2022年10月分

// 挿入位置，ボタンなどの定義
var parentDiv = document.getElementsByClassName('pull-right mf-mb-medium')[0];
if(document.getElementById('download') != null) {
    parentDiv.removeChild(download);
}

var childA = document.getElementsByClassName('btn cf-new-btn btn-warning')[0];
var blob = new Blob([ bom, data_csv ], { "type" : "text/csv" });

var newElement = document.createElement('a');
var newContent = document.createTextNode('Download');
newElement.appendChild(newContent);
newElement.setAttribute('id', 'download');
newElement.setAttribute('href', '#');
newElement.setAttribute('style', 'padding:5px 20px; width: 100px; height: 32px; line-height: 34px; margin-right: 10px;');
newElement.className = 'btn cf-new-btn btn-warning';
newElement.href = window.URL.createObjectURL(blob);
newElement.download = fileName;

// 作成したコードを挿入する
parentDiv.insertBefore(newElement, childA);
```

このコードでcsvにしてダウンロードするまでは出来た．

## ダウンロードデータの更新

Money Forward Meでは月の移動をしたときにページの再読み込みをしない(部分的再読み込み)為，このままでは月の移動をした際に，欲しいデータがダウンロードできない．

したがって，上記コードを関数化(今回のコード上では`handleDownload()`)し，適宜呼び出すことにする．
その為にはページ内の変更を監視し，その度に再呼び出しすればよい．

以下のように実装した．

```js
var observer = new MutationObserver(function(){
    // DOMの変化が起こった時の処理
    const parentDiv = document.getElementsByClassName('pull-right mf-mb-medium')[0];
    handleDownload();
    if(document.getElementsByTagName('h2')[0].innerText.indexOf('Loading') != -1) {
        if(document.getElementById('download') != null) {
            parentDiv.removeChild(download);
        }
    }
}, false);

// 監視対象の要素オブジェクト
const elem = document.getElementsByClassName('date_range transaction-in-out-header')[0];

// 監視時のオプション
const config = {
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true,
};

// 要素の変化監視をスタート
observer.observe(elem, config);
```

これで要素の変化を取得し，適宜`handleDownload()`を実行することでデータの再読み込みを行っている．

なお，`parentDiv.removeChild(download);`ではdownloadボタンの一次的な削除を行い，データのcsv化が終わるまではボタンを表示しないようにしている．

また，監視時のオプション`const config`において，`subtree: true,`にしないと更新の読み込みが上手く行かないので注意が必要である．

## 最終的なコード

```js:CSV Download
// ==UserScript==
// @name         CSV Download
// @namespace    https://github.com/Yuto-34
// @license      Yuto-34
// @version      1.0.2
// @description  Export csv file of displayed month.
// @author       Yuto
// @match        https://moneyforward.com/cf
// @icon         https://www.google.com/s2/favicons?sz=64&domain=moneyforward.com
// @grant        none
// ==/UserScript==


function handleDownload() {
    // セルの中の改行を消す為の正規表現
    const removeN = /\n.*/g;
    const removeComma = /,/g;
    const regex = 'icon-check icon-large';

    // 文字コードをBOM付きUTF-8に指定
    var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

    // データが入っている要素を取得
    var table = document.getElementById('cf-detail-table');
    const yearSlash = document.getElementsByTagName("h2")[1].innerText.slice(0,5);

    // ここに文字データとして値を格納していく
    var data_csv="計算対象, 日付, 内容, 金額, 保有金融機関, 大項目, 中項目, メモ, 振替\n";


    for(var i = 1; i < table.rows.length; i++){
        var isTransfar
        // 振替チェック
        if(table.rows[i].cells[3].innerText.indexOf('\n') != -1) {
            isTransfar = true;
        } else {
            isTransfar = false;
        }
        for(var j = 0; j < table.rows[i].cells.length - 1; j++){
            // data_cell の用意
            var data_cell = table.rows[i].cells[j];

            // HTML中の表のセル値をdata_csvに格納
            if(j == 0) {
                if((data_cell.innerHTML + '').indexOf(regex) != -1) {
                    data_csv += 'TRUE';
                } else {
                    data_csv += 'FALSE';
                }
            } else if(j == 1) {
                var dataTemp = (yearSlash + data_cell.innerText.replace(removeN, "").replace(removeComma, ""));
                data_csv += dataTemp.slice(0,10);
            } else if(j == 8 && isTransfar) {
                data_csv += 'TRUE';
            } else {
                data_csv += data_cell.innerText.replace(removeN, "").replace(removeComma, "");
            }

            // 行終わりに改行コードを追加
            if(j == table.rows[i].cells.length - 2) data_csv += "\n";

            // セル値の区切り文字として,を追加
            else data_csv += ",";
        }
    }

    // ダウンロードボタンの作成
    var fileName = document.getElementsByTagName('h2')[1].innerText.slice(2,7).replace('/', '_');
    fileName += '.csv';

    var parentDiv = document.getElementsByClassName('pull-right mf-mb-medium')[0];
    if(document.getElementById('download') != null) {
        parentDiv.removeChild(download);
    }

    var childA = document.getElementsByClassName('btn cf-new-btn btn-warning')[0];
    var blob = new Blob([ bom, data_csv ], { "type" : "text/csv" });

    var newElement = document.createElement('a');
    var newContent = document.createTextNode('Download');
    newElement.appendChild(newContent);
    newElement.setAttribute('id', 'download');
    newElement.setAttribute('href', '#');
    newElement.setAttribute('style', 'padding:5px 20px; width: 100px; height: 32px; line-height: 34px; margin-right: 10px;');
    newElement.className = 'btn cf-new-btn btn-warning';
    newElement.href = window.URL.createObjectURL(blob);
    newElement.download = fileName;

    parentDiv.insertBefore(newElement, childA);

    // delete data_csv;//data_csvオブジェクトはもういらないので消去してメモリを開放
}


(function() {
    'use strict';
    // Your code here...

    handleDownload();

    var observer = new MutationObserver(function(){
        // DOMの変化が起こった時の処理
        const parentDiv = document.getElementsByClassName('pull-right mf-mb-medium')[0];
        handleDownload();
        if(document.getElementsByTagName('h2')[0].innerText.indexOf('Loading') != -1) {
            if(document.getElementById('download') != null) {
                parentDiv.removeChild(download);
            }
        }
    }, false);

    // 監視対象の要素オブジェクト
    const elem = document.getElementsByClassName('date_range transaction-in-out-header')[0];

    // 監視時のオプション
    const config = {
        attributes: true,
        childList: true,
        characterData: true,
        subtree: true,
    };

    // 要素の変化監視をスタート
    observer.observe(elem, config);
})();
```

# 公開済みURL

現在Greasy Forkにて公開中(誰でもインストールできる状態)にしております．
必要に応じてご使用ください．
月1程度でメンテナンスする予定です．

https://greasyfork.org/ja/scripts/452371-csv-download

# おまけ

batファイルで出力したcsvファイルを結合した

```bat
@echo off
powershell -c "$csvfiles=dir -file '*.csv';$csvfiles|%%{ipcsv $_ -encoding utf8}|epcsv 'comb.csv' -NoTypeInformation -encoding utf8"
```