Skip to content

Instantly share code, notes, and snippets.

@ddio
Last active July 25, 2021 14:49
Show Gist options
  • Save ddio/d482435cf9e6214c2929df4aa2fed64d to your computer and use it in GitHub Desktop.
Save ddio/d482435cf9e6214c2929df4aa2fed64d to your computer and use it in GitHub Desktop.
(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` ,就可以一口氣爬全部~~
@ddio
Copy link
Author

ddio commented Jul 25, 2021

陽光福特家每期帳單匯出器

為了避免每兩個月就要記帳記到往生,所以寫了這支小工具,把每期帳單細目匯出成 csv ,方便記帳。

用法

  1. 登入後,進到「日期模式」的電費收益頁
  2. 打開瀏覽器後台(按 F12 或 ctrl + shift + i)
  3. 在主控台 / Console 貼上上面的程式碼,就會自動從第一頁開始抓資料,最後轉成 csv 下載~~
  4. 預設只會抓目前的日期,如果想要一次抓所有的日期,請把倒數第二行的 true 改成 false ,再貼到瀏覽器上,就會看到網頁自己動啦

聲明

本程式以 MIT 授權無償使用與各種利用,但不保證效用,有問題請留言~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment