
文章目录隐藏
没错,我回来更新了!没想到上篇文章安利效果超强,所以我就打算先行跳过第二篇,先介绍个人方案以便各位参考啦。至于第二篇的详细做账方法就慢慢摸吧。不过由于这篇文章按照规划来讲还是第三篇,所以会有一些虚空引用第二篇的部分内容,如果看到的话请假装看过第二篇吧(逃)
相信对于看过前两篇还依旧选择点开第三篇的你来说,Beancount记账应该是有一定吸引力的。所以我觉得有必要在介绍方案之前分析下我们对记账究竟是怎么一个需求。当然我非你,所以这里就谈谈我个人对记账的需求吧。
其中第一点依靠的是妥善的账户设计,在第一篇中已经有所介绍。第二点中关于各类资产的管理、做账也已经在第二篇中介绍了。因此本篇文章的重点就是解决后三点提到的需求。
由于目标是使“手动完成的部分尽可能的少”,因此账单导入自然是离不开的选择。但很遗憾,实践证明账单导入其实问题多多:金额“来往”不明确,“往”基本没有;账单描述大部分情况都不符合要求,只是订单号;经常产生重复交易(比如转账)。就算是通过自己编写导入脚本能只能改善其中的一部分问题,因为账单本身没有记录的信息是没办法变出来的(比如消费类别)。当然,你也可以在每月腾出一天专门用来整理帐,但这又失去了“对‘当前’开销情况的直观认识”——你永远只能在账本里看到上个月的账。
考虑到需求,这样的一个工作流才是最理想的:平时消费后可以随时手动记账,而想导入账单的时候又能迅速完成。虽然这样导入账单依旧是数据的主要来源,但更准确的手动记账却可以作为其补充,平摊开导入、对账时花费的精力。因此我的方案是:Telegram机器人完成“快速随时记账”、自己编写导入脚本实现“增量”导入。这个方案的优势是显而易见的:
为了证明这套方案的高效,我特意对2021-10-01至2021-10-14的账单进行了一次对账,效果如下:
对我而言,半小时左右的时间消耗完全够我随时查账检查收支情况了。而且一般情况下也完全没有必要导入所有账户,导个支付宝、微信就差不多了,其它账户留着每月导入就行,那样耗费的时间也就更短了。
接下来,我将先逐个部分介绍我记账方案中的组件,然后再介绍部署的方法以及使用的经验。各位可以各取所需,没必要完整的阅读。
账单导入其实非常个人化,所以这一节主要介绍如何获得账单数据、修改我个人目前的脚本。
账单数据的主要来源:官方对账单、账单邮件、自食其力。
对于提供官方对账单的”带善人“,直接下载提供的对账单即可。大部分国内平台、网上银行都能下载得到。一般来说对账单是CSV、XLSX、PDF格式的,而解析的难易度刚好也是这个顺序,前二者最简单,PDF最困难。不过大部分国内平台的对账单都”挺能藏“,比如这二位:
虽然支付宝网页版也可以导出,但是那个结果缺少了支付账户,无法区分余额、花呗、银行卡、余额宝。
对于账单邮件,建议使用邮箱客户端下载eml文件。部分网页版也支持,比如阿里云邮箱。
剩下的那些就属于”自食其力“的范畴了。对于那些平台,我建议用Table Capture插件直接抓网页表格。由于免费版不能下载CSV,所以需要先复制然后粘贴到Excel里再转存CSV。
脚本方面,我果断参考了现成脚本zsxsoft/my-beancount-scripts,修改过的脚本见此modules。pip install -r requirements.txt安装环境之后就可以使用了,使用方式是python import.py --entry [账本主文件] [待导入文件],结果会生成在同目录out.bean。另外要注意,脚本使用文件名+文件特征选择合适的导入器,所以比如微信、支付宝的订单压缩包之类的都不需要重命名或解压,直接导入即可。
主要需要修改的地方是modules/accounts.py,其中记录了账单导入时的各种匹配规则,在多个导入器之间共用。其次就是modules/imports下的各种导入器,其中可能存在各种硬编码的账户。
然后就是关于增量导入的功能。核心部分的代码在modules/imports/deduplicate.py,由于我使用了Beancount本身的API进行了重写,因此只需要了解Transaction类型的结构就可以自己添加去重规则。目前的规则大致如下:对于导入的每一条交易,查询账本中是否存在交易满足
我比较推荐的工作流是每次做账都更新一下accounts.py的内容,这样在多次迭代后,导入就基本不需要手动补充任何信息了。原因之前也提过:一般日常生活中的消费都是非常规律的。在实际测试中也可以看到,自动补全的交易占到了将近70%,因此很有必要及时更新规则。

然后就是灵魂部分的Telegram机器人了,源程序已经开源:kaaass/beancount_bot。此处主要介绍相关配置。
我个人推荐用Docker省掉麻烦的部署操作。推荐使用包含Costflow插件的这个镜像:kaaass/beancount_bot_costflow_docker。部署操作也相当简单,只需要创建两个文件夹:
config:存放机器人配置。默认配置文件beancount_bot.ymlbean:存放账本之后在同目录运行以下指令启用Docker容器即可:
docker run -d \
-v ./bean:/bean \
-v ./config:/config \
kaaass/beancount_bot_costflow_docker其他参数请见相关Github仓库。
当然,如果没有配置文件的话容器会直接退出。因此建议下载仓库中的示例beancount_bot.yml进行修改。详细的修改方法参考文件注释即可,但一般来说需要修改这三个参数:
bot.proxy:代理。仅支持HTTP代理bot.token:Telegram 机器人 Token。需要向@BotFather申请,在Telegram里搜索到这个机器人,然后发送/new bot指令就能获得bot.auth_token:鉴权用令牌。第一次进入机器人时用于校验身份transaction.beancount_file:机器人记账的默认Beancount文件。可以在文件名中包含多级目录、使用{year}{month}{date}代表年月日。此外因为是在Docker中,因此需要保证路径在/bean下之后把三个配置都丢进config文件夹应该就可以顺利启动了。注意第一次使用Bot需要通过/start来鉴权。
此外,示例配置文件里还预先配置了两个交易语句处理器。它们用来将TGBOT的输入转换为Beancount语句。当然Bot也支持自定义处理器,具体实现方法可以参考仓库的Wiki。
模板是Beancount Bot内建的交易消息处理器(beancount_bot.builtin.TemplateDispatcher),虽然简单但功能却十分强大。模板语句的语法类似Shell或CMD,格式是:
指令名 必填参数 [可选参数] < 目标账户例如:饭 20 < zfb。其中,
具体的指令、账户需要在配置文件(示例配置:template.yml)中进行配置,具体可以参考示例配置文件的注释。简而言之,定义一个模板需要你先写出一条合法的Beancount语句,比如
2021-10-14 * "Vultr" "月费"
Assets:Digital:Alipay
Expenses:Tech:Cloud 5 USD之后再用一些模板变量来替换语句中的部分
{date} * "Vultr" "月费"
{account}
Expenses:Tech:Cloud 5 USD此外,模板也支持必填参数(args)、可选参数(optional_args),甚至还支持使用Python表达式计算(computed)。这些高级的用法请参考相关示例。
虽然模板语法很强大,但是终究还是需要预先配置好语句,不够灵活。而Costflow语法就可以解决这个问题,因为它几乎为Beancount的各种语句都设计了“一句话”的缩略版本。官方文档在:https://www.costflow.io/docs/syntax/。可以用Playground体验下语法:https://playground.costflow.io。Beancount Bot也为此提供了插件(kaaass/beancount_bot_costflow),如果选用了推荐的Docker的话那应该已经包含了这个插件。
这里只简单介绍下Costflow的一种交易语法:
[年月日] [*|!] [@交易对象|"对象"] ["注释"] [#tag] [^link] 数额 [货币] 原始账户 > [数额] [货币] 目标账户其中,>的语义类似Shell,意味着左侧账户转账给右侧账户,因此命令中的所有金额都是正数的。例如41 zfb > 26 日用品 + 15 零食会被转换成这个交易:
2021-10-14 * "+"
Assets:Digital:Alipay -41.00 CNY
Expenses:Life:Consumables 26.00 CNY
Expenses:Food:Snacks 15.00 CNY虽然不知道为什么目前不写注释的情况下交易描述会变成加号。不过问题也不大,因为导入的时候都会自动补全信息。此外Costflow目前的实现也有些其它BUG,不过好在原作者依旧在维护,因此遇到的话请积极在Github发Issue。
和模板一样,Costflow也需要修改配置(示例配置:costflow.json),具体建议参考文档和引用中的第二篇文章。主要需要配置的其实也就是各个账户的别名了,分享下我个人的设置思路:
Beancount Bot还有一个功能就是执行定时任务。我个人使用这个功能来进行基金、股票、外币价格的每日更新和自动定时备份。定时任务的配置和交易语句处理器很像,也都支持载入自己定义的任务。内建的定时任务类只有一个,就是每日指定时间运行若干指令的任务(beancount_bot.builtin.DailyCommandTask)。相关配置超简单:
schedule:
# 定时任务定义
# name:定时任务名,可以用 /task name 主动触发
# class:定时任务类
# args:创建任务需要的参数
# 定时任务示例:定时更新价格
# 使用内建任务类:beancount_bot.builtin.DailyCommandTask
# 该类在每日 time 时执行指令,之后广播 message 消息
- name: price
class: 'beancount_bot.builtin.DailyCommandTask'
args:
time: '21:30'
message: '当日价格更新完成'
commands:
- 'bean-price /bean/main.bean >> /bean/automatic/prices.bean'鉴于Telegram Bot一般会部署在服务器上,因此顺便搭建一个Fava来实时查看账本也是个很不错的选择。不过Fava本身没有鉴权,因此公网部署非常的危险。zsxsoft/fava-management给Fava添加了登录、重启功能,也提供了Docker镜像。不过本身Fava版本有点旧,所以我fork了一份版本较新的:kaaass/fava-management。
但是需要注意的是,使用过程中我多次遇到了CPU占用率突然飙升至100%的情况,原因暂且不明。因此也可以使用官方Fava+反代时添加Basic Auth的部署方式。
如果全部手工记账,那备份其实一点也不难。但是我们的方案却分出了两套账本:
因此就需要保证两边的账本是同步的,不然就会出现问题。
最简单的方法就是用自带文件夹同步的程序。坚果云、Google Drive等等全都OK。
但是青春版有两个问题:首先就是这需要我把自己的账本交给第三方托管,感到困惑害怕希望不要再发了;其次就是版本管理都很初级。于是我就想到使用Git来管理版本,那两个账本刚好就对应了两个分支:
master:查账、修改配置用bot:Telegram Bot进行定期备份此外,为了便于部署本地的更改,还可以使用CI在master接到push时自动覆盖文件到Bot目录。如此下来,每次查账时只需要:
origin/botmerge进master:git merge origin/bot --squash如果使用公共服务如Github,也可以用git-crypt来加密账本文件。当然代价就是Github变成了单纯的网盘。
拼接文章中的所有拼图,我们就可以得到一个最终的部署方案了。我开源了一套示例配置供大家参考:kaaass/my-beancount-template。
需要部署的服务主要就是Fava和Beancount Bot,由于两者都有提供各自的镜像,因此使用Docker部署就很方便。我自己是使用Docker Compose来管理镜像的:
version: '3'
services:
fava:
image: kaaass/fava-management
container_name: 'fava'
restart: always
ports:
- 8080:80
environment:
- BEANCOUNT_FILE=/bean/main.bean
- USERNAME=admin
- PASSWORD=123456
volumes:
- ./data:/bean
beancount_bot:
image: kaaass/my_beancount_bot_docker
container_name: 'beancount_bot'
restart: always
environment:
- TZ=Asia/Shanghai
- PYTHONPATH=/modules
volumes:
- ./data:/bean
- ./data:/config
- ./modules:/modules
- ./init.d:/init.d这里Beancount Bot并没有用官方的Docker镜像,而是自己重新建了一个(虽然官方的也是我的)。原因主要是官方镜像中没有git和openssh,所以备份脚本跑不了。然后就是把Bot的配置与Bean丢在了一起,一并使用Git进行版本控制。因此最后的文件结构大致如下:
.
├── data ; 账本、配置
│ ├── accounts ; 账户
│ ├── automatic ; 自动生成,存放导入账单、价格
│ ├── tgbot ; Bot 记账
│ └── txs ; 手工记账
├── init.d ; 初始化 Git 环境
├── modules ; 各种脚本
└── docker-compose.yml根据之前的方案,data必需是一个Git仓库。但由于Git初始化配置难以自动化,因此需要手动进行操作。由于我自己建了一个Gitea因此也就没有搞git-encrypt,如果使用Github等公共平台的话建议使用。另外,建议给机器人单独指定一个SSH密钥,有条件的话还可以单独开个账户保证账号安全。
# 初始化 Git 仓库
cd data
git init
git add .
git commit -m "初始化账本"
git remote add origin [远端地址]
git push -u master
# 初始化密钥
cd ../init.d
ssh-keygen tgbot
cp ~/.ssh/known_hosts ./至于push时执行覆盖的CI,我使用的是Drone CI。不过其实哪个CI大致的操作都一样,只需要clone后执行如下脚本即可:
if git -C /deployment diff-index --quiet HEAD; then
echo "无未提交更新,可以覆盖"
else
echo "有未提交更改,请先 /task backup 后合入 master!"
exit 1
fi
# 备份
cp /deployment/bot.session /tmp
# 删除老文件
rm -rf /deployment/*
# 部署文件
rm -f .drone.yml
rm -rf .git
cp -r ./ /deployment
cp /tmp/bot.session /deployment脚本开头还检查了当前data文件夹的状况,如果存在未提交更改就及时阻止覆盖,以免丢失记账数据。
这套记账方案从我开始调研Beancount到TGBOT编写、服务部署,再到迭代改进导入脚本,断断续续花费了我一周左右的时间。不过好在这套方案我自己用着确实很顺手,经过半个多月的使用,我已经完全习惯于在消费后打开TG发一行文本记账了。
Beancount确实是个很有趣的东西。正如我在系列开篇所言,它可以非常“Geek”。对我来说,由于Beancount本身只是记账的一个模块,只承担了记账操作的“语言”部分,因此它非常容易被用来整合进一个解决方案(简单搜索都能找到不少Beancount个人方案)。这也是我开发这个解决方案的其他部分(如Beancount Bot)的指导思想:功能最简、易于拓展。希望这篇文章能帮助更多人快速设计、规划自己的记账方案。