跟着近些年消费结构的改动,抵抗消费主义的行为越来越流行,越来越多的年轻人开端意识到个人财政管理的重要性。与此一起,年轻人也越来越意识到,假如他们想要完结自己的财政方针,如购买房屋、投资、旅游等,就需求把握有用的财政管理技术。因而,记账现已成为了日常生活中不可或缺的一部分。
但于此一起,真实尝试过记账的小伙伴许多,能年复一年坚持下来的却很少。原因在于记账是一个过分机械性的行为,且很容易因为或主观或客观的因素忘记漏记。这样记账变得枯燥乏味的一起,数据还不行牢靠,无法精确地记载消费趋势,然后导致记账的动力缺乏。
那么,此时假如有一个能够彻底自动化的记账体系,能够彻底不需求你手动录入每一笔买卖,全自动完结一切的数据清洗和分类,并自动地剖析消费趋势,终究以图表等形式生成报表,直观地展现出来,你会不会觉得很便利,记账的动力也会大大增强呢?这便是我近些年来一直在考虑和优化的财政管理方针。
记账模板展现
先展现成果,以增强咱们的记账动力。我运用了腾讯文档作为一切账单汇总的数据源。因为它支持多人协作,且能够随时随地在任何设备上面修改,且 UI 规划超卓,十分适合作为小数据量的账单数据库运用。
以下在不一起期依据本身需求优化出来的财政报表,从上面能够很明晰直观地看到自己的月度消费趋势,以及各个消费类别在每个月的占比,均摊及统计变化趋势。
甚至依据本身定制的需求,还能够展现运用的付出方法占比、每年各类别均摊开销及变化趋势等,都能很直观的以图表的形式展现出来。
当然只展现全体的显现作用,详细的数据涉及个人隐私都打了厚码,影响美观见谅。
那么接下来就开端从零开端逐一解说怎么规划一个这样的自动记账体系了。
一个彻底自动化的记账体系,大体由三个重要部分组成:数据获取、数据清洗和数据剖析。我将依据这三个部分顺次介绍。
数据获取
数据获取是一个彻底自动化记账体系的第一步,也是最重要的一步。买卖数据的精确性以及信息丰厚度直接决定了咱们后续的数据清洗和数据剖析的作用。因而,咱们需求尽或许挑选信息量更大的数据源,以获取更多的每条买卖更多的上下文信息,然后削减进一步人工标示的干涉。
对中国大陆来说,微信和付出宝便是国民级的付出软件,它满足普及和便利,简直不需求大多数人改动买卖习惯。此外,它们的买卖数据也是最丰厚的。因而,咱们的数据获取就以微信和付出宝为主。
但很不幸的是,与国外一些信用卡每月会定时发送账单邮件不同,微信和付出宝并不会自动推送买卖账单,而且没有开放获取买卖数据的 API,且安全性校验很杂乱,也无法运用无头的爬虫来获取数据。因而咱们需求经过自行操作来获取买卖账单。(当然,假如由乐意折腾的小伙伴也能够考虑模拟用户的行为获取账单)
微信付出账单获取
微信付出账单的获取需求在移动端完结(究竟小而美)。需求在自己的微信 APP 中,顺次点击我
=> 服务
=> 钱包
=> 账单
=> 常见问题
=> 下载账单
=> 用于个人对账
,输入账单时刻
和 邮箱地址
,即可将账单发送到你的邮箱中。下载到本地后,输入发送到微信的暗码,即可获取微信付出账单。
可是,微信付出的账单只能获取最近三个月的数据。因而,咱们最多每三个月就需求更新下载账单。
付出宝账单获取
付出宝账单的获取需求在 PC 端付出宝官网完结。登录后挑选 我是个人用户
=> 我的付出宝
=> 买卖记载
,就能够挑选对应的买卖时刻段,挑选下载账单为 Excel 格局
,即可获取到付出宝账单。
其他数据源
当然,假如微信和付出宝不是你首要运用的买卖东西。其实一切的银行 APP 都提供打印流水服务,彻底能够运用你首要运用的买卖东西来作为数据源。当然详细入口或许会有区别,且单条买卖记载的信息丰厚度或许缺乏,需求手动补充,在此就不加赘述了。
数据清洗
数据清洗是一个记账体系很重要的一步。因为这种自动化导出的账单必定是十分杂乱的,不同的数据源,其买卖记载的格局都不一样,且信息丰厚度也不一样。因而,咱们需求对不同的数据源进行不同的数据清洗。
数据清洗的意图,首先是一致好来自不同数据源的格局以便利接下来的归纳剖析,其次是除掉去一些咱们不关心的噪声,例如我只想记载 100 元以上的大宗买卖,或许我不期望个人不同账户直接的转账被计入进来等等,这部分也彻底能够依据本身的需求进行定制。
观前重要提示:
假如有读者彻底没有编程根底也彻底不用害怕,能够在 Excel 中自行操作出一模一样的格局,再兼并数据即可,运用代码仅仅为了提升自动化效率。当然,我之后也将开源一切脱敏的预处理与训练代码,有兴趣的小伙伴能够自行尝试。
以下的脚本都以 typescript
为例,也彻底能够运用任何你所熟悉的语言。
下面依然以微信和付出宝为例,来介绍怎么进行数据清洗。
首先是一致各个数据源的账单格局。假如咱们打开了在上一步中获取的微信与付出宝的账单文件,会发现它们之间其实有许多信息都很类似,能够经过一定的规则兼并起来。例如,它们都有买卖时刻、买卖类型、买卖对方、买卖金额等信息。因而,咱们能够将它们一致为一个格局:
interface IBookKeepingRow {
买卖时刻:Date;
类型:RecordType | "";
"金额(元)": string;
"收/支": "开销" | "收入" | "/";
付出方法:string;
买卖对方:string;
产品名称:string;
补白:string;
}
找到了方针格局,接下来咱们来看看怎么将数据源进行转换。
微信付出账单清洗
运用记事本打开微信付出的账单 csv,会发现它的格局类似于:
微信付出账单明细,,,,,,,,
微信昵称:[Duang],,,,,,,,
起始时刻:[2023-06-04 00:00:00] 停止时刻:[2023-07-04 00:00:00],,,,,,,,
导出类型:[悉数],,,,,,,,
导出时刻:[2023-07-25 18:08:39],,,,,,,,
,,,,,,,,
共 44 笔记载,,,,,,,,
收入:3 笔 1145.14 元,,,,,,,,
开销:40 笔 2333.33 元,,,,,,,,
中性买卖:1 笔 888.00 元,,,,,,,,
注:,,,,,,,,
1. 充值/提现/理财通购买/零钱通存取/信用卡还款等买卖,将计入中性买卖,,,,,,,,
2. 本明细仅展现当时账单中的买卖,不包括已删除的记载,,,,,,,,
3. 本明细仅供个人对账运用,,,,,,,,
,,,,,,,,
----------------------微信付出账单明细列表--------------------,,,,,,,,
买卖时刻,买卖类型,买卖对方,产品,收/支,金额(元), 付出方法,当时状态,买卖单号,商户单号,补白
2023-07-04 12:05:22, 商户消费,甜甜花酿鸡,"xxx", 开销,6.00, 零钱通,付出成功,xxx,xxx,"/"
2023-07-02 19:42:54, 扫二维码付款,愚人众,"收款方补白:二维码收款", 开销,15.00, 零钱通,已转账,xxx,xxx,"/"
...
咱们能够看出,微信付出的账单中,第一行到第 14 行都是无用的信息,咱们需求越过这些行,从第 15 行开端读取数据。一起,咱们还需求将 买卖时刻
、买卖类型
、买卖对方
、产品
、收/支
、金额(元)
、付出方法
、补白
这些信息去除空格后提取出来,其余的信息如买卖单号等都能够依据需求疏忽掉。
我给咱们一个我自己常用的模板列头供咱们参阅:
接下来咱们就能够来看看怎么处理数据使得账单的摆放符合咱们自己设置的模板。
下面我经过 fast-csv
库读取 csv 文件并挑选相关数据,终究回来一个 IBookKeepingRow[]
数组。
export async function wechatPayFormatter(
sourceFilePath: string
): Promise<IBookKeepingRow[]> {
const csvStream = fs.createReadStream(sourceFilePath);
return new Promise((resolve, reject) => {
const bookKeepingRows: IBookKeepingRow[] = [];
parseStream<IWechatBillRow, IBookKeepingRow>(csvStream, {
headers: true,
ignoreEmpty: true,
skipLines: 14,
trim: true,
})
.transform((row: IWechatBillRow) => {
const bookKeepingRow: IBookKeepingRow = {
买卖时刻:new Date(row. 买卖时刻),
类型:"",
"金额(元)": row["金额(元)"].replace(//g, ""),
"收/支": row["收/支"],
付出方法:row. 付出方法 === "亲属卡" ? "亲属卡" : "微信付出",
买卖对方:row. 买卖对方,
产品名称:row. 产品,
补白:row. 补白 === "/" ? "" : row. 补白,
};
return bookKeepingRow;
})
.on("data", (row: IBookKeepingRow) => {
bookKeepingRows.push(row);
})
.on("end", () => {
resolve(bookKeepingRows);
})
.on("error", (error) => reject(error));
});
}
付出宝账单清洗
对付出宝账单的清洗也采用类似的逻辑。
不过这儿需求留意是,付出宝账单导出的 csv 是 gbk 格局的,并不能被包括 fast-csv 在内的许多 csv 解析库处理,因而不能运用 stream 的方法直接读取,需求先做一个转码的操作。
别的,付出宝除了上面几行记载了账单总览信息外,最下面几行也加了几行统计信息,因而在清洗时也需求同时除掉。
export async function aliPayFormatter(
sourceFilePath: string
): Promise<IBookKeepingRow[]> {
const csvBuffer = fs.readFileSync(sourceFilePath);
// 将 GBK 编码转换为 UTF-8 编码
const utf8Csv = iconv.decode(csvBuffer, "gbk");
const lines = utf8Csv.split("\n");
// 截取以"--------"最初的行中间的一切行
let flag = false;
const csvTableString = lines
.filter((line: string) => {
const isDividerLine = line.startsWith("--------");
if (!flag) {
if (isDividerLine) {
flag = true;
}
return false;
}
if (isDividerLine) {
flag = false;
return false;
}
return true;
})
.map((line: string) => line.replace(/[\s]+,/g, ","))
.join("");
return new Promise((resolve, reject) => {
const bookKeepingRows: IBookKeepingRow[] = [];
parseString<IAlipayBillRow, IBookKeepingRow>(csvTableString, {
headers: true,
ignoreEmpty: true,
})
.transform((row: IAlipayBillRow) => {
const bookKeepingRow: IBookKeepingRow = {
买卖时刻:new Date(row. 买卖创立时刻),
类型:"",
"金额(元)": row["金额(元)"],
"收/支": row["收/支"] === "不计出入" ? "/" : row["收/支"],
付出方法:"付出宝",
买卖对方:row. 买卖对方,
产品名称:row. 产品名称,
补白:row. 补白,
};
return bookKeepingRow;
})
.on("data", (row: IBookKeepingRow) => {
bookKeepingRows.push(row);
})
.on("end", () => {
resolve(bookKeepingRows);
})
.on("error", (error) => reject(error));
});
}
这样,将以上两个 IBookKeepingRow 数组兼并起来,并按照买卖时刻排序,就能够得到一个完好的买卖账单记载了。
const bookKeepRecords = [...aliPayRecords, ...wechatPayRecords].sort(
(a, b) => {
return a. 买卖时刻。getTime() - b. 买卖时刻。getTime();
}
);
数据降噪处理
兼并账单仅仅数据清洗的第一步,咱们还需求对数据进行一些预处理,清洗掉一些无关噪声,便于后续的数据剖析。
不过因为这一部分的定制需求十分高,每个人都有自己的理由为自己的账单定制挑选逻辑,去除一部分数据,保留一部分数据。因而这一章节,我将不进行过多的代码解说,仅仅抛砖引玉,简略介绍一下我自己的数据预处理逻辑的思路。
首先是金融相关出入的分类。例如许多人在付出宝或微信中买了理财产品(余额宝这种也算),还有银行间的转入转出,以及理财收益等,这些都是金融相关的出入,它们的收益也会体现在导出的账单中,咱们需求将他挑选出来。
我的观察是,这些金融相关的出入,其 收/支
一栏都会显现为 /
或 不计出入
。因而,咱们能够经过这个特征来挑选出来。
这儿需求留意的是,这儿会有一些 Corner Case
需求处理,因为涉及到个人消费偏好,每个人的设置都会需求微调一下。例如在微信付出或许付出宝的账单中,有一些退款的记载,也会显现为 /
。可是这些退款的记载,其 产品名称
一栏都会包括 退款
字样,因而咱们能够经过这个特征来反向挑选出来。
// 优先去掉非出入部分
const filteredBookKeepRecords = bookKeepRecords.filter((row) => {
return !(row["收/支"] === "/" && !row["产品名称"].includes("退款"));
});
同理,咱们也能够相同挑选出来一些不需求的数据,例如我不想记载某个个人账户之间的转账,那么咱们能够经过 买卖对方
这一栏来挑选出来放弃掉。又例如,我只想记载 100 元以上的大宗买卖,那么咱们能够经过 金额(元)
这一栏来挑选出来。
经过这样的方法,咱们完结了最重要的一步,数据清洗,使得账面整齐且明晰了起来。
拥有了一致且规范化的数据格局,接下来咱们就能安心肠在类型下填入该笔消费的类型,对整个账单数据进行分类,然后得知自己的各项消费趋势了。
小结
到这儿,咱们现已完结了一个完好的数据清洗流程,将来自不同数据源的账单数据一致为一个格局,且去除了一些咱们不关心的噪声,使得数据更加干净整齐,便于后续的更进阶的数据剖析了。
什么?你说我是标题党?这仍是需求我自己手动一个个进行分类?
没错。但本篇仅仅一个根底篇,它帮助你处理了账单数据源从无到有,从杂乱到规范的进程,这一进程通常是最繁琐的,本篇将这一流程完结了自动化处理。能为有需求的同学省下了许多时刻。
但假如你问,有没有更智能化的方法,能将账单分类的进程也给我省掉了,不需求分类操作,躺着就能自动更新获取我近年来一切的消费趋势。我会说,有的。咱们能够经过搜集一些历史消费数据,经过建立机器学习模型来完结预测。
接下来的数据剖析,便是一个十分有趣的进程了,咱们将在下一篇中进行解说。