使用 Python 避免文档成本
引言
本文展示了我如何将 Python、COM、DocBook、OpenJade 和 Word 集成在一起,为 BEACON(一个可视化编程环境)创建了一个文档工具。这个文档工具在我公司的软件开发方法论中用于代码审查,并带来了显著的(超过 100 万美元)成本节约。
在开始这个项目之前,我没有使用 SGML、XML 或其他文档标记语言的经验。直到我险些通过改编 Mark Hammond 的书《Win32 上的 Python 编程》中的 PythonCOM 到 Word 的直接接口,重新发明了标记语言的概念时,我才意识到从设计和维护的角度来看,一定有更好的方法来完成这项工作。
一次网络搜索为我提供了关于 XML、HTML 和 SGML 等标记语言的速成课程。我的搜索还提供了关于 DocBook SGML(一个流行的开放标准)和 OpenJade(一个可以将 DocBook SGML 翻译成 Word 富文本的开源软件包)的见解。
这种安排并不完美,但我很快意识到我可以用它节省一年的开发和维护成本,并更快地响应新任务。
核心数据流
我的主要任务是将从组织内分散的各种来源中挖掘出的任意数据,翻译成看起来合理的 Microsoft Word 97 报告。
我决定最好通过一个核心应用程序管道来处理这个问题,这些应用程序使用共同的数据约定相互协作。这个管道将由一个 Python 生成器应用程序控制,该应用程序将驱动一组前端翻译器、一个内容插入器和一个后处理格式化程序来生成报告。Python 最近被选为我们部门自动化测试脚本的后续语言。我注意到了 Python 在测试之外的潜力,因此我获得了经理的许可,将其用于这项任务。
此应用程序中的前端翻译器从各种数据源中收集内容(图片、表格、段落),并将其放入字典中。内容插入器创建一个 Word 文档,并将字典中的值插入其中。后处理格式化程序获取结果并根据最新的公司 Word 格式样式模板对其进行修改。
此流程旨在应对我们部门内不同团队和公司级标准的需求变化。例如,报告的布局由生成器应用程序中的布局类确定,该类可以替换为其他类以支持新型报告。
我需要创建的第一个前端翻译器是用于从航空航天工业软件可视化编程工具 BEACON 构建的递归属性列表中获取图片、表格和数据。通过这个翻译器,我获得了大量的样本数据,适合测试 Word 内容插入器。
插入器的问题
内容插入器的第一个版本基于《Win32 上的 Python 编程》中演示的原理,该书详细描述了 Python 如何使用 Word 97 COM 对象模型创建和操作 Word 文档。此实现通过其 COM 接口直接驱动 Word 将内容插入 Word 文档。
不幸的是,COM 接口太慢,无法处理从 BEACON 源代码中提取的大量表格单元格。
更糟糕的是,我为处理节、标题、段落和短语等不同级别的不同样式要求而编写的类的重用问题。
为了解决这些问题,我考虑编写 ASCII 文本文件来指定标准表格的边距、字体、标题级别和插入点。但是,发明我自己的文本指定排版标准,无论是开发还是维护,都将耗费大量时间和成本。
我真正需要的是一个 Python API,可以快速生成 Word 97 中漂亮排版的副本,用于有限的文档集,但在 2001 年,还没有可用的工具来完成这项工作。我唯一的选择是寻找一个开放的排版标准,可以在文档生成后将其翻译成 Word 97 格式。
寻找标准
为了找到解决方案,我花了一些时间调查了可用的开源排版解决方案。最受欢迎的两种是 TeX 和 DocBook,两者都由用 C 语言编写的开源实现支持。
选择 DocBook 是因为它具有更清晰定义和文档化的排版元素生产规则。DocBook 权威指南在详细解释这些规则方面是一笔真正的财富。
DocBook 提供了一组用 DSSSL(文档样式语义和规范语言)编写的文档类型定义(.DTD)和文档样式表(.DSL)文件。DocBook 有两种风格,一种用于 SGML,另一种用于 XML 文档编码。SGML 和 XML 都具有相似的嵌套标签结构以及 DTD 和 DSL 的相同逻辑结构。然而,XML 规则在 2001 年仍在开发中,所以我选择了更可靠和成熟的 SGML 规则集。
DocBook 还提供了编写本地文档类型定义和样式表的能力,分别称为Local.DTD和Local.DSL。这些允许在 DocBook 提供的元素之上引入额外的文档元素及其渲染。
例如,我公司的一些团队希望最终的 Word 文档中包含一些功能,这些功能相当于 SGML 中的任意 Word 域代码支持。
为了支持这一点,我编写了一个本地 DocBook 样式表和定义文件对(Local.DSL,Local.DTD)以发出 RTF 中对应于所需域代码的序列。RTF 需要未转义的字符{, },以及\\,所以我修改了 OpenJade,将样式表中的 Unicode0xFFFD, 0xFFFE,以及0xFFFF映射到这些未转义的字符。
我还在 docbook-apps 邮件列表中找到了一个存档帖子,该帖子对于将 DocBook 下载组件的内容对齐为可用的层次结构非常有帮助。
DocBook 的 Python API 示例
为了支持使用 DocBook 开发必要的内容插入器,我需要一个 Python API,可以快速生成 SGML 格式的文档。为此 API 选择的设计为 DocBook Python 模块中的每个文档元素类型提供抽象类。这些抽象类可以在定义特定文档结构的代码中继承,并且可以任意嵌套,因此每个类都映射到输出文档结构的不同级别或部分。
例如,假设我们希望生成以下表格作为 Word 文档的一部分
名称 类型 statex 整数 statey 长整型 
用于此表的 SGML 文本是根据Local.DSL和Local.DTD编写的,如下所示
<!DOCTYPE informaltable SYSTEM "C:\Local.dtd"> <informaltable frame='all'> <tgroup cols='2' colsep='1' rowsep='1' align='center'> <colspec colname='Name' colwidth='75' align='left'></colspec> <colspec colname='Type' colwidth='64' align='center'></colspec> <thead> <row> <entry><emphasis role='bold'>Name</emphasis></entry> <entry><emphasis role='bold'>Type</emphasis></entry> </row> </thead> <tbody> <row> <entry><phrase role='xe' condition='italic'>statex</phrase></entry> <entry>Integer</entry> </row> <row> <entry><phrase role='xe' condition='italic'>statey</phrase></entry> <entry>Long</entry> </row> </tbody> </tgroup> </informaltable>
这是基于 DocBook 类树生成上述 SGML 的 Python 列表
from DocBook import DocBook
class ItalicIndexPhrase (DocBook.Rules.Phrase):
    "italic indexible text phrase"
    TITLE    = DocBook.Rules.Phrase
    def __init__        (self, text):
        DocBook.Rules.Phrase.__init__ (self, 'xe', 'italic')
        self.data = [ text ]
class NameCell          (DocBook.Rules.Entry):
    "table row cell describing name of identifier (italic and indexible text!)"
    TITLE    = DocBook.Rules.Entry
    def __init__        (self, text):
        DocBook.Rules.Entry.__init__ (self)
        self.data = [ ItalicIndexPhrase (text) ]
class StorageCell       (DocBook.Rules.Entry):
    "table row cell describing storage type of identifier (ordinary text)"
    TITLE    = DocBook.Rules.Entry
    def __init__        (self, text):
        DocBook.Rules.Entry.__init__ (self)
        self.data = text
class TRow              (DocBook.Rules.Row):
    "each row in application's informal table body"
    TITLE    = DocBook.Rules.Row
    def __init__        (self, binding):
        (identifier, storage) = binding
        DocBook.Rules.Row.__init__ (self, [ NameCell    (identifier),
                                            StorageCell (storage)
                                          ])
class TBody             (DocBook.Rules.TBody):
    "application's informal table body"
    TITLE    = DocBook.Rules.TBody
    def __init__        (self, items):
        DocBook.Rules.TBody.__init__ (self, map (TRow, items))
class TGroup            (DocBook.Rules.TGroup):
    "application's informal table group"
    COLSPECS = [ DocBook.Rules.ColSpec ('Name', 75, 'left'),
                 DocBook.Rules.ColSpec ('Type', 64, 'center')
               ]
    SHAPE    = [ '2', '1', '1', 'center' ]
    TBODY    = TBody
class InformalTable     (DocBook.Rules.InformalTable):
    "application's informal table"
    TGROUP   = TGroup
class Example           (DocBook):
    'example application of DocBook formatting class'
    SECTION  = str  (InformalTable)
    def __call__    (self):
        self.data = [ InformalTable ()(self.data) ]
        return DocBook.__call__ (self)
if __name__ == '__main__':
    print Example ([('statex', 'Integer'), ('statey', 'Long')]) ()
OpenJade 接口
OpenJade 是一个开源产品,提供了一种将 SGML 编码文档转换为 Microsoft 富文本格式 (RTF) 的方法。它读取 DocBook DSSSL 样式表和用户本地的 DSSSL 样式表(如果有)。DSSSL 在用户的 SGML 源文本上执行,以写入最终文档并加载到用户的文字处理器中。
对于这个项目,我们希望自动生成 Microsoft Word 可读的文件,因此 OpenJade 被设置为输出 Microsoft Word 富文本文件。OpenJade 作为命令行应用程序运行,因此可以使用Popen4Python 标准库调用,轻松地从 Python 代码中控制。
使用 PythonCOM 进行 Word 自动化后处理
OpenJade 创建的 Microsoft 富文本格式文件整体外观非常吸引人。然而,它们不符合许多公司级别的格式化 Word 文档文件的标准。
编写了一个本地 DSSSL 样式表(Local.DSL)以覆盖几个默认的 DocBook DSSSL 设置,并使其与公司标准保持一致。然而,这并未解决在文档中设置标准化 Word 样式标识符名称的需求。
为了解决这个问题,需要一个重格式化器作为文档管道的最后阶段。它以 COM 对象的身份访问 Word,以便遍历生成 RTF 文档对象模型各个级别的表格、图表、标题和节级别样式标识符。在遍历过程中,它重命名样式标识符,使其符合我们本地复制部门作为标准分发的 Microsoft Word 文档模板(.DOT)文件中提供的样式标识符。
转换后,后处理器将完成的文档保存为 Microsoft Word 文档格式。
所有后处理任务都没有特别困难。一旦对 Win32 应用程序的 COM 接口有了很好的理解,该应用程序在 Python 开发人员手中就退化为另一个库。
投资回报
用于得出此处所示投资回报率 (ROI) 数字的假设是保守的。
我花费了 2001 年的大部分时间开发了一个系统,利用本文中的思想,将 BEACON 可视化编程语言文件中的内容直接自动翻译成 Word 文档。2002 年,我还对软件进行了重大修订。我在开发、维护和支持方面的总投入大约是两年时间内的一半时间。
在 2002 年到 2003 年之间,我们部门有 5 个正在进行的项目,处于不同的开发阶段,复杂程度从 30 个可视化编程文件到多达 150 个,平均大约 75 个。
在这些年中的每个项目中,至少有 2 个主要强制发布,其中每个文件的重要内容都必须经过同行评审:至少有 3 名工程师同时详细检查(主持人、作者和检查员)。
每次发布都要求将每个可视化编程文件渲染成可查看的硬拷贝格式,其中包含所有图表;以及一个交叉引用的表格,其中包含每个图表中所有标识符的存储类、范围、初始值、文档和其他字段。
可视化编程语言 GUI 应用程序 BEACON 没有全面的硬拷贝生成器。相反,需要一名初级工程师在中等监督下,通过在 UNIX 上运行 BEACON 并使用 UNIX 到 Win32 X 终端模拟器来检查文件,手动将文本从 X 终端传输到打开的 Word 文档中。
这些文件中最不复杂的部分(约 20%)需要半天时间。大部分文件(60%)平均需要一整天。这些文件中最复杂的部分(约 20%)至少需要两天。
这浪费了大量的工程劳动力,而这些劳动力本可以更好地用于提高我部门软件产品的质量。
Each project release:       1/5 * 75 *  4 hours     =           60  hours
                            3/5 * 75 *  8 hours     =          360  hours
                            1/5 * 75 * 16 hours     =          240  hours
                                                            -------------
                                                               660  hours
Two major releases per year:        * 2             =        1 320  hours
Five projects needing releases:     * 5             =        6 600  hours
Two year period (2002-2003)         * 2             =       13 200  hours
Total effort avoided:                                       13 200  hours
Automated releases over 2 year period:                         160  hours
My effort (12 * 140 hours per labor month):                  1 680  hours
Total investment:                                            1 840  hours
Net effort avoided, 2002-3:                                 11 360  hours
Net cost avoided by customers 2002-3 at $100/hour        1 136 000  dollars
Net labor years avoided at 1680 hours/year:                   6.76  years
Head count avoided per year:                                  3.38  people
ROI (Total effort avoided / total invested) 2002-3:           7.17
从上表可以看出,仅针对客户的正式发布自动化文档生成的投资回报,显然帮助我部门避免了大量人工劳动成本。
Python 和 DocBook 共同证明了它们在消除现实世界业务流程瓶颈方面的强大组合。
结论
我部门决定采用 Python 并允许我将其与另一个开放标准 DocBook 一起使用,即使仅从避免的文档成本来看,也已在中期获得了可观的投资回报,从而证明了这一决定的正确性。
