Beancount 虽然可以系统得记录和统计收入和支出,但记录起来不是很方便,根据账单自动化记录数据便会省下大量精力和时间。

综述

Importer 个人理解的作用是将整理好的账单文本转化为Bean记录的形式,即格式化(表格,JSON等)账单 -> Importer -> Beancount 记录,Importer在其中起到一个消费记录格式转化作用。

Beancount 作者对 Importer 有详细的文档叙述,即Importing External Data in BeancountGitHub@lidingchao也写了简单的sample来实现一个importer。Beancount 官方也有基于机器学习的智能importer beancount/smart_importer

而我的需求是:

  • 基于csv的账单生成bean文件
  • 能够自行定制规则来实现对不同消费的分类

正好,Beancount 官方有关于CSV的一个Reader,参考博客beancount 简易入门指南就有了我的实现。

一个例子

准备csv文件

将下面表格生成为CSV文件bill.csv

记账日期 交易日期 收款人 交易摘要 人民币金额 卡号后四位 类别
2019-12-03 2019-12-04 铁路 京九线 10.00 2223 train
2019-12-03 2019-12-04 餐饮 老北京火锅 300.00 8763 huoguo
2019-12-03 2019-12-04 公交 120公交车 2.00 2134 bus

准备Import Config

编写Config文件为myconfig.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env python

import os
import sys

import beancount.ingest.extract
from beancount.ingest.importers import csv

beancount.ingest.extract.HEADER = ''


def dumb_categorizer(txn):
# At this time the txn has only one posting
try:
posting1 = txn.postings[0]
except IndexError:
return txn

# Guess the account(s) of the other posting(s)
if 'train' in txn.narration.lower():
account = 'Expenses:Train'
elif 'huoguo' in txn.narration.lower():
account = 'Expenses:Food'
elif 'bus' in txn.narration.lower():
account = 'Expenses:Bus'
else:
return txn

# Make the other posting(s)
posting2 = posting1._replace(
account=account,
units=-posting1.units
)

# Insert / Append the posting into the transaction
if posting1.units < posting2.units:
txn.postings.append(posting2)
else:
txn.postings.insert(0, posting2)

return txn


CONFIG = [
# BoC 0000
csv.Importer(
{
csv.Col.DATE: '记账日期',
csv.Col.TXN_DATE: '交易日期',
csv.Col.PAYEE: '收款人',
csv.Col.NARRATION1: '交易摘要',
csv.Col.AMOUNT_DEBIT: '人民币金额',
csv.Col.NARRATION2: '类别',
csv.Col.LAST4: '卡号后四位'
},
account='Assets:CN:Card:BoC:0000',
currency='CNY',
categorizer=dumb_categorizer,
),
]

说明:

  1. CONFIG里面可以写多个csv.Importer,每个 importer 判断自己是否会处理这个文件,如果会处理,交给这个 imoprter 处理导入,并不再往下判断;
  2. importer 源码位于beancount/beancount/ingest/importers/csv.py,初始化需要配置以下参数`
    • config: 第三条细讲
    • account: 对应的消费账户,一般是借记卡或者信用卡
    • currency: 币种
    • regexps, skip_lines, last4_map: 不知道,待实践
    • categorizer: 提供一个函数来对消费进行规则分类,例子中自定义了函数dumb_categorizer,根据Narration的信息判断是哪种消费
  3. config里包含了csv.Col.xxx来自动提取CSV中的信息,importer以键做索引,csv header由值提供
    • ACCOUNT: 不知道
    • AMOUNT: 不知道
    • AMOUNT_CREDIT: 信用卡消费金额
    • AMOUNT_DEBIT: 借记卡消费金额
    • BALANCE: 不知道
    • DATE: 记账日期
    • DRCR: 不知道
    • LAST4: 卡号后四位,会以metadata形式记录在账本中
    • NARRATION: 注释1
    • NARRATION2: 注释2
    • NARRATION3: 注释3,如果存在多个注释,账本中会合并为一个并以;分隔
    • PAYEE: 收款人
    • TAG: 标签
    • TXN_DATE: 交易日期,会以metadata形式记录在账本中
    • TXN_TIME: 交易时间,会以metadata形式记录在账本中

生成bean账单

执行命令

1
bean-extract myconfig.py bill.csv

得到账单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
**** /xxx/bill.csv

2019-12-03 * "铁路" "京九线; train"
card: "2223"
date: 2019-12-04
Assets:CN:Card:BoC:0000 -10.00 CNY
Expenses:Train 10.00 CNY

2019-12-03 * "餐饮" "老北京火锅; huoguo"
card: "8763"
date: 2019-12-04
Assets:CN:Card:BoC:0000 -300.00 CNY
Expenses:Food 300.00 CNY

2019-12-03 * "公交" "120公交车; bus"
card: "2134"
date: 2019-12-04
Assets:CN:Card:BoC:0000 -2.00 CNY
Expenses:Bus 2.00 CNY

Done!

Importer in Fava

可视化工具Fava也支持Importer,可以通过设置

1
2
2017-01-01 custom "fava-option" "import-config" "./importers/path/to/importer.py"
2017-01-01 custom "fava-option" "import-dirs" "./importers/path/to/csv_tmp/"

在Fava界面侧栏看到Importer,并手动导入数据。

注 :Importer在Fava中使用的时候metadata会被去除