前言
虽然过去这些年一直保持着记账习惯,但从没能落实预算控制,也没严格使用复式记账以跟踪资金流向。Wallet by BudgetBakers 的银行帐务同步(基于 Sale Edge)还算好用,但每隔一段时间需要重新刷新银行账户的 Token,消费类型不如手动记录的准确,且没法很好的 handle “使用 debit 账户转账还信用卡账单”这一行为。在此过程中逐渐意识到,好的方法和工具还是很有必要的。
于是我开始寻找替代品。V2EX 上不少人推荐了 MoneyWiz,但是 30 USD 的年费还是有点劝退了,我也不想再次被 SaaS 给捆绑。最后从 ledger、GnuCash 和 Beancount 中选用了 Beancount。
参考
简单上手只需读完前两篇,再看眼 Syntax Cheatsheet 就够了。
- Beancount —— 命令行复式簿记 @ wzyboy
- 使用 Beancount 记录证券投资 @ wzyboy
- 『Beancount指南』复式记账 @ Fermi
- 复式记账工具:Beancount 入门 @ val
我的理解,不一定对
Beancount 生态主要由以下部分组成:
- fava 前端:提供基本的报表和交互逻辑
- beancount 后端:处理账务逻辑,实现统计
- 账本文件:以
.bean或者.beancount为后缀的纯文本;支持文件嵌套(include [filename])
综上,“记账”和“统计”被巧妙地解耦了,账本可以托管在 S3、GitHub 或者任何你想的地方。搭配各种 API 和 Webhook,有无尽的可能。
实践
痛苦的洗数据
从 Wallet 导出的数万行 CSV,以及各个银行的月结单 PDF——前者还能用 Python 自动导入为 beancount 记录,后者只能手动一条条加 balance assertion。也可以使用 pikepdf 或者 pytesseract 来提取 PDF 中的文本,但工作量也不小。
最后花了数天时间把 2025 年全年,以及一些早期但大额的账目给梳理好。其余的就用 balance 指令归类为 Equity:Suspenses 平账了。
使用 Telegram 随时随地记账
移动端 App 有 BeanWise,但我认为记账是一个“write only”行为,引入一个不联网的客户端,还需要手动处理账本文件同步,问题更麻烦了。
所以手搓了个 Beancounter Telegram Bot,使用 GitHub REST API 读写账本所在仓库。
- 用换行区分字段
- 使用正则判断
#tag和^link - 基于后缀匹配账户,并忽略大小写:
cash -> Assets:Cash
自己用了两三天效果还挺满意。
财务支出报告
Bean-query 是用于 beancount 账本统计分析的命令行工具。
# 获取 2025-12 的支出汇总
bean-query -q main.bean \
"SELECT account, cost(sum(position))
WHERE account ~ '^Expenses:'
AND date >= 2025-12-01 AND date < 2026-01-01
GROUP BY account
ORDER BY account"由于账本托管在 GitHub,只需利用 GitHub Actions 定时任务和 Beanquery 工具,以及 Telegram API,即可实现每次账本修改后获取本月支出信息。Telegram Bot Token 可以使用 GitHub Actions Secrets 来导入,Chat ID 也同理,无需硬编码在代码里。
name: notify expenses this month
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
python3 -m pip install --user beanquery
- name: Compute current month range
id: daterange
shell: bash
run: |
# 计算当月第一天和下月第一天(ISO 8601)
FIRST_DAY="$(date -u +'%Y-%m-01')" # 当月1号
NEXT_MONTH_FIRST="$(date -u -d "$(date -u +'%Y-%m-01') +1 month" +'%Y-%m-01')"
echo "first_day=$FIRST_DAY" >> "$GITHUB_OUTPUT"
echo "next_month_first=$NEXT_MONTH_FIRST" >> "$GITHUB_OUTPUT"
echo "Using range: $FIRST_DAY .. $NEXT_MONTH_FIRST"
- name: Run bean-query (per-account totals)
run: |
bean-query -q main.bean \
"SELECT account, cost(sum(position))
WHERE account ~ '^Expenses:'
AND date >= ${{
steps.daterange.outputs.first_day
}} AND date < ${{
steps.daterange.outputs.next_month_first
}}
GROUP BY account
ORDER BY account" | sed '1s/^/```markdown\n/;$s/$/\n```/;s/Expenses://' |curl -sS -X POST \
--data-urlencode "chat_id={your_chat_id}" \
--data-urlencode "text@-" \
--data-urlencode "parse_mode=Markdown" \
"https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage"结语
慢慢摸索中。