Created
August 29, 2025 23:20
-
-
Save cho45/e0ed3e408f87ab9a5c29e2d612db82ab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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