alipay-bills
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -1,4 +0,32 @@ | ||
import query from './public/query'; | ||
import setUser from './public/set-user'; | ||
import options from './public/options'; | ||
export { query, setUser, options }; | ||
export declare const options: { | ||
debug: boolean; | ||
interval: number; | ||
params: {}; | ||
}; | ||
export interface Bill { | ||
createTime?: number; | ||
memo: string; | ||
name: string; | ||
orderNo?: string; | ||
tradeNo?: string; | ||
target: string; | ||
amount: number; | ||
status: string; | ||
} | ||
/** | ||
* 监听事件 | ||
* @param {string} name | ||
* @param {function} handler | ||
*/ | ||
export declare function on(name: string, handler: (...args: any[]) => any): void; | ||
/** | ||
* 使用指定的用户名和密码开始循环读取数据 | ||
* @param {string} user - 用户名 | ||
* @param {string} pwd - 密码 | ||
*/ | ||
export declare function start(user: string, pwd: string): Promise<void>; | ||
/** | ||
* 停止刷新 | ||
* @return {Promise<void>} | ||
*/ | ||
export declare function stop(): Promise<void>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var query_1 = require("./public/query"); | ||
exports.query = query_1.default; | ||
var set_user_1 = require("./public/set-user"); | ||
exports.setUser = set_user_1.default; | ||
var options_1 = require("./public/options"); | ||
exports.options = options_1.default; | ||
var tslib_1 = require("tslib"); | ||
var EventEmitter = require("events"); | ||
var querystring_1 = require("querystring"); | ||
var selenium_webdriver_1 = require("selenium-webdriver"); | ||
var create_driver_1 = require("./utils/create-driver"); | ||
var constants_1 = require("./utils/constants"); | ||
var titleIs = selenium_webdriver_1.until.titleIs, elementLocated = selenium_webdriver_1.until.elementLocated; | ||
var driver; | ||
var logged = false; | ||
var timeId; | ||
var stopped = true; | ||
var lastTradeNo; | ||
var refreshing; | ||
var event = new EventEmitter(); | ||
exports.options = { | ||
debug: false, | ||
interval: 60 * 1000, | ||
params: {} | ||
}; | ||
/** | ||
* 在控制台输出日志 | ||
* @param msg | ||
*/ | ||
function log(msg) { | ||
if (exports.options.debug) { | ||
console.log("[" + new Date().toLocaleString() + "]", msg); | ||
} | ||
} | ||
/** | ||
* 模拟人类的输入行为,输入每个字符之间间隔一段时间 | ||
* @param {WebElementPromise} ele | ||
* @param {string} str | ||
* @return {Promise<void>} | ||
*/ | ||
function humanInput(ele, str) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var i; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, ele.clear()]; | ||
case 1: | ||
_a.sent(); | ||
i = 0; | ||
_a.label = 2; | ||
case 2: | ||
if (!(i < str.length)) return [3 /*break*/, 6]; | ||
return [4 /*yield*/, ele.sendKeys(str[i])]; | ||
case 3: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.sleep(500)]; | ||
case 4: | ||
_a.sent(); | ||
_a.label = 5; | ||
case 5: | ||
i++; | ||
return [3 /*break*/, 2]; | ||
case 6: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* 使用当前的账号与密码登录并将 driver 跳转到「我的账户」高级版 | ||
* @param {string} user - 用户名 | ||
* @param {string} pwd - 密码 | ||
* @return {Promise<void>} | ||
*/ | ||
function login(user, pwd) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var elements, e_1; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!driver) | ||
driver = create_driver_1.default(); | ||
return [4 /*yield*/, logout()]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 19, , 21]); | ||
// 跳转到登陆页 | ||
return [4 /*yield*/, log('正在跳转到登陆页……')]; | ||
case 3: | ||
// 跳转到登陆页 | ||
_a.sent(); | ||
return [4 /*yield*/, driver.get(constants_1.URLs.login + '?goto=' + encodeURIComponent(constants_1.URLs.billsIndex)) | ||
// 输入用户名 | ||
]; | ||
case 4: | ||
_a.sent(); | ||
// 输入用户名 | ||
return [4 /*yield*/, log('正在输入用户名……')]; | ||
case 5: | ||
// 输入用户名 | ||
_a.sent(); | ||
return [4 /*yield*/, humanInput(driver.findElement({ id: 'J-input-user' }), user) | ||
// 输入密码 | ||
]; | ||
case 6: | ||
_a.sent(); | ||
// 输入密码 | ||
return [4 /*yield*/, log('正在输入密码……')]; | ||
case 7: | ||
// 输入密码 | ||
_a.sent(); | ||
return [4 /*yield*/, humanInput(driver.findElement({ id: 'password_rsainput' }), pwd) | ||
// 点击登陆按钮 | ||
]; | ||
case 8: | ||
_a.sent(); | ||
// 点击登陆按钮 | ||
return [4 /*yield*/, log('正在点击登陆按钮……')]; | ||
case 9: | ||
// 点击登陆按钮 | ||
_a.sent(); | ||
return [4 /*yield*/, driver.sleep(500) | ||
// 故意没有 await 下面的语句,这样 5 秒内检测到没跳转到账单页就重试 | ||
]; | ||
case 10: | ||
_a.sent(); | ||
// 故意没有 await 下面的语句,这样 5 秒内检测到没跳转到账单页就重试 | ||
driver.findElement({ id: 'J-login-btn' }).click(); | ||
// 确认浏览器跳转到了账单页 | ||
return [4 /*yield*/, log('正在等待浏览器跳转到账单页……')]; | ||
case 11: | ||
// 确认浏览器跳转到了账单页 | ||
_a.sent(); | ||
return [4 /*yield*/, driver.wait(titleIs(constants_1.BillPageTitle), 5000, '5 秒内没有跳转到账单页') | ||
// 判断是否在「我的账单」高级版页面 | ||
]; | ||
case 12: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.findElements({ id: 'main' })]; | ||
case 13: | ||
elements = _a.sent(); | ||
if (!!elements.length) return [3 /*break*/, 17]; | ||
return [4 /*yield*/, log('检测到当前是标准版账单页,正在跳转到高级版账单页……')]; | ||
case 14: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.get(constants_1.URLs.billsSwitch)]; | ||
case 15: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.wait(elementLocated({ id: 'main' }), 5000, '5 秒内没有跳转到高级版账单页')]; | ||
case 16: | ||
_a.sent(); | ||
_a.label = 17; | ||
case 17: | ||
logged = true; | ||
return [4 /*yield*/, log('登陆成功')]; | ||
case 18: | ||
_a.sent(); | ||
return [3 /*break*/, 21]; | ||
case 19: | ||
e_1 = _a.sent(); | ||
return [4 /*yield*/, log('尝试登陆时失败:「' + e_1.message + '」,正在重试……')]; | ||
case 20: | ||
_a.sent(); | ||
return [2 /*return*/, login(user, pwd)]; | ||
case 21: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* 停止循环并退出登录 | ||
* @return {Promise<void>} | ||
*/ | ||
function logout() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!(driver && logged)) return [3 /*break*/, 2]; | ||
log('准备登出账号……'); | ||
logged = false; | ||
clearTimeout(timeId); | ||
return [4 /*yield*/, driver.get(constants_1.URLs.logout)]; | ||
case 1: | ||
_a.sent(); | ||
log('已登出账号。'); | ||
_a.label = 2; | ||
case 2: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* 循环刷新页面并读取数据 | ||
* @return {Promise<void>} | ||
*/ | ||
function refresh() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var _this = this; | ||
return tslib_1.__generator(this, function (_a) { | ||
if (stopped) | ||
return [2 /*return*/]; | ||
refreshing = new Promise(function (resolve) { return tslib_1.__awaiter(_this, void 0, void 0, function () { | ||
var bills; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, query(lastTradeNo)]; | ||
case 1: | ||
bills = _a.sent(); | ||
if (bills.length) { | ||
lastTradeNo = bills[0].tradeNo; | ||
event.emit('new bills', bills); | ||
} | ||
resolve(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
timeId = setTimeout(refresh, exports.options.interval); | ||
return [2 /*return*/]; | ||
}); | ||
}); | ||
} | ||
/** | ||
* 查询账单数据 | ||
* @param {string} tradeNo - 根据流水号判断订单查询应该何时结束 | ||
* @param params - 页面参数 | ||
* @return {Promise<Bill[]>} | ||
*/ | ||
function query(tradeNo, params) { | ||
if (params === void 0) { params = exports.options.params; } | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
function queryPage() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var pageBills, tradeIndex, els; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!driver) | ||
return [2 /*return*/]; | ||
return [4 /*yield*/, driver.get(constants_1.URLs.billsAdvanced + '?' + querystring_1.stringify(Object.assign({ page: page }, params)))]; | ||
case 1: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.wait(elementLocated({ id: 'tradeRecordsIndex' }), 5000)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, driver.executeScript(getBills) | ||
// 如果没有设置终点流水号,则只返回第一页的数据 | ||
]; | ||
case 3: | ||
pageBills = _a.sent(); | ||
// 如果没有设置终点流水号,则只返回第一页的数据 | ||
if (!tradeNo) { | ||
bills.push.apply(bills, pageBills); | ||
return [2 /*return*/]; | ||
} | ||
pageBills.some(function (bill, index) { | ||
if (bill.tradeNo === tradeNo) { | ||
tradeIndex = index; | ||
return true; | ||
} | ||
return false; | ||
}); | ||
// 如果找到了匹配的流水号,则将前面的账单数据推入数组中并中断查询 | ||
if (typeof tradeIndex === 'number') { | ||
bills.push.apply(bills, pageBills.slice(0, tradeIndex)); | ||
return [2 /*return*/]; | ||
} | ||
// 没有找到匹配的流水号,则把这页数据全都推入数组 | ||
bills.push.apply(bills, pageBills); | ||
return [4 /*yield*/, driver.findElements({ className: 'page-next' })]; | ||
case 4: | ||
els = _a.sent(); | ||
if (els.length) { | ||
page += 1; | ||
return [2 /*return*/, queryPage()]; | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
var bills, page; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!logged) | ||
return [2 /*return*/, Promise.reject(new Error('Login first.'))]; | ||
bills = []; | ||
page = 1; | ||
return [4 /*yield*/, queryPage()]; | ||
case 1: | ||
_a.sent(); | ||
return [2 /*return*/, bills]; | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* 这个函数会运行在浏览器里通过 DOM 分析出账单数据 | ||
*/ | ||
function getBills() { | ||
var bills = []; | ||
// 支付宝网站用了 jQuery 所以这里可以使用 | ||
jQuery('#tradeRecordsIndex tbody tr').each(function () { | ||
var $tr = jQuery(this); | ||
var bill = { | ||
memo: $tr.find('td.memo .memo-info').text().trim(), | ||
name: $tr.find('td.name a').text().trim(), | ||
target: $tr.find('td.other .name').text().trim(), | ||
amount: Number($tr.find('td.amount .amount-pay').text().trim().replace(/\s+/g, '')), | ||
status: $tr.find('td.status').text().trim() | ||
}; | ||
bills.push(bill); | ||
// 通过「操作」里的「备注」链接获取到精确度到秒的时间戳 | ||
var link = $tr.find('.action [data-action="edit-memo"]').attr('data-link'); | ||
var match = link && link.match(/&createDate=\s*(\d+)/); | ||
var createDateStr = match ? match[1] : null; | ||
if (createDateStr) { | ||
var year = Number(createDateStr.slice(0, 4)); | ||
var month = Number(createDateStr.slice(4, 6)) - 1; | ||
var day = Number(createDateStr.slice(6, 8)); | ||
var hour = Number(createDateStr.slice(8, 10)); | ||
var minute = Number(createDateStr.slice(10, 12)); | ||
var second = Number(createDateStr.slice(12, 14)); | ||
bill.createTime = new Date(year, month, day, hour, minute, second).getTime(); | ||
} | ||
// 获取账单的订单号、交易号或流水号。 | ||
// 个人对个人的账单只有流水号, | ||
// 个人与商户之间的交易会有订单号和交易号。 | ||
// 支付宝对交易号和流水号是同等对待的。 | ||
var tradeStr = $tr.find('td.tradeNo').text().trim(); | ||
var nos = tradeStr.split('|'); | ||
nos.forEach(function (noStr) { | ||
var _a = noStr.trim().split(':'), key = _a[0], value = _a[1]; | ||
switch (key) { | ||
case '交易号': | ||
case '流水号': | ||
bill.tradeNo = value; | ||
break; | ||
case '订单号': | ||
bill.orderNo = value; | ||
break; | ||
} | ||
}); | ||
}); | ||
return bills; | ||
} | ||
/** | ||
* 监听事件 | ||
* @param {string} name | ||
* @param {function} handler | ||
*/ | ||
function on(name, handler) { | ||
event.on(name, handler); | ||
} | ||
exports.on = on; | ||
/** | ||
* 使用指定的用户名和密码开始循环读取数据 | ||
* @param {string} user - 用户名 | ||
* @param {string} pwd - 密码 | ||
*/ | ||
function start(user, pwd) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var loginPromise; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!!stopped) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, stop()]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
loginPromise = login(user, pwd); | ||
loginPromise.then(refresh); | ||
return [2 /*return*/, loginPromise]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.start = start; | ||
/** | ||
* 停止刷新 | ||
* @return {Promise<void>} | ||
*/ | ||
function stop() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!driver) | ||
return [2 /*return*/]; | ||
stopped = true; | ||
return [2 /*return*/, refreshing]; | ||
}); | ||
}); | ||
} | ||
exports.stop = stop; |
{ | ||
"name": "alipay-bills", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "自动获取支付宝的账单信息。", | ||
@@ -11,4 +11,4 @@ "main": "libs/index.js", | ||
"scripts": { | ||
"build": "tsc", | ||
"prepublish": "rm -rf libs && npm run build" | ||
"build": "rm -rf libs && tsc", | ||
"prepublish": "npm run build" | ||
}, | ||
@@ -19,3 +19,2 @@ "dependencies": { | ||
"@types/selenium-webdriver": "^3.0.4", | ||
"noshjs": "^0.1.3", | ||
"phantomjs-prebuilt": "^2.1.15", | ||
@@ -22,0 +21,0 @@ "selenium-webdriver": "^3.5.0", |
@@ -13,3 +13,3 @@ # alipay-bills | ||
alipay-bills 只有两个方法: | ||
alipay-bills 只有三个方法: | ||
@@ -20,20 +20,37 @@ ```js | ||
alipayBills.options.debug = true // 在控制台输出日志 | ||
alipayBills.options.interval = 5000 // 间隔多久刷新一次,默认是 60000 毫秒(即一分钟) | ||
alipayBills.options.params = { // 自定义「我的账单」页面的查询参数,默认没有任何参数。具体有哪些参数可以通过 Chrome 开发者工具分析得出。 | ||
// 部分参数举例: | ||
status: 'success', // 交易状态:成功 | ||
fundFlow: 'out' // 资金流向:支出 | ||
} | ||
// alipayBills 的接口都是异步的,强烈推荐使用 async/await 语法 | ||
;(async function () { | ||
// 请确保你的用户名和密码是正确的,否则会无限重试登录。 | ||
// 如果你已经登录过了,重新调用这个方法会先将已经登录的账号退出。 | ||
await alipayBills.setUser('用户名', '密码') | ||
await alipayBills.query().then(bills => { | ||
// 先监听 `new bills` 事件,每次检测到新账单时会触发这个事件 | ||
alipayBills.on('new bills', bills => { | ||
// bills 是一个数组,数组中每一项的结构为: | ||
//{ | ||
// day: string - 日期,如 '2017.08.15' | ||
// time: string - 具体时间,如 '04:10' | ||
// name: string - 对应「我的账单高级版」的「名称」 | ||
// orderNo: string - 对应「我的账单高级版」的「商户订单号|交易号」 | ||
// createTime: number - 精确到「秒」的创建时间戳 | ||
// memo: string - 对应「我的账单高级版」中的「备注」 | ||
// name: string - 对应「我的账单高级版」中的「名称」 | ||
// orderNo: string - 对应「我的账单高级版」的「订单号」,可能为空 | ||
// tradeNo: string - 对应「我的账单高级版」的「交易号」或者「流水号」 | ||
// target: string - 对应「我的账单高级版」的「对方」 | ||
// amount: string - 对应「我的账单高级版」的「金额」 | ||
// amount: number - 对应「我的账单高级版」的「金额」,正数代表收入,负数代表支出 | ||
// status: string - 对应「我的账单高级版」的「状态」 | ||
//} | ||
}) | ||
// 请确保你的用户名和密码是正确的,否则会无限重试登录。 | ||
// 调用 start 方法后每隔一段时间(具体时间为前面设置的 `options.interval`)就会刷新一次并检测是否有新账单。 | ||
// 第一次检测只会将「我的账单」高级版第一页的账单数据传给 `new bills` 事件, | ||
// 后续会循环查询每一页的账单数据,直到碰到上一次查询时的第一个账单信息为止。 | ||
await alipayBills.start('用户名', '密码') | ||
// 停止刷新 | ||
await alipayBills.stop() | ||
// 重新调用 start 方法会先退出前面登录过的账号 | ||
await alipayBills.start('另一个用户名', '另一个密码') | ||
}()) | ||
@@ -46,3 +63,3 @@ ``` | ||
代码参考了[利用『爬虫』 折衷解决 个人支付宝支付系统 ---- 获取账单信息](https://www.v2ex.com/t/383179)这篇帖子。 | ||
部分代码参考了[利用『爬虫』 折衷解决 个人支付宝支付系统 ---- 获取账单信息](https://www.v2ex.com/t/383179)这篇帖子。 | ||
@@ -49,0 +66,0 @@ ## 许可 |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
21323
6
460
66
1
8
- Removednoshjs@^0.1.3
- Removednoshjs@0.1.3(transitive)