Skip to content

Instantly share code, notes, and snippets.

@cho45
Created August 29, 2025 23:20
Show Gist options
  • Save cho45/e0ed3e408f87ab9a5c29e2d612db82ab to your computer and use it in GitHub Desktop.
Save cho45/e0ed3e408f87ab9a5c29e2d612db82ab to your computer and use it in GitHub Desktop.
javascript:(async function() {
try {
// CSRFトークンを取得
const metaToken = document.querySelector('meta[name="csrf-token"]');
const token = metaToken ? metaToken.content : null;
if (!token) {
alert('CSRFトークンが見つかりません');
return;
}
// 進捗表示用のUI作成
const progressDiv = document.createElement('div');
progressDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border: 2px solid #333;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 10000;
font-family: sans-serif;
min-width: 300px;
`;
progressDiv.innerHTML = `
<h3 style="margin-top: 0;">MoneyForward 統合データ取得</h3>
<div id="mf-status">データ取得中...</div>
<div id="mf-detail" style="margin-top: 10px; font-size: 12px; color: #666;"></div>
`;
document.body.appendChild(progressDiv);
const statusEl = document.getElementById('mf-status');
const detailEl = document.getElementById('mf-detail');
// 月次データ範囲を計算
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const months = [];
for (let i = 1; i <= 12; i++) {
let targetDate = new Date(currentYear, currentMonth - 1 - i, 1);
months.push({
year: targetDate.getFullYear(),
month: targetDate.getMonth() + 1
});
}
// データ取得を並列実行
statusEl.textContent = '資産推移データと取引履歴を並列取得中...';
const [assetsResult, csvResult] = await Promise.allSettled([
// 資産推移データ取得
fetchAssetData(token, detailEl),
// CSV取引履歴取得
fetchTransactionData(token, months, detailEl)
]);
// 結果を統合
statusEl.textContent = 'データを統合中...';
const exportData = {
exportDate: new Date().toISOString().slice(0, 10),
period: {
from: `${months[months.length-1].year}-${String(months[months.length-1].month).padStart(2,'0')}`,
to: `${months[0].year}-${String(months[0].month).padStart(2,'0')}`
},
monthlyAssets: assetsResult.status === 'fulfilled' ? assetsResult.value : null,
transactions: csvResult.status === 'fulfilled' ? csvResult.value : null,
errors: []
};
// エラー情報を収集
if (assetsResult.status === 'rejected') {
exportData.errors.push({ type: 'assets', message: assetsResult.reason?.message || 'Unknown error' });
}
if (csvResult.status === 'rejected') {
exportData.errors.push({ type: 'transactions', message: csvResult.reason?.message || 'Unknown error' });
}
// JSONファイルをダウンロード
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `moneyforward_export_${exportData.exportDate}.json`;
link.click();
URL.revokeObjectURL(url);
// 結果表示
statusEl.textContent = '✅ 完了!';
const assetsCount = exportData.monthlyAssets?.length || 0;
const transactionsCount = exportData.transactions?.rowCount || 0;
console.log(exportData.errors)
detailEl.innerHTML = `
<div>資産推移: ${assetsCount}ヶ月分</div>
<div>取引履歴: ${transactionsCount}件</div>
${exportData.errors.length > 0 ? `<div style="color: #d00;">エラー: ${exportData.errors.length}件</div>` : ''}
<button onclick="this.parentElement.parentElement.remove();" style="margin-top: 10px;">閉じる</button>
`;
} catch (error) {
const statusEl = document.getElementById('mf-status');
const detailEl = document.getElementById('mf-detail');
if (statusEl) statusEl.textContent = '❌ エラーが発生しました';
if (detailEl) detailEl.textContent = error.message;
console.error('統合データ取得エラー:', error);
}
// 資産推移データ取得関数
async function fetchAssetData(token, detailEl) {
detailEl.textContent = '資産推移データ取得中...';
const response = await fetch("https://moneyforward.com/update_chart/365", {
headers: {
"accept": "*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript",
"accept-language": "ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": token,
"x-requested-with": "XMLHttpRequest"
},
referrer: "https://moneyforward.com/bs/history",
method: "GET",
mode: "cors",
credentials: "include"
});
if (!response.ok) {
throw new Error(`資産推移データ取得失敗 HTTP ${response.status}`);
}
const jsCode = await response.text();
// データを抽出
const categoriesMatch = jsCode.match(/var categoriesData = \[([\s\S]*?)\];/);
const timeSeriesMatch = jsCode.match(/var timeSeriesData = \[([\s\S]*?)\];/);
if (!categoriesMatch || !timeSeriesMatch) {
throw new Error('資産推移データの解析に失敗しました');
}
let categoriesData, timeSeriesData;
eval('colors = [];'); // avoid errors
eval('categoriesData = [' + categoriesMatch[1] + '];');
eval('timeSeriesData = [' + timeSeriesMatch[1] + '];');
// 月末データを抽出
const monthlyData = [];
let currentMonth = '';
for (let i = categoriesData.length - 1; i >= 0; i--) {
const date = categoriesData[i];
const yearMonth = date.substring(0, 7);
if (yearMonth !== currentMonth) {
currentMonth = yearMonth;
const monthEndData = {
date: date,
assets: {}
};
timeSeriesData.forEach(series => {
monthEndData.assets[series.name] = series.data[i];
});
monthlyData.unshift(monthEndData);
if (monthlyData.length >= 12) break;
}
}
return monthlyData;
}
// 取引履歴CSV取得関数
async function fetchTransactionData(unusedToken, months, detailEl) {
detailEl.textContent = '取引履歴データ取得中...';
const allRows = [];
let header = null;
let successCount = 0;
const failedMonths = [];
for (let i = 0; i < months.length; i++) {
const { year, month } = months[i];
const monthStr = String(month).padStart(2, '0');
try {
const fromDate = `${year}%2F${monthStr}%2F01`;
const url = `https://moneyforward.com/cf/csv?from=${fromDate}&month=${month}&year=${year}`;
const response = await fetch(url, {
credentials: 'include',
headers: { 'Accept': 'text/csv' }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
const decoder = new TextDecoder('shift-jis');
const csvText = decoder.decode(arrayBuffer);
const lines = csvText.split('\n').filter(line => line.trim());
if (lines.length > 0) {
if (header === null) {
header = lines[0];
allRows.push(header);
allRows.push(...lines.slice(1));
} else {
allRows.push(...lines.slice(1));
}
successCount++;
}
} catch (error) {
console.error(`${year}-${monthStr}の取得に失敗:`, error);
failedMonths.push(`${year}-${monthStr}`);
}
// 進捗更新
detailEl.textContent = `取引履歴取得中... ${i + 1}/${months.length}`;
// サーバー負荷軽減
await new Promise(resolve => setTimeout(resolve, 200));
}
return {
format: 'csv',
encoding: 'utf-8',
successCount,
failedMonths,
rowCount: allRows.length - 1, // ヘッダー除く
data: allRows.join('\n')
};
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment