使用 Beancount 和 Fava 的可编写脚本的工作流
Beancount(一种纯文本复式记账工具)和 Fava(其 Web 界面)具有高度的可扩展性和可编写脚本性。它们的设计允许您通过编写 Python 脚本来自动化财务任务、生成自定义报告和设置警报。正如一位用户所说,“我真的很喜欢以如此方便的格式存储我的数据,而且我喜欢我可以随心所欲地自动化事情。没有像磁盘上的文件那样的 API;它很容易集成。” 本指南将引导您创建可编写脚本的工作流——从初学者友好的自动化到高级 Fava 插件。
入门:将 Beancount 作为 Python 脚本运行
在深入了解具体任务之前,请确保已安装 Beancount(例如,通过 pip install beancount)。由于 Beancount 是用 Python 编写的,因此您可以在自己的脚本中将其用作库。一般方法是:
-
加载您的 Beancount 账本:使用 Beancount 的加载器将
.beancount文件解析为 Python 对象。例如:from beancount import loader
entries, errors, options_map = loader.load_file("myledger.beancount")
if errors:
print("Errors:", errors)这为您提供了一个
entries列表(交易、余额等)和一个包含元数据的options_map。现在可以在代码中访问您的所有帐户、交易和余额。 -
利用 Beancount 查询语言 (BQL):您可以运行类似于 SQL 的查询来查询数据,而无需手动迭代。例如,要按月获取总支出,您可以使用查询 API:
from beancount.query import query
q = query.Query(entries, options_map)
result = q.query("SELECT month, sum(position) WHERE account ~ 'Expenses' GROUP BY month")
print(result)这使用 Beancount 的查询系统来聚合数据。(在底层,这类似于
bean-query命令所做的事情,但在这里您在脚本中使用它。) 事实上,Beancount 的作者指出,您可以加载文件并直接通过 Python API 调用run_query(),从而避免了在循环中调用外部命令的需要。 -
设置项目结构:将您的脚本与您的账本一起组织。常见的布局是为 importers(用于获取/解析外部数据)、reports 或 queries(用于分析脚本)和 documents(用于存储下载的报表)创建目录。例如,一位用户保留:
importers/– 自定义 Python 导入脚本(带有测试),queries/– 用于生成报告的脚本(可通过python3 queries/...运行),documents/– 按帐户组织的下载的银行 CSV/PDF。
通过此设置,您可以手动运行脚本(例如 python3 queries/cash_flow.py)或安排它们(通过 cron 或任务运行器)以自动化您的工作流程。
自动化对账任务
对账 意味着确保您的账本与外部记录(银行对账单、信用卡报告等)匹配。Beancount 的纯文本账本和 Python API 使自动化此过程的大部分成为可能。
导入和匹配交易(初学者)
对于初学者,建议的方法是使用 Beancount 的 导入器插件。您编写一个小型的 Python 类,遵循 Beancount 的导入器协议来解析给定的格式(CSV、OFX、PDF 等)并生成交易。然后使用 bean-extract 命令或脚本来应用这些导入器:
- 为您银行的 CSV 格式编写一个导入器(一个 Python 类,其中包含
identify()、extract()等方法)。Beancount 的文档提供了指南和示例。 - 在脚本或 Makefile(如
justfile示例)中使用bean-extract来解析新的报表。例如,一个工作流程在~/Downloads中的所有文件上运行bean-extract并将交易输出到临时文件。 - 手动审查并将交易从临时文件复制到您的主账本中,然后运行
bean-check以确保余额 对账。
虽然此过程仍涉及审查步骤,但解析和格式化条目的大部分繁琐工作已自动化。导入器脚本还可以自动分配类别,甚至设置余额断言(预期余额的声明)以捕获差异。例如,导入后,您可能会有类似 2025-04-30 balance Assets:Bank:Checking 1234.56 USD 的行,该行断言期末余额。当您运行 bean-check 时,Beancount 将 验证所有这些余额断言是否正确,如果缺少或重复交易,则会标记任何错误。这是一个最佳实践:为每个报表期自动生成余额断言,让计算机为您发现未对账的差异。
自定义对账脚本(中级)
为了获得更多控制,您可以编写一个自定义 Python 脚本来比较银行的交易列表(CSV 或通过 API)与您的账本条目:
- 读取外部数据:使用 Python 的
csv模块(或 Pandas)解析银行的 CSV 文件。将数据标准化为交易列表,例如,每个交易都具有日期、金额和描述。 - 加载账本交易:如前所示使用
loader.load_file获取所有账本条目。将此列表过滤到感兴趣的帐户(例如,您的支票帐户)以及报表的日期范围。 - 比较并查找不匹配项:
- 对于每个外部交易,检查账本中是否存在相同的条目(按日期和金额匹配,可能还有描述)。如果未找到,则将其标记为“new”并可能将其输出为 Beancount 格式的交易供您审核。
- 相反,识别该帐户中未出现在外部来源中的任何账本条目——这些可能是数据输 入错误或尚未清算的交易。
- 输出结果:打印报告或创建一个新的
.beancount代码段,其中包含缺少的交易。
例如,一个名为 reconcile.py 的社区脚本正是这样做的:给定一个 Beancount 文件和一个输入 CSV,它会打印一个应该导入的新交易列表,以及输入中不存在的任何现有账本过账(可能表示错误分类)。使用这样的脚本,每月对账可以像运行它然后将建议的交易附加到您的账本一样简单。一位 Beancount 用户指出,他们 “每个月都对所有帐户执行对账过程” 并使用不断增长的 Python 代码集合来消除导入和对账数据中的大部分手动工作。
提示: 在对账期间,利用 Beancount 的工具来提高准确性:
- 如前所述,使用余额断言,对帐户余额进行自动检查。
- 如果需要,使用
pad指令,它可以自动插入平衡条目以解决较小的舍入差异(谨慎使用)。 - 为您的导入器或对账逻辑编写单元测试(Beancount 提供测试帮助程序)。例如,一个工作流程涉及获取一个示例 CSV,编写具有预期交易的失败测试,然后实现导入器直到所有测试通过。这可确保您的导入脚本适用于各种情况。
生成自定义报告和摘要
虽然 Fava 提供了许多标准报告(损益表、资产负债表等),但您可以使用脚本创建自定义报告。这些报告的范围可以从简单的控制台输出到格式丰富的格式文件或图表。
查询用于报告的数据(初学者)
在基本层面上,您可以使用 Beancount 查询语言 (BQL) 获取摘要数据并打印或保存它。例如:
-
现金流量摘要: 使用查询来计算净现金流量。“现金流量”可以定义为特定期间内某些帐户的余额变化。使用 BQL,您可以这样做:
SELECT year, month, sum(amount)
WHERE account LIKE 'Income:%' OR account LIKE 'Expenses:%'
GROUP BY year, month这将按月计算所有收入和支出过账的净额。您可以通过
bean-queryCLI 或通过 Python API(如前所示的query.Query)运行它,然后格式化结果。 -
类别支出报告: 查询每个类别的总支出:
SELECT account, round(sum(position), 2)
WHERE account ~ 'Expenses'
GROUP BY account
ORDER BY sum(position) ASC这将生成一个按类别列出的支出表。您可以在脚本中运行多个查询,并将结果输出为文本、CSV 甚至 JSON 以进行进一步处理。
一位用户发现使用 Fava 或使用脚本分析财务数据 “微不足道”,并引用说他们使用 一个 Python 脚本通过查询语言从 Beancount 中提取数据,然后将其放入 Pandas DataFrame 中以准备自定义报告。例如,您可以使用查询获取每月总额,然后使用 Pandas/Matplotlib 绘制随时间变化的现金流量图。BQL 和数据科学库的结合使您可以构建超出 Fava 默认提供的报告。
高级报告(图表、绩效等)
对于更高级的需求,您的脚本可以计算投资业绩等指标或创建可视化输出:
-
投资业绩 (IRR/XIRR):由于您的账本包含所有现金流量(买入、卖出、股息),因此您可以计算投资组合回报率。例如,您可以编写一个脚本来过滤您的投资帐户的交易,然后计算内部收益率。有一些库(或公式)可以根据现金流量数据计算 IRR。一些社区开发的 Fava 扩展(例如 PortfolioSummary 或 fava_investor)正是这样做的,计算投资组合的 IRR 和其他指标。作为脚本,您可以对一系列供款/提款加上期末价值使用 IRR 函数(来自 NumPy 或您自己的)。
-
多期或自定义指标:想要一份每月储蓄率(储蓄与收入之比)的报告吗?一个 Python 脚本可以加载账本,将所有收入帐户和所有支出帐户相加,然后计算储蓄 = 收入 - 支出和百分比。这可以输出一个漂亮的表格,甚至可以生成一个 HTML/Markdown 报告以供您记录。
-
可视化:您可以在 Fava 之外生成图表。例如,在脚本中使用
matplotlib或altair创建一个随时间变化的净值图表,使用账本数据。由于账本具有所有历史余额(或者您可以通过迭代条目来累积它们),因此您可以生成时间序列图。将这些图表保存为图像或交互式 HTML。(如果您喜欢应用内视觉效果,请参阅下面的 Fava 扩展部分,了解如何在 Fava 内部 添加图表。)
输出选项: 确定如何交付报告:
- 对于一次性分析,打印到屏幕或保存到 CSV/Excel 文件可能就足够了。
- 对于仪表板,请考虑生成一个包含数据的 HTML 文件(可能使用 Jinja2 等模板库,甚至只是编写 Markdown),您可以在浏览器中打开该文件。
- 您还可以与 Jupyter Notebook 集成以获得交互式报告环境,但这更多是用于探索而不是自动化。
从您的账本触发警报
可编写脚本的工作流的另一个强大用途是根据您的财务数据中的条件设置警报。由于您的账本会定期更新(并且可以包括未来日期的项目,例如即将到来的账单或预算),因此您可以使用脚本扫描它并收到重要事件的通知。
低帐户余额警告
为了避免透支或维持最低余额,您可能希望在任何帐户(例如支票或储蓄)低于阈值时发出警报。以下是如何实现此目的:
-
确定当前余额: 通过加载器加载
entries后,计算感兴趣帐户的最新余额。您可以通过聚合过账或使用查询来做到这一点。例如,使 用 BQL 查询特定帐户的余额:SELECT sum(position) WHERE account = 'Assets:Bank:Checking'这将返回该帐户的当前余额(所有过账的总和)。或者,使用 Beancount 的内部函数来构建资产负债表。例如:
from beancount.core import realization
tree = realization.realize(entries, options_map)
acct = realization.get_or_create(tree, "Assets:Bank:Checking")
balance = acct.balance # an Inventory of commodities然后提取数值(例如,
balance.get_currency_units('USD')可能会给出 Decimal)。但是,在大多数情况下,使用查询更简单。 -
检查阈值: 将余额与您预定义的限制进行比较。如果低于,则触发警报。
-
触发通知: 这可能就像将警告打印到控制台一样简单,但对于真正的警报,您可能会发送电子邮件或推送通知。您可以与电子邮件(通过
smtplib)或 IFTTT 或 Slack 的 webhook API 等服务集成以推送警报。例如:if balance < 1000:
send_email("Low balance alert", f"Account XYZ balance is {balance}")(使用您的电子邮件服务器详细信息实现
send_email。)
通过每天运行此脚本(通过 cron 作业或 Windows 任务计划程序),您将获得主动警告。因为它使用账本,所以它可以考虑 所有 交易,包括您刚刚添加的交易。
即将到来的付款截止日期
如果您使用 Beancount 来跟踪账单或截止日期,您可以标记未来的付款,并让脚本提醒您。在 Beancount 中表示即将到来的义务的两种方式:
-
事件: Beancount 支持用于任意日期注释的
event指令。例如:2025-05-10 event "BillDue" "抵押贷款付款到期"这不会影响余额,但会记录带有标签的日期。脚本可以扫描
entries中Event.type == "BillDue"(或您选择的任何自定义类型)的Event条目,并检查日期是否在今天起的未来 7 天内。如果是,则触发警报(电子邮件、通知,甚至弹出窗口)。 -
未来交易: 有些人输入未来日期的交易(延期付款),例如预定 的付款。在日期过去之前,这些交易不会显示在余额中(除非您将报告运行到未来日期)。脚本可以查找在不久的将来注明日期的交易并列出它们。
使用这些,您可以创建一个“提醒”脚本,该脚本在运行时输出即将到来的任务或账单列表。如果您想在那里自动创建提醒,请与 Google 日历或任务管理器等 API 集成。
异常检测
除了已知的阈值或日期之外,您还可以为异常模式编写自定义警报。例如,如果通常每月发生的费用尚未发生(可能您忘记支付账单),或者如果某个类别的支出在本月异常高,您的脚本可以标记它。这通常涉及查询最近的数据并与历史记录进行比较(这可能是一个高级主题——可能需要使用统计或 ML)。
在实践中,许多用户依靠对帐来捕获异常(意外交易)。如果您收到银行通知(例如每笔交易的电子邮件),您可以使用脚本解析这些通知并自动将其添加到 Beancount,或者至少验证它们是否已记录。一位爱好者甚至配置他们的银行发送交易警报电子邮件,计划解析它们并自动将其附加到账本。这种事件驱动的警报可以确保没有交易未被记录。
使用自定义插件和视图扩展 Fava
Fava 已经可以通过其扩展系统进行编写脚本。如果您希望将您的自动化或报告直接集成到 Web 界面中,您可以用 Python 编写一个 Fava 扩展(也称为插件)。
Fava 扩展的工作原理: 扩展是一个 Python 模块,它定义了一个从 fava.ext.FavaExtensionBase 继承的类。您通过自定义选项在您的 Beancount 文件中注册它。例如,如果您有一个文件 myextension.py,其中包含一个类 MyAlerts(FavaExtensionBase),您可以通过将以下内容添加到您的账本来启用它:
1970-01-01 custom "fava-extension" "myextension"
当 Fava 加载时,它将导入该模块并初始化您的 MyAlerts 类。
扩展可以做几件事:
- 挂钩: 它们可以挂钩到 Fava 生命周期中的事件。例如,在加载账本后调用
after_load_file()。您可以使用它来运行检查或预先计算数据。如果您想在 Fava 内部 实现低余额检查,after_load_file可以迭代帐户余额并可能存储警告(尽管将它们显示到 UI 可能需要更多的工作,例如引发 FavaAPIError 或使用 Javascript 显示通知)。 - 自定义报告/页面: 如果您的扩展类设置了
report_title属 性,Fava 将在侧边栏中为其添加一个新页面。然后,您为该页面的内容提供一个模板(HTML/Jinja2)。这就是您创建全新视图的方式,例如 Fava 默认没有的仪表板或摘要。该扩展可以收集它需要的任何数据(您可以访问self.ledger,它具有所有条目、余额等),然后渲染模板。
例如,Fava 中内置的 portfolio_list 扩展添加了一个页面,列出您的投资组合头寸。社区扩展更进一步:
- 仪表板: fava-dashboards 插件允许定义自定义图表和面板(使用 Apache ECharts 等库)。它读取要运行的查询的 YAML 配置,通过 Beancount 执行它们,并在 Fava 中生成一个动态仪表板页面。从本质上讲,它将 Beancount 数据和 JavaScript 图表库结合在一起,以生成交互式可视化效果。
- 投资组合分析: PortfolioSummary 扩展(用户贡献)计算投资摘要(对帐户进行分组,计算 IRR 等)并在 Fava 的 UI 中显示它们。
- 交易审查: 另一个扩展 fava-review 帮助审查一段时间内的交易(例如,以确保您没有遗漏任何收据)。
要自己创建一个简单的扩展,首先要继承 FavaExtensionBase。例如,添加页面的最小扩展可能如下所示:
from fava.ext import FavaExtensionBase
class HelloReport(FavaExtensionBase):
report_title = "Hello World"
def __init__(self, ledger, config):
super().__init__(ledger, config)
# any initialization, perhaps parse config if provided
def after_load_file(self):
# (optional) run after ledger is loaded
print("Ledger loaded with", len(self.ledger.entries), "entries")
如果您将此代码放入 hello.py 并在您的账本中添加了 custom "fava-extension" "hello",Fava 将显示一个新的“Hello World”页面(您还需要一个模板文件 HelloReport.html 在 templates 子文件夹中定义页面内容,除非该扩展仅使用挂钩)。该模板可以使用您附加到扩展类的数据。Fava 使用 Jinja2 模板,因此您可能会将您的数据渲染为 HTML 表格或该模板中的图表。
注意: Fava 的扩展系统功能强大,但被认为是“不稳定的”(可能会发生变化)。如果您要创建自定义页面,则需要熟悉 Web 开发 (HTML/JS)。如果您的目标只是运行脚本或分析,则将其保留为外部脚本可能更容易。当您想要为您的工作流程提供定制的应用内体验时,请使用 Fava 扩展。
集成第三方 API 和数据
可编写脚本的工作流的优势之一是能够引入外部数据。以下是常见的集成:
-
汇率和商品: Beancount 在设计上不会自动获取价格(以使报告具有确定性),但它提供了 Price 指令供您提供费率。您可以自动获取这些价格。例如,脚本可以查询 API(Yahoo Finance、Alpha Vantage 等)以获取最新的汇率或股票价格,并将价格条目附加到您的账本:
2025-04-30 price BTC 30000 USD
2025-04-30 price EUR 1.10 USD有一些工具,例如
bean-price(现在是 beancount 保护下的外部工具),可以获取每日报价并以 Beancount 格式输出它们。您可以安排bean-price每晚运行以更新prices.beancount包含文件。或使用 Python:例如,使用requests库调用 API。Beancount 的文档建议,对于公开交易的资产,您可以“调用一些代码来下载价格并为您写出指令”。 换句话说,让脚本进行查找并插入price行,而不是您手动执行。 -
股票投资组合数据: 与汇率类似,您可以与 API 集成以获取详细的股票数据或股息。例如,Yahoo Finance API(或像
yfinance这样的社区库)可以检索代码的历史数据。脚本可能会使用您拥有的每只股票的每月价格历史记录来更新您的账本,从而能够准确地历史报告市场价值。一些自定义扩展(例如 fava_investor)甚至动态地提取价格数据以进行显示,但最简单的方法是定期将价格导入到账本中。 -
银行 API(开放银行/Plaid): 您可以使用 API 自动提取交易,而不是下载 CSV。Plaid 等服务会聚合银行帐户并允许以编程方式访问交易。在高级设置中,您可以拥有一个 Python 脚本,该脚本使用 Plaid 的 API 每天提取新交易并将它们保存到文件中(或直接导入到账本中)。一位高级用户构建了一个系统,其中 Plaid 馈送到他们的导入管道中,使他们的账簿几乎自动化。他们指出,“没有什么可以阻止您注册 Plaid API 并在本地执行相同的操作”——也就是说,您可以编写一个本地脚本来获取银行数据,然后使用您的 Beancount 导入器逻辑将其解析为账本条目。某些地区拥有银行提供的开放银行 API;这些 API 可以类似地使用。
-
其他 API: 您可以集成预算编制工具(导出计划的预算以与 Beancount 中的实际预算进行比较),或者使用 OCR API 读取收据并 将其自动匹配到交易。由于您的脚本可以完全访问 Python 的生态系统,因此您可以集成从电子邮件服务(用于发送警报)到 Google Sheets(例如,使用每月财务指标更新表格)到消息应用程序(通过 Telegram 机器人向自己发送摘要报告)的所有内容。
使用第三方 API 时,请记住保护您的凭据(对 API 密钥使用环境变量或配置文件),并在脚本中优雅地处理错误(网络问题、API 停机)。通常明智的做法是缓存数据(例如,存储提取的汇率,这样您就不会重复请求相同的历史汇率)。
模块化、可维护脚本的最佳实践
在构建可编写脚本的工作流时,请保持您的代码组织性和健壮性:
-
模块化: 将不同的问题拆分为不同的脚本或模块。例如,为“数据导入/对帐”与“报告生成”与“警报”提供单独的脚本。您甚至可以为您的账本创建一个小型 Python 包,其中包含
ledger_import.py、ledger_reports.py等模块。这使得每个部分都更容易理解和测试。 -
配置: 避免硬编码值。对帐户名称、阈值、API 密钥、日期范围等内容使用配置文件或脚本顶部的变量。这使得无需深入编辑代码即可轻松调整。例如,在顶部定义
LOW_BALANCE_THRESHOLDS = {"Assets:Bank:Checking": 500, "Assets:Savings": 1000},您的警报脚本可以循环遍历此 dict。 -
测试: 将您的财务自动化视为关键任务代码——因为它确实如 此!为复杂的逻辑编写测试。Beancount 提供了一些测试帮助程序(在内部用于导入器测试),您可以利用它们来模拟账本输入。即使没有花哨的框架,您也可以拥有一个虚拟 CSV 和预期的输出交易,并断言您的导入脚本生成了正确的条目。如果您使用
pytest,您可以轻松地集成这些测试(正如 Alex Watt 通过包装 pytest 的just test命令所做的那样)。 -
版本控制: 将您的账本和脚本置于版本控制 (git) 之下。这不仅为您提供备份和历史记录,而且鼓励您以受控的方式进行更改。您可以标记您的“财务脚本”的发布版本,或在调试问题时查看差异。有些用户甚至在 Git 中跟踪他们的财务记录,以查看一段时间内的变化。只需注意在您的存储库中忽略敏感数据(例如原始报表文件或 API 密钥)。
-
文档: 为未来的您记录您的自定义工作流程。您的存储库中的 README 解释了如何设置环境、如何运行每个脚本以及每个脚本的功能,这在几个月后将非常宝贵。还要注释您的代码,尤其是任何不明显的会计逻辑或 API 交互。
-
Fava 插件维护: 如果您编写了 Fava 扩展,请使其保持简单。Fava 可能会发生变化,因此具有目标功能的小型扩展更容易更新。避免复制过多的逻辑——尽可能使用 Beancount 的查询引擎或现有的帮助程序函数,而不是硬编码可能对账本更改敏感的计算。
-
安全性: 由于您的脚本可能会处理敏感数据并连接到外部服务,因此请谨慎对待它们。不要公开 API 密钥,并考虑在安全的计算机上运行您的自动化。如果您使用托管解决方案或云(例如安排 GitHub Actions 或服务器来运行 Fava),请确保您的账本数据在静态时已加密,并且您对隐私影响感到满意。
通过遵循这些实践,您可以确保您的工作流程保持可靠,即使您的财务状况(以及工具本身)不断发展。您需要可以年复一年重复使用的脚本,只需进行最少的调整。
结论
Beancount 和 Fava 为精通技术的用户提供了一个强大而灵活的平台,可以完全自定义他们的个人财务跟踪。通过编写 Python 脚本,您可以自动化繁琐的任务(如对帐报表)、生成根据您的需求量身定制的丰富报告,并通过及时的警报掌握您的财务状况。我们涵盖了一系列从基本到高级的示例——从简单的查询和 CSV 导入开始,然后转向成熟的 Fava 插件和外部 API 集成。在您实施这些时,请从简单开始并逐步构建。即使是一些小的自动化脚本也可以节省数小时的工作并大大提高准确性。请记住,因为一切都是纯文本和 Python,所以您可以完全控制——您的财务系统与您一起成长,屈服于您的特定需求。祝您编码愉快!
来源: 上述技术来自 Beancount 文档和社区经验。如需进一步阅读,请参阅 Beancount 的官方文档、社区指南和博客,以及 Awesome Beancount 存储库,其中包含指向有用插件和工具的链接。