Last active
July 25, 2021 14:49
-
-
Save ddio/d482435cf9e6214c2929df4aa2fed64d 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
(async function main (downloadOnlyCurrentPeriod = true) { | |
const LOADING_TIMER = 1000 | |
function getPeriod () { | |
const billingPeriodEle = document.querySelector('.custom-select') | |
if (!billingPeriodEle) { | |
alert('找不到帳單週期,程式終止 -____-') | |
return | |
} | |
// value === '/dashboard/revenues/periods/YYYY-MM-DD' | |
return billingPeriodEle.value.split('/').slice(-1)[0] | |
} | |
function getPaginator () { | |
const pager = document.querySelector('.pagination') | |
if (!pager) { | |
return { | |
isAtHead: true, | |
isAtTail: true | |
} | |
} | |
// when there are pager it must contains, <<, <, >, and >> | |
const pagerList = pager.querySelectorAll('.pagination .page-item') | |
const toHead = pagerList[0] | |
const toTail = pagerList[pagerList.length - 1] | |
return { | |
pagerEle: pager, | |
isAtHead: toHead.classList.contains('disabled'), | |
isAtTail: toTail.classList.contains('disabled'), | |
headEle: toHead, | |
nextEle: pagerList[pagerList.length - 2] | |
} | |
} | |
function getStationInPage () { | |
return Array | |
.from(document.querySelectorAll('.ibox-body .row-5.align-items-center')) | |
.map((stationEle) => { | |
let title, profit | |
const titleEle = stationEle.querySelector('.mx-3') | |
const profitEle = stationEle.querySelector('.h3[title="發電收益"]') | |
if (titleEle) { | |
title = titleEle.textContent.trim() | |
// padleft if only one digit | |
if (!title.match(/\d\d/)) { | |
title = title.replace(/(\d)/, '0$1') | |
} | |
} | |
if (profitEle) { | |
profit = Number.parseFloat(profitEle.textContent.replace(/[$, ]/g, '')) | |
} | |
return { title, profit } | |
}) | |
.filter(station => station.title) | |
} | |
function genCsv (period, billing) { | |
const rows = [ | |
['案廠', period], | |
...billing.map(item => [item.title, item.profit]) | |
] | |
const text = rows.reduce((sum, row) => { | |
return sum + row.join(',') + '\n' | |
}, '') | |
const filename = `陽光伏特家-${period}.csv` | |
const fakeDownloader = document.createElement('a') | |
fakeDownloader.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(text)) | |
fakeDownloader.setAttribute('download', filename) | |
fakeDownloader.style.display = 'none' | |
document.body.appendChild(fakeDownloader) | |
fakeDownloader.click() | |
document.body.removeChild(fakeDownloader) | |
} | |
function genUnionCsv (periodList, crossPeriodBilling) { | |
const periodListAsc = periodList.slice().sort() | |
const titleListAsc = periodListAsc.map(period => period.split('/').slice(-1)) | |
const rows = [ | |
[ '案廠', ...titleListAsc ] | |
] | |
Object.keys(crossPeriodBilling).forEach((stationName) => { | |
const profitMap = crossPeriodBilling[stationName] | |
const profitList = periodListAsc.map((period) => { | |
return period in profitMap ? profitMap[period] : '' | |
}) | |
rows.push([ stationName, ...profitList ]) | |
}) | |
const csvText = rows | |
.map(row => row.join(',')) | |
.join('\n') | |
const fakeDownloader = document.createElement('a') | |
fakeDownloader.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvText)) | |
fakeDownloader.setAttribute('download', '陽光伏特家.csv') | |
fakeDownloader.style.display = 'none' | |
document.body.appendChild(fakeDownloader) | |
fakeDownloader.click() | |
document.body.removeChild(fakeDownloader) | |
} | |
function waitUntilPageReload (action) { | |
return new Promise((resolve) => { | |
let pageLoadTimer | |
function finishLoading () { | |
clearTimeout(pageLoadTimer) | |
pageLoadTimer = setTimeout(() => { | |
resolve() | |
pageObserver.disconnect() | |
}, LOADING_TIMER) | |
} | |
const pageObserver = new MutationObserver(finishLoading) | |
pageObserver.observe( | |
// rails seem to replace the entire document XD | |
document, | |
{ childList: true, subtree: true } | |
) | |
action() | |
}) | |
} | |
// | |
// Generate a csv, headers: | |
// 1. station name | |
// 2. profit of given period | |
// | |
function getStationBilling () { | |
return new Promise(async (resolve, reject) => { | |
const period = getPeriod() | |
if (!period) { | |
reject(new Error('Period not found')) | |
} | |
const billing = [] | |
let cursor = getPaginator() | |
if (cursor.isAtHead && cursor.isAtTail) { | |
billing.push(...getStationInPage()) | |
// genCsv(period, billing) | |
resolve({ period, billing }) | |
} | |
async function getBillingAndNext () { | |
cursor = getPaginator() | |
billing.push(...getStationInPage()) | |
if (!cursor.isAtTail) { | |
await waitUntilPageReload(() => { | |
gotoPage(cursor.nextEle) | |
}) | |
getBillingAndNext() | |
} else { | |
// pageObserver.disconnect() | |
// genCsv(period, billing) | |
resolve({ period, billing }) | |
} | |
} | |
function gotoPage (pageEle) { | |
pageEle.querySelector('a').click() | |
} | |
if (!cursor.isAtHead) { | |
await waitUntilPageReload(() => { | |
gotoPage(cursor.headEle) | |
}) | |
getBillingAndNext() | |
} else { | |
getBillingAndNext() | |
} | |
}) | |
} | |
async function switchPeriod (period) { | |
await waitUntilPageReload(() => { | |
const periodEle = document.querySelector('.custom-select') | |
periodEle.value = period | |
// generate change event manually XD | |
const evt = document.createEvent('HTMLEvents') | |
evt.initEvent('change', false, true) | |
periodEle.dispatchEvent(evt) | |
}) | |
} | |
if (downloadOnlyCurrentPeriod) { | |
const { period, billing } = await getStationBilling() | |
genCsv(period, billing) | |
} else { | |
const periodList = Array | |
.from(document.querySelectorAll('.custom-select option')) | |
.map(ele => ele.value) | |
const crossPeriodBilling = {} | |
for (const period of periodList) { | |
await switchPeriod(period) | |
console.log(`====== Start crawling ${period} ======`) | |
const { billing } = await getStationBilling() | |
billing.forEach(({ title, profit }) => { | |
if (!crossPeriodBilling[title]) { | |
crossPeriodBilling[title] = {} | |
} | |
crossPeriodBilling[title][period] = profit | |
}) | |
} | |
genUnionCsv(periodList, crossPeriodBilling) | |
} | |
})(true) | |
// 把上一行的 `true` 改成 `false` ,就可以一口氣爬全部~~ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
陽光福特家每期帳單匯出器
為了避免每兩個月就要記帳記到往生,所以寫了這支小工具,把每期帳單細目匯出成 csv ,方便記帳。
用法
true
改成false
,再貼到瀏覽器上,就會看到網頁自己動啦聲明
本程式以 MIT 授權無償使用與各種利用,但不保證效用,有問題請留言~