前言

虽然过去这些年一直保持着记账习惯,但从没能落实预算控制,也没严格使用复式记账以跟踪资金流向。Wallet by BudgetBakers 的银行帐务同步(基于 Sale Edge)还算好用,但每隔一段时间需要重新刷新银行账户的 Token,消费类型不如手动记录的准确,且没法很好的 handle “使用 debit 账户转账还信用卡账单”这一行为。在此过程中逐渐意识到,好的方法和工具还是很有必要的。

于是我开始寻找替代品。V2EX 上不少人推荐了 MoneyWiz,但是 30 USD 的年费还是有点劝退了,我也不想再次被 SaaS 给捆绑。最后从 ledgerGnuCashBeancount 中选用了 Beancount。

参考

简单上手只需读完前两篇,再看眼 Syntax Cheatsheet 就够了。

我的理解,不一定对

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 也同理,无需硬编码在代码里。

.github/workflows/expenses-on-change.yml
 
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"

结语

慢慢摸索中。