Beancount 脚本与插件

Martin Blais,2014 年 7 月

http://furius.ca/beancount/doc/scripting

引言

加载流水线

编写插件

编写脚本

从文件加载

从字符串加载

打印错误

打印条目及往返处理

进一步探索

引言

本文提供了如何编写使用账本内容的脚本的示例和指南,并介绍了如何编写自己的“插件”——这些插件只是您可以配置以转换或程序化生成交易的 Python 函数。这是扩展 Beancount 功能和编写自定义报表的两种主要方法,您只需使用 Python 即可实现。

加载流水线

您需要了解 Beancount 处理输入文件的基本机制。内部而言,加载输入文件的唯一入口是 beancount.loader.load_file() 函数,该函数接受一个输入文件并执行一系列转换步骤,如下图所示:

加载阶段如下:

  1. 解析器。 将输入文件通过解析器处理。此阶段的输出为:

    1. entries:一个元组列表(在 beancount.core.data 中定义),每个元组精确对应文件中的每条指令,并按日期和行号排序。此外,与其它指令同一天出现的交易指令始终保证排在其后。这为后续处理做好了准备。此条目列表将由后续各阶段进行转换和优化。

    2. options_map:一个包含输入文件中选项值的 Python 字典。详情请参见 beancount.parser.options。一旦创建,该字典将不再被修改。

    3. errors:错误对象列表(如有错误发生)。在每个阶段中,新生成的错误都会被收集。

  2. 处理插件。 对每个插件,加载插件模块,并使用前一阶段的 entries 列表和 options_map 调用其函数,用插件返回的新列表替换当前列表。这使得插件能够过滤条目。
    要运行的插件列表由一组实现 Beancount 内置功能的默认插件模块组成,后接用户在输入文件的 “plugin” 选项中提供的插件列表。

  3. 验证。 将生成的条目送入验证阶段,以确保由插件合成或修改的指令符合代码库所依赖的某些不变性条件。此阶段主要用于生成错误信息。

此流水线生成的条目列表包含 beancount.core.data 中定义的各种类型,在典型输入文件中,大多数条目为 Transaction 类型。Beancount 自身的过滤和报告程序直接处理这些条目,您也可以同样处理。这些条目是只读的哑对象(Python namedtuples),没有显式修改其内容的方法。Beancount 内部的所有处理均采用函数式风格,假设条目列表是不可变的1

要运行的用户插件列表属于加载阶段,因为这样允许监控文件变化的程序重新加载文件并重新应用相同的插件列表。同时,这也允许输入文件的作者以这种方式有选择地启用各种可选功能。

编写插件

正如上一节所见,加载 Beancount 文件本质上会生成一个指令列表。许多语法扩展可以通过在插件处理阶段将指令列表转换为新的列表来实现。以下是一些你可能希望对某些指令执行的转换示例:

  • 自动添加一些分录

  • 将某些交易用共同标签关联

  • 合成新的交易

  • 移除或替换某些交易集合

  • 修改各个字段

只要你生成的条目满足某些约束条件(所有分录平衡,所有数据类型符合预期),你可以做任何事情。

插件通过选项语法添加到输入文件中,例如:

plugin "accounting.wash_sales"

通过此指令,加载器将尝试导入 accounting.wash_sales Python 模块(代码必须为 Python 3.3 或更高版本),查找名为 __plugins__ 的特殊属性(该属性应为待执行函数的序列),然后运行这些函数。

有关插件的执行,请参见下方的“执行插件”部分。

例如,你可以在一个名为 “accounting/wash_sales.py” 的文件中放置如下代码:

__plugins__ = ['wash_sales']

def wash_sales(entries, options_map):
    errors = []
    for entry in entries:
        print(type(entry))
    return entries, errors

这是一个最小示例,它不修改条目,仅在控制台打印它们。实际上,为了实现有用的功能,你需要修改列表中的某些条目并输出它们。

然后你可以使用 Beancount 提供的常规工具处理你的输入文件。各种过滤器和报表将基于你的插件输出的条目列表运行。有关操作条目的详细信息和示例,请参阅 beancount.core 中的源代码。

插件配置

某些插件需要配置。为了向插件提供与你的文件相关的特定数据,你可以提供一个配置字符串:

plugin "accounting.wash_sales" "days=31"

插件函数将接收一个额外的参数,即配置字符串。如何解释该字符串完全由插件自身决定。

编写脚本

如果你需要生成无法通过内置过滤和报告功能实现的自定义分析或可视化,可以直接编写一个显式加载指令的脚本。这让你完全掌控程序流程,并可以执行任意操作。

从文件加载

你可以直接调用 beancount.loader.load_file() 加载函数。以下是一个最小脚本示例:

#!/usr/bin/env python3
from beancount import loader

filename = "/path/to/my/input.beancount"
entries, errors, options = loader.load_file(filename)
…

此时,你可以按需处理条目,打印它们、生成 HTML、调用 Python 库等(建议你遵循最佳编程实践,在脚本中使用文档字符串和主函数;上述脚本仅为最小示例)。再次提醒,有关操作条目的详细信息和示例,请参阅 beancount.core 中的源代码。

从字符串加载

您也可以直接解析字符串。使用 beancount.loader.load_string()

#!/usr/bin/env python3
from beancount import loader

entries, errors, options = loader.load_string("""

   2014-02-02 open Assets:TestAccount   USD
   …

""")

如果您希望缩进 Beancount 指令并自动去除缩进,标准库中的 textwrap.dedent 函数非常有用。要获取大量示例,请参阅 Beancount 源代码中的各种测试。

打印错误

默认情况下,加载器在加载时不会打印任何错误;我们默认希望加载过程不产生任何副作用。您可以提供一个可选参数来打印错误,该参数是用于写入错误字符串的函数:

#!/usr/bin/env python3
import sys
from beancount import loader

filename = "/path/to/my/input.beancount"
entries, errors, options = loader.load_file(filename, 
                                            log_errors=sys.stderr)
…

或者,如果您更倾向于显式地自行处理,可以调用 beancount.parser.printer.print_errors() 辅助函数:

#!/usr/bin/env python3
from beancount import loader
from beancount.parser import printer

filename = "/path/to/my/input.beancount"
entries, errors, options = loader.load_file(filename)
printer.print_errors(errors)
…

打印条目与反序列化

直接打印 namedtuple 条目会输出一些可读但格式较差的内容。最好使用 beancount.parser.printer.print_entry() 工具函数以可读方式打印条目:

#!/usr/bin/env python3
from beancount import loader
from beancount.parser import printer

filename = "/path/to/my/input.beancount"
entries, errors, options = loader.load_file(filename)
for entry in entries:
    printer.print_entry(entry)

特别地,Beancount 保证打印机的输出始终可被解析,并在重新读取时生成相同的数据结构。(如果不符合此情况,应视为缺陷。)

有关更多工具函数,请参阅 beancount.parser.printer 模块的源代码。

执行插件

要使插件模块被找到,只需确保它位于您的 PYTHONPATH 环境变量中(您需要确保相关的 __init__.py 文件存在以便导入)。它可以位于您自己的代码中:您无需修改 Beancount 本身。

此外,您还可以在 Beancount 文件中添加以下选项:

option "insert_pythonpath" "True"

这会将包含 Beancount 文件的文件夹添加到 PYTHONPATH 中。结果是,您可以将插件与 Beancount 文件放在同一目录下,并在使用该文件时自动执行它们。

以下是一个简短示例,使用我们上面编写的 wash_sales.py 插件。您的 Beancount 文件应包含以下行:

option "insert_pythonpath" "True"

plugin "wash_sales"

Python 文件 wash_sales.py 应存储在与 .beancount 文件相同的文件夹中。

进一步探索

要了解如何操作条目,您应参考源代码,并学习以下模块的相关内容:

更多详情请参阅 设计文档。祝您使用愉快!