复式记账指北(三):如何打造不半途而废的记账方案

没错,我回来更新了!没想到上篇文章安利效果超强,所以我就打算先行跳过第二篇,先介绍个人方案以便各位参考啦。至于第二篇的详细做账方法就慢慢摸吧。不过由于这篇文章按照规划来讲还是第三篇,所以会有一些虚空引用第二篇的部分内容,如果看到的话请假装看过第二篇吧(逃)

当我们兴冲冲地开始记账时,我们想要的是什么

相信对于看过前两篇还依旧选择点开第三篇的你来说,Beancount记账应该是有一定吸引力的。所以我觉得有必要在介绍方案之前分析下我们对记账究竟是怎么一个需求。当然我非你,所以这里就谈谈我个人对记账的需求吧。

  • 希望对当前的开销情况有直观的认识
  • 希望能综合各种资产的管理。包括:基金、股票、货币基金,甚至Switch卡带
  • 希望需要我手动完成的部分能尽可能的少,这样才能坚持使用下来
  • 希望随时能记账、查看财务状况,因此需要跨平台也能用(至少手机、PC)
  • 希望账本存储安全,容易备份

其中第一点依靠的是妥善的账户设计,在第一篇中已经有所介绍。第二点中关于各类资产的管理、做账也已经在第二篇中介绍了。因此本篇文章的重点就是解决后三点提到的需求。

核心思路

由于目标是使“手动完成的部分尽可能的少”,因此账单导入自然是离不开的选择。但很遗憾,实践证明账单导入其实问题多多:金额“来往”不明确,“往”基本没有;账单描述大部分情况都不符合要求,只是订单号;经常产生重复交易(比如转账)。就算是通过自己编写导入脚本能只能改善其中的一部分问题,因为账单本身没有记录的信息是没办法变出来的(比如消费类别)。当然,你也可以在每月腾出一天专门用来整理帐,但这又失去了“对‘当前’开销情况的直观认识”——你永远只能在账本里看到上个月的账。

考虑到需求,这样的一个工作流才是最理想的:平时消费后可以随时手动记账,而想导入账单的时候又能迅速完成。虽然这样导入账单依旧是数据的主要来源,但更准确的手动记账却可以作为其补充,平摊开导入、对账时花费的精力。因此我的方案是:Telegram机器人完成“快速随时记账”、自己编写导入脚本实现“增量”导入。这个方案的优势是显而易见的:

  1. 手动记账快速而且只作为补充,所以就算忘了记账也没事
  2. 日常开销其实大部分都很规律,因此很容易写一些导入规则直接匹配出消费类型(比如点外卖),连手动记账都不需要
  3. 导入、对账需要的工作量大大降低,而且可以只完成一部分。所以想了解支出状况的时候随时导入都行
  4. 多平台:Telegram支持啥平台,啥平台就能记账;啥平台能打开Fava网页,啥平台就能看账本

为了证明这套方案的高效,我特意对2021-10-01至2021-10-14的账单进行了一次对账,效果如下:

  • 总共导入了5个平台的数据,共计交易89项
  • 导入用时28分钟左右
  • 需要手动补充信息的只有8项交易,其中5项来自支付宝,3项来自微信
  • Telegram记录了19项交易,通过导入规则自动匹配了61项交易

对我而言,半小时左右的时间消耗完全够我随时查账检查收支情况了。而且一般情况下也完全没有必要导入所有账户,导个支付宝、微信就差不多了,其它账户留着每月导入就行,那样耗费的时间也就更短了。

接下来,我将先逐个部分介绍我记账方案中的组件,然后再介绍部署的方法以及使用的经验。各位可以各取所需,没必要完整的阅读。

主力:账单导入

账单导入其实非常个人化,所以这一节主要介绍如何获得账单数据、修改我个人目前的脚本。

获取账单数据

账单数据的主要来源:官方对账单、账单邮件、自食其力。

官方对账单

对于提供官方对账单的”带善人“,直接下载提供的对账单即可。大部分国内平台、网上银行都能下载得到。一般来说对账单是CSV、XLSX、PDF格式的,而解析的难易度刚好也是这个顺序,前二者最简单,PDF最困难。不过大部分国内平台的对账单都”挺能藏“,比如这二位:

  • 支付宝:支付宝 App → 我的 → 账单 → 右上角… → 开具交易流水证明
  • 微信:微信支付公众号 → 我的账单 → 账单明细 → 常见问题 → 下载账单

虽然支付宝网页版也可以导出,但是那个结果缺少了支付账户,无法区分余额、花呗、银行卡、余额宝。

账单邮件

对于账单邮件,建议使用邮箱客户端下载eml文件。部分网页版也支持,比如阿里云邮箱。

自食其力

剩下的那些就属于”自食其力“的范畴了。对于那些平台,我建议用Table Capture插件直接抓网页表格。由于免费版不能下载CSV,所以需要先复制然后粘贴到Excel里再转存CSV。

轮子的改

脚本方面,我果断参考了现成脚本zsxsoft/my-beancount-scripts,修改过的脚本见此modulespip 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机器人

然后就是灵魂部分的Telegram机器人了,源程序已经开源:kaaass/beancount_bot。此处主要介绍相关配置。

通过Docker安装

我个人推荐用Docker省掉麻烦的部署操作。推荐使用包含Costflow插件的这个镜像:kaaass/beancount_bot_costflow_docker。部署操作也相当简单,只需要创建两个文件夹:

  • config:存放机器人配置。默认配置文件beancount_bot.yml
  • bean:存放账本

之后在同目录运行以下指令启用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。其中,

  1. 指令名可以有多个,都可以触发同一个模板;
  2. 目标账户可以省略,省略将使用默认账户;
  3. 参数使用空格隔开,可以用引号(”)包裹带空格参数

具体的指令、账户需要在配置文件(示例配置: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语法

虽然模板语法很强大,但是终究还是需要预先配置好语句,不够灵活。而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),具体建议参考文档和引用中的第二篇文章。主要需要配置的其实也就是各个账户的别名了,分享下我个人的设置思路:

  • 所有资产、负债使用拼音缩写,比如:zfb、yeb
  • 所有收入、支出使用自己最习惯的中文名,可以重复设置多个,比如:饭、中饭、吃饭

定时任务

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'

Fava部署

鉴于Telegram Bot一般会部署在服务器上,因此顺便搭建一个Fava来实时查看账本也是个很不错的选择。不过Fava本身没有鉴权,因此公网部署非常的危险。zsxsoft/fava-management给Fava添加了登录、重启功能,也提供了Docker镜像。不过本身Fava版本有点旧,所以我fork了一份版本较新的:kaaass/fava-management

但是需要注意的是,使用过程中我多次遇到了CPU占用率突然飙升至100%的情况,原因暂且不明。因此也可以使用官方Fava+反代时添加Basic Auth的部署方式。

老大难问题:备份

需求

如果全部手工记账,那备份其实一点也不难。但是我们的方案却分出了两套账本:

  • 本地帐本:查账的时候用
  • 远端账本:Telegram Bot更新、Fava查看

因此就需要保证两边的账本是同步的,不然就会出现问题。

青春版:坚果云、Google Drive……

最简单的方法就是用自带文件夹同步的程序。坚果云、Google Drive等等全都OK。

用牛刀版:Git+CI

但是青春版有两个问题:首先就是这需要我把自己的账本交给第三方托管,感到困惑害怕希望不要再发了;其次就是版本管理都很初级。于是我就想到使用Git来管理版本,那两个账本刚好就对应了两个分支:

  • master:查账、修改配置用
  • bot:Telegram Bot进行定期备份

此外,为了便于部署本地的更改,还可以使用CI在master接到push时自动覆盖文件到Bot目录。如此下来,每次查账时只需要:

  1. 触发Bot进行一次备份
  2. Pull本地,将origin/botmerge进mastergit merge origin/bot --squash
  3. 查完帐之后commit、push

如果使用公共服务如Github,也可以用git-crypt来加密账本文件。当然代价就是Github变成了单纯的网盘。

部署总结

拼接文章中的所有拼图,我们就可以得到一个最终的部署方案了。我开源了一套示例配置供大家参考:kaaass/my-beancount-template

万物基于Docker

需要部署的服务主要就是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)的指导思想:功能最简、易于拓展。希望这篇文章能帮助更多人快速设计、规划自己的记账方案。

Reference

  1. zsxsoft – Beancount复式记账:接地气的Why and How(https://blog.zsxsoft.com/post/41
  2. leplay – 使用 Costflow 提高 Beancount 记账效率(https://medium.com/leplay/%E4%BD%BF%E7%94%A8-costflow-%E6%8F%90%E9%AB%98-beancount-%E8%AE%B0%E8%B4%A6%E6%95%88%E7%8E%87-bdae22d1f6c4
分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

评论

  1. vicat 2021.12.07 4:10下午

    好耶,博主写的真不辍,之前纯手动记了两个月,直到某次没记上以后就雪崩了,再也不记账了,这次准备把服务搭一下,至少能让记账压力小一些

  2. CTLING 2022.02.07 2:34下午

    提供一些 CPU 飆到 100% 的資訊,不知道有沒有幫助
    看了 top 發現是有一大堆 gunicorn 都衝到 100%

    • KAAAsS 2022.02.07 5:31下午

      感谢报告!这个现象发生时间也很诡异,至今也没什么头绪><

      • CTLING 2022.02.08 12:34上午

        我從兩個方向處理,我的 fava 架在 server 上,有 CPU 有 12 核。

        一開始我先直接限制 docker 可以使用的 CPU 變成 0.5 ,
        發現還是會出現超多個 gunicorn 的行程。
        只是每一個 gunicorn 都變成個位數的 CPU 使用率。

        後來從 docker-compose up (不加 -d) 發現真的啟動了 24 個 worker 再跑…

        我就想說是誰啟動這麼多 gunicorn ,找到源頭的 docker image 發現可能是這邊造成的。

        可以設定 WEB_CONCURRENCY 限制最多有多少 WORKER 。
        https://github.com/tiangolo/meinheld-gunicorn-docker#web_concurrency

        加進 docker-compose.yml 的環境變數就可以了

        雖然還是有一個會跑到 100% 的 >"<
        但是至少先降低整台 server 的負擔了。

        • KAAAsS 2022.02.09 10:58上午

          我这里也是依旧有一个会跑到 100%,不过我只有 2 core,就没法用了 ><

        • hsk 2024.01.16 10:26下午

          同样发现了这个问题,想看看有没有人fork过原repo,通过zxsoft的repo找到博主的repo,然后找到这个评论,终于找到这个问题的解决办法。绕了一大圈,用这个方法解决了。
          非常感谢!!!

  3. winkmoyu 2022.03.11 8:36下午

    确实很有收获,也想动手弄一个,我评论的主要目的是催更你的bot快整上账单导入吧~~

    • KAAAsS 2022.03.11 9:11下午

      感谢支持,不过因为 Beancount 账单导入方案各种各样,就连官方也打算重新实现,所以这部分短期内大概是搁置了 ><
      如果支持的话,大概也是提供一个读取文件、管理账单的接口,本身不负责导入的逻辑。

  4. Haskell 2022.10.05 5:30上午

    用kaaass/beancount_bot_costflow_docker,bot生成是对的,但是transaction.beancount_file路径下的.bean文件里面总是没有反应,试了很多轮,改路径也没用

  5. 233 2023.02.06 1:57下午

    第二篇怎么没有啦?

    • KAAAsS 2023.02.06 3:18下午

      因为还没写出来(咕咕咕)

  6. hh 2023.02.10 8:12下午

    两年前想开始记账,看了你的第一篇搭好了 ,一个账单没记,哈哈。两年后想用docker部署一个,看到了你的第三篇(跳过第二篇亮了!)

    • KAAAsS 2023.02.10 9:08下午

      噗,这么说第二篇已经要拖更两年,不妙啊~

  7. hh 2023.02.12 10:09上午

    路飞:2Y 后见(手动狗头)

  8. ubuntu 2024.04.18 10:18上午

    用了你的bot,可以自动更新基金价格。
    如何才能获取股票价格呢?
    是不是把https://github.com/kaaass/my-beancount-template/blob/main/modules/modules/price_sources/10jqka.py这个文件里的
    "http://fund.10jqka.com.cn/$ticker/json/jsondwjz.json&quot😉
    改成
    "http://stock.10jqka.com.cn/$ticker/json/jsondwjz.json&quot😉
    就可以了?

    • KAAAsS 2024.04.20 4:11下午

      用另一个雪球的 price source 就可以了。CNY:modules.price_sources.xueqiu/CN:xxxxx。

在此评论中不能使用 HTML 标签。