Beancount 旧版文档自动化转换工具链:从 Google Docs 批量导出 PDF、Markdown 与 RST 的完整实践
2026/9/17 21:11:01 网站建设 项目流程

Beancount 旧版文档自动化转换工具链:从 Google Docs 批量导出 PDF、Markdown 与 RST 的完整实践

【免费下载链接】beancountBeancount: Double-Entry Accounting from Text Files.项目地址: https://gitcode.com/GitHub_Trending/be/beancount

本文基于 Beancount 仓库中experiments/docs_rst/old/目录下的实验性与遗留脚本,系统讲解这套"以 Google Docs 为唯一事实来源、自动下载并转换为离线文档"的工具链。读者将掌握其整体工作流(索引发现 → 批量下载 → 格式转换 → PDF 合并)、核心工具库docs.py中认证、缓存、链接枚举等关键实现,以及单个文档经 pandoc 转换到 Markdown 的细节与格式选型经验,并了解如何通过 Makefile 将转换结果集成进静态文档站构建流程。

这套工具要解决什么问题

Beancount 的早期文档体系以 Google Docs 为源,但发布时需要产出 PDF、Markdown、reStructuredText(RST)等离线格式,以便生成静态站点文档或离线手册。手动逐份导出显然不可行,于是就有了本目录下的自动化脚本。其核心诉求记录在 experiments/docs_rst/README 中:原始文档始终保留在 Google Docs,每次发布前自动完成转换,转换产物再集成回 Beancount 的静态文档站。

整套工具链围绕三个核心概念展开:

  • Google Drive API 集成:通过apiclient(Google API Python 客户端)检索、列出并导出文档,认证采用服务账户(service account)方式;
  • 基于索引的文档发现:先从 Drive 中定位名为 "Beancount - Index" 的索引文档,解析其中的链接得到全部相关文档章节;
  • 多格式转换管线:支持 DOCX、PDF、ODT、HTML、TXT、RTF 等导出格式,并借助pandocpdftk做进一步加工(如 DOCX → Markdown、多个 PDF 合并)。

目录结构与整体工作流

experiments/docs_rst/old/目录下共有 5 个文件,职责划分清晰:

文件角色
docs.py共享工具库:认证、缓存、索引发现、下载、PDF 合并
download_docs.py批量下载索引中全部关联文档,可按需合并 PDF
convert_doc.py下载并转换单个 Google Doc,经 pandoc 产出 Markdown
convert_filter_docx.pypandoc 过滤器,用于定制 DOCX 转换行为
docs_test.py工具库的单元测试

端到端流程可以概括为四步:

  1. 认证:用服务账户凭据换取授权的 HTTP 客户端;
  2. 发现:在 Drive 中检索Beancount - Index文档,导出其 HTML 并解析出全部docs.google.com/document/d/...链接;
  3. 下载:对每个文档 id 导出为目标 MIME 类型的文件,写入本地目录;
  4. 后处理:若目标为 PDF,则用pdftk将所有 PDF 合并为一个文件;若目标为 Markdown,则用 pandoc 加自定义过滤器转换。

核心工具库 docs.py 源码剖析

docs.py 是整套工具链的基石,其模块头注释明确写着"Utility functions to download and convert Google Docs"(下载并转换 Google Docs 的工具函数),版权与许可证信息显示其为 GNU GPLv2,维护周期跨越 2014 至 2025 年。

Cache 类:远程调用的持久化缓存

远程工作场景下,同一文档可能被反复下载,Cache 类 专门解决这一问题。它的设计很有借鉴价值:

  • 构造时接收缓存文件名与delegate_factory工厂,内部用shelve打开持久化存储;
  • 通过__getattr__将任意属性访问(如files.list)包装为Cache.Method
  • Method.__call__(方法名, 位置参数, 排序后的关键字参数)整体 pickle 后取 MD5 摘要作为缓存键;命中则直接返回缓存值,未命中才真正调用 Google API 并将结果写回 shelve;
  • 返回的ExecuteWrapper模拟 Google API 客户端的.execute()语义,使得调用方无需感知缓存层存在。

从实现看,这是一种"透明方法级缓存"模式:调用方照常写files.list(q=...).execute(),缓存逻辑全部隐藏在代理之后,且日志会输出Cache miss for <digest>便于排查。

服务账户认证

get_auth_via_service_account 从环境变量HOME下的.google-apis-service-account.json读取服务账户 JSON 密钥,通过oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name构造凭据,再用httplib2.Http()授权。它返回(credentials, http)二元组,供discovery.build构建 Drive v3 服务。注意源码第 18 行留有 TODO 注释:oauth2client已被官方弃用,这是一处典型的"遗留代码"标志。

索引文档的定位与链接枚举

find_index_document 通过files.list(q="name = 'Beancount - Index'")精确检索索引文档,若匹配数量不等于 1 会抛出ValueError,这是对数据一致性的强校验。

enumerate_linked_documents 先将索引文档导出为text/html,再用正则https?://docs.google.com/document/d/([^/";&]+)扫描全部文档链接,去重后返回文档 id 列表(索引自身也包含在内)。

批量下载与 CONVERSION_MAP

download_docs 接收文档 id 列表与输出目录,为每个文档执行files.get取元数据(用于命名)与files.export取内容。文件名经过清洗:re.sub("[^A-Za-z0-9=-]", "_", name)将非法字符替换为下划线,再依次规整连续下划线与_-_序列。下载后校验文件大小,空文件会被判为下载失败并跳过,同时记录 error 日志。

格式与 MIME 类型、后处理函数的映射关系集中在 CONVERSION_MAP:

扩展名MIME 类型后处理
htmltext/html
txttext/plain
rtfapplication/rtf
pdfapplication/pdfconvert_pdf(合并)
odtapplication/vnd.oasis.opendocument.text
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document

pdftk 合并多个 PDF

collate_pdf_filenames 构造pdftk <file1> <file2> ... cat output <out>命令并用subprocess.Popen执行,捕获FileNotFoundError/PermissionError并给出"pdftk is probably not installed"的友好提示;若pdftk返回码非零则抛出IOError

批量下载脚本 download_docs.py

download_docs.py 的模块 docstring 描述了其目标:把 Google Drive 上所有 Beancount 文档下载下来并"烘焙"成一个漂亮的 PDF,同时留下 TODO:后续计划用 Pandoc 将全部文档转换为原生格式,并编写自定义过滤器以更好地识别代码块,再输出为 Markdown 等格式。

命令行用法

脚本通过argparse接收三个参数:

./download_docs.py [extension] [output] [--cache CACHE]
  • extension:必填位置参数,可选值即CONVERSION_MAP的键(htmltxtrtfpdfodtdocx),默认pdf
  • output:必填位置参数,输出文件/目录路径(默认值虽为None,但下方os.makedirs(args.output)会实际使用);
  • --cache:可选,指定服务缓存文件路径,用于离线工作。

执行流程

main()(download_docs.py)的核心逻辑如下:

  1. 配置日志级别为 INFO;
  2. 定义get_service():请求https://www.googleapis.com/auth/drive作用域,经服务账户认证后discovery.build("drive", "v3", http=http)返回service.files()
  3. 若传了--cache,用docs.Cache包装服务对象,否则直接用原始服务;
  4. 调用find_index_documentenumerate_linked_documents得到全部文档 id;
  5. CONVERSION_MAP取出后处理函数,创建输出目录;
  6. download_docs完成下载;若后处理函数存在(即 PDF 场景)则调用之合并文件,最后打印输出位置。

单文档转换 convert_doc.py 与 pandoc 过滤器

convert_doc.py 面向"只处理一个文档"的场景,模块注释点明了动机:Google 自带的 Markdown 导出质量不足,因此需要下载多种导出格式,再经 Pandoc 转换以得到最忠实的 Markdown。

pandoc()辅助函数(convert_doc.py)构造命令:

pandoc -f <informat> -t markdown --filter convert_filter_docx.py <filename>

其中--filter指向同目录下的 convert_filter_docx.py。该过滤器基于pandocfilters.toJSONFilter实现,目前只关注BlockQuote节点并打印到 stderr——从实现看,这是一个最小骨架,用于后续定制"多余引用块"等转换细节(对应notes-about-formats.txt中"docx 转换会带出多余 blockquote"的调研结论)。

main()(convert_doc.py)接收docidoutput--cache参数,对同一文档分别以docxodttxtpdfhtmlrtf六种格式下载到临时目录,其中下载 DOCX 后调用 pandoc 转换的代码被注释保留(# native = pandoc(filenames[0], 'docx')),说明多格式并行下载是"广撒网、按需取"策略:不同格式各自携带不同信息(如 DOCX 结构最好但缺标题,HTML 有标题却混入大量样式),最终拼接出完整结果。

格式选型:notes-about-formats 的调研结论

notes-about-formats.txt 记录了作者对 Google Drive 导出格式的系统性评估,是理解本工具链设计取舍的第一手材料:

  • 可下载格式htmltxtrtfodtpdfdocxepub
  • pandoc 可读取的格式htmlodtdocxepub
  • 实测观察
    • HTML 与 epub 输出"很糟糕",混入大量样式元素;
    • ODT 解析出的结构信息很少,章节标题甚至不被识别为标题而只是锚点标记;
    • DOCX 是质量最好的选择——但它不产出标题(因此需要下载多种格式各取所长),含有多余的 blockquote,且代码块无法被识别、账户名前的空白会丢失;
  • 输出到 HTML时,结果需要被拼接进带 UTF-8 编码的 HTML 包装器中。

这些发现直接对应了代码中的设计:convert_doc.py为什么要同时下载六种格式、pandoc 过滤器为什么要处理BlockQuote

单元测试与跳过机制

docs_test.py 使用unittest.mock.MagicMock模拟 Google API 服务对象,验证find_index_document能从files().list().execute()的返回中正确提取文档 id。值得注意的工程实践是:整个测试类用@unittest.skipIf(apiclient is None, ...)装饰——若环境未安装google-api-python-client则自动跳过,保证在无 Google 依赖的 CI 环境中测试套件依然能通过。

与构建流程的集成:Makefile 三阶段

目录级的 Makefile 把工具链串成了三个可复现的阶段:

download: # ./download_docs.py $(CONVERT_DOCS) —— 下载全部文档 convert: # ./convert_docs.py $(CONVERT_DOCS) —— 转换为 Markdown copy: # ./copy_docs.py $(CONVERT_DOCS) $(RST_DOCS) —— 拷贝到静态文档源

其中CONVERT_DOCS=$(HOME)/docs是下载产物目录,RST_DOCS=$(HOME)/p/beancount-docs是静态文档生成器源码目录,拷贝后即可用 Sphinx 构建并检查效果。这印证了 README 的定位:转换的最终目的是把产物集成进 Beancount 的静态文档站(其对应生成源即aumayr.github.io/beancount-docs-static),而 Google Docs 始终保持为"pristine source"(原始权威来源)。

适用前提与遗留限制

  • 依赖外部工具:PDF 合并依赖pdftk,缺失时脚本会明确报错退出;Markdown 转换依赖pandocpandocfilters
  • 认证文件:必须在$HOME/.google-apis-service-account.json提供服务账户密钥,并授予 Drive 作用域;oauth2client已弃用(源码 TODO 已注明);
  • 实验性定位:本目录属于experiments/docs_rst/old/,脚本自带"experimental and legacy"定位,与同目录下更新的 convert_docs.py、copy_docs.py 等并存;README 中列出的静态站点链接是 2016 年前后的历史产物,如今 Beancount 文档已主要内置于仓库本体(如 docs.md 及各子模块的docs.md)。

总体而言,这套旧工具链的价值在于:它以不足 300 行的代码,完整示范了"Google Docs 权威源 + Drive API 自动化导出 + pandoc 二次加工 + 静态站构建"的文档发布流水线,其 Cache 缓存模式、索引发现机制与格式选型方法论,对任何以云文档为源、需要自动发布离线文档的项目都具有直接的参考意义。

【免费下载链接】beancountBeancount: Double-Entry Accounting from Text Files.项目地址: https://gitcode.com/GitHub_Trending/be/beancount

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询