为什么微软Office 的档案格式如此复杂?(以及一些解决方案)

作者:周思博(Joel Spolsky)
属于Joel on Software, http://www.joelonsoftware.com

上个礼拜,微软公开了他们Office 软体的二进位档案格式。这些格式看起来真是太疯狂了。Excel 97-2003 的档案格式是一份349 页的PDF 档。等一下,这还不是全部喔!这份文件中有下面这句耐人寻味的注解:

 每一份Excel 工作簿都被存在一份复合档案(compound file) 中。

你看,Excel 97-2003 的档案其实是OLE 复合文件;这意味着:基本上,每个档案中都存在着一份具体而微的档案系统。你得把另外的九份文件都读完才能全盘掌握,够复杂了吧?而且,这些「规格书」的内容看起来比较像是一堆 C 语言的资料结构,而不是我们凭经验所想像的那种规格书。它是一个完整的阶层式档案系统。

如果你真的开始阅读这些文件,并且幻想着能利用周末的时间写些很炫的程式码(像是把Word 档汇入你的部落格中,或是把你的个人帐簿输出成Excel 档案格式),这些又臭又长的规格书可能很快就会让你心灰意冷。面对这些Office 档案格式,一个普通的程式员可能会有以下的结论:

  • 一定是故意搞得这么复杂,不想让人看懂的
  • 是一个神经错乱的柏格人搞出来的玩意儿
  • 当初是由一群疯狂的程式设计师制定的
  • 绝对不可能读取、建立这些档案而完全不出错

以上四点全是错的。让我带你挖掘一些事实,告诉你为什么这些档案格式会复杂到令人难以想像,为什么它们不是「微软的程式写得很烂」的证明,以及你该如何面对这些事实的方法。

首先我们必须要了解的是:这些二进位档案格式的设计目的和其他的档案格式(像是HTML)是完全不同的。

它们是为了能在老电脑上快速处理而设计的。 对早期版本的Windows 版Excel 来说,记忆体的合理使用量是1 MB,而且在80386 20MHz 的电脑上应该要能跑得够顺。这些档案格式中有许多设计,是为了能更快速开启、储存档案而做的最佳化:

  • 这些档案是以二进位格式储存的,因此读取一笔记录通常只是单纯地「把一个范围的位元资料从磁碟片上搬(复制)到记忆体中」而已。所以最好的做法就是把档案格式订得跟C 语言的资料结构一样,这样在读取档案的时候就完全不需要什么语法分析。通常使用语法分析所花的时间可能会比直接复制多上几个数量级

  • 当有需要的时候,这些档案格式会使用一些非正常的手段,让常用的动作能快些。举例来说:Excel 95 和Excel 97 有时会利用到一种叫做「简单储存」的功能,可以把档案快速地存成某种变种的OLE 复合档案格式——因为原本的档案格式在实战中实在是不够快。Word 也有种东西叫做「快速储存」,在储存的时候只把有变动到的地方附加到档案末端,而不是重新写回整个档案。这么做可以让储存加快14、15 倍。拿以前的硬碟来说,这意味着原本存一个大档案得花13 秒,现在只要不到一秒就解决了。(这也意味着已经被删除的资料还是会存在档案之中。最后这成了人们不想要的功能

这些设计是以既存的函式库为基础的。 如果你打算从头开始写一套可以读取二进位格式的程式的话,你得要能支援Windows Metafile Format(才能显示绘图图型)和OLE 复合储存。如果你是在Windows 下开发的话,这些功能都有函式库可以使用,你可以很轻松地完成这些功能…. 使用这些功能是微软团队的捷径。但如果你想要独力完成每一个功能的话,那你就得全部重写一次了。

Office 大规模地支援复合文件格式。举例来说,你可以把试算表嵌到一份Word 档案里面。一个合格的Word 档案处理程式要能够很聪明地处理内嵌的试算表。

它们不是为了你的脑袋瓜子设计的。 这些格式设计的前题(以那个时间点来说是相当合理的)是:Word 的档案格式只需要能够被Word 读取、写入,就够了。这意味着,每当Word 开发团队的程式员要去决定怎么修改档案格式的时候,他只需要考虑的事情只有(a)这样做够快吗? (b)要怎么做才能在_现有的Word 程式码中_ 做最小幅度的修改就能达成目的。像是SGML 和HTML 那种追求「互换性、标准化」的档案格式,在那个Internet 尚未兴起、档案互换性并不高的年代,并不是设计档案格式时的首要考量——Office 二进位档案格式的诞生时间可比他们早上十年。在那个时候的假设是:如果你需要做文件互换的话,你可以使用「汇出/汇出」功能。事实上Word 也真的有一种为了能轻松达成互换的档案格式,称做 RTF。它从一开始就存在了,而且现在的Word 还是 100% 支援这种格式。

它们必须反应出应用软体的复杂度。 每一个checkbox、每一个格式选项,以及每一项微软Office 中的功能,都必须反应在档案格式中的某个角落。那个在Word 的「段落」选单中,让一个段落在必要的时候能移动到下一页、好让它能和下一段出现在同一页,叫做「与下段同页」的 checkbox?这得出现在档案格式中。而这也就是说,如果你想要实作一个能完美正确地读取Word 文件的仿制品,你得要实作这个功能。如果你正在写一个足以和 Word 竞争、并且可以读取Word 文件的文件处理程式,「从档案格式中读取这个设定」的程式码也许只花你不到几分钟的时间,但你可能得要再花上几个礼拜的时间去调整你的页面规划演算法,好让结果看起来和 Word 一样。否则,你的顾客会用你的程式打开Word 文件,然后发现所有的页面都乱七八糟。

它们必须要能反应出应用程式的沿革。 这些档案格式如此错综复杂,有很大一部份是为了反应那些老旧的、复杂的、讨人厌的,以及很少人用的功能。为了能向前相容,它们依然存在于档案格式中——反正留着那些程式码又不会增加微软什么负担。但如果你真的想要完整透彻地解译、写入这些档案格式,你得要重做一些 15 年前微软的实习生们做过的工作。我们得认清一件事实:最新版的Word 和Excel 是数千个开发人年 的成果。如果你真的想要完完全全地复制这些应用程式的功能,你一样得花数千人年的工夫。档案格式只是简明扼要地列举了应用程式所支援的所有功能。

来点有趣的吧!让我们深入一点看个小例子。Excel 的工作表是由一堆不同型别的BIFF(译注:Binary Interchange File Format)记录组成的。我想要看规格书中的第一个BIFF 记录的定义。它叫做1904 记录。

Excel 的档案格式规格对这个记录的说明很明显地含糊不清。它只提到1904 记录代表了「是否使用了1904 日期系统」。唉,又是一个典型的无用规格说明。要是你正在做Excel 档案相关的程式开发工作,然后你在规格书中发现这个东西,你可能会断定微软又藏了一手。这短短的说明文字没办法给你足够的资讯。你还需要一些额外的知识,我就在这儿说明一下吧。Execl 的工作表可以分为两种:其中一种的日期记算是以1900 年1 月1 日为纪元(包括一个为了和1-2-3 相容而存在的闰年错误,不过现在去谈它没啥意思),另一种则是以1904 年1 月1 日为纪元。Excel 两种日期格式都支援,因为第一版的Excel(Mac 版)直接使用作业系统的纪元系统(这样比较简单);但是 Windows 版的Excel 必须要能汇入使用1900 纪元系统的1-2-3 档案。这就足够让你欲哭无泪了。无论是过去还是现在,没有程式员不想依正道而行的;但有时候,你就是得屈服于现实。

不管是1900 还是1904 的档案都很常见,通常是端看那个档案是在Windows 还是Mac 上产生出来的。如果我们在使用者不知情的情况下自动转换格式,那么资料很可能会被损毁。因此,Excel 不会自动帮你转换。这不只是个把一个bit 从档案中读取出来的问题。这意味着你得把你的日期显示、处理程式码整个翻修一遍,好同时支援这两种纪元系统。我想,这大概得花掉你好几天的时间去实作吧。

没错,当你在写Excel 仿制品的时候,你会发现各式各样处理日期的微妙细节。Excel 在什么情况下会把数字转换成日期?这种格式化是怎么做的?为什么 1/31 会被解译成「1 月31 日」,而1/50 又会被解译成「1950 年1 月 1 日」?这些微妙的动作没办法在文件上三言两语讲清楚;真的要详细地写成文件的话,那文件的内容大概就跟Excel 的原始码没两样了。

别忘了这只是几百个BIFF 记录中的第一个而已,而且还是最简单的一个。大部份的记录都是复杂到会让合格的程式员崩溃的程度。

唯一可能的结论是:微软释出他们的档案格式对大家是很有帮助的,但这并不代表汇入或是储存Office 档案就会变得比以前简单。这些应用软体的功能极为众多而复杂。你也[没办法只实作出20% 的功能,然后预期80% 的使用者会满意](/wiki/The_Joel_on_Software_Translation_Project:%E7%AD%96%E7%95%A5%E6%9B%B8%E4%B9%8B%E5%9B%9B “The Joel on Software Translation Project:策略书之四”)。这些二进位档案的规格书,顶多只会帮你省下对这套复杂的系统做逆向工程的几分钟罢了。

好吧!我答应要给你一些可行方案。好消息是:在绝大部份的一般情况下,想要去读些Office 的二进位档案都是错误的选择。你可以认真地考虑采行另外的两种可行方案:让Office 自己处理这些问题,或是使用比较容易读写的档案格式。

让Office 帮你处理这些繁复的工作。 透过COM Automation 机制,Word 和Excel 提供了极为完整的物件模型。这个物件模型可以让你用写程式的方式完成_任何事情_ 。在许多情况下,你应该要重覆利用Office 内部的功能,而不是试着重新实作一次。以下是一些范例。

  1. 你有一个需要把现有的Word 档案输出成PDF 格式的网页介面应用程式。如果是我的话,我会这么做:写几行的Word VBA 程式,读取一个Word 档案,再利用Word 2007 内建的PDF 转出功能把文件存成PDF 格式。你可以直接执行这几行程式——即使是从在IIS 系统下执行的ASP 或是ASP.NET 程式中呼叫也没问题。它做得到的。第一次把Word 唤起可能会花上几秒钟的时间。然后COM 子系统会把Word 留在记忆体中几分钟,以便你再需要它。所以,第二次执行会快得多。这样的速度对于一个网页介面应用程式来说是够快的了。
  2. 需求和前一点相同,但网页伺服器得用Linux。买一台Windows 2003 伺服器,安装一套完全符合授权的Word,然后写个小的网页服务程式做这件事。使用C# 和ASP.NET,大约是半天的工作量吧。
  3. 需求和前面相同,但规模非常大。丢一台负载分配伺服器在一堆你在第2 点中建立起来的伺服器前面。一行程式码也不用写。

这种方法适用于所有你可能伺服器上处理的一般Office 应用。举例来说:

  • 打开一份Excel 工作簿,排序一些输入格中的资料,重新计算,然后把一些结果放在输出格中。
  • 使用Excel 产生GIF 格式的图表。
  • 不需花时间去思考档案格式,把各式各样的资料从任意种类的Excel 工作表中捉出来。
  • 把Excel 格式的档案转成CSV 表格资料(另一种做法是透过Excel ODBC 驱动程式,利用SQL 查询把资料吸出来)。
  • 编辑Word 文件。
  • 填写Word 格式的表单。
  • 把档案在Office 支援的各种格式间互转(你可以找到一大票文字处理程式和试算表档案的汇入器)。

在所有的这些情况里,你都有办法让Office 物件在非互动模式执行,因此它们不会出现在萤幕上,也不会要求使用者输入任何东西。对了,如果你想用这个方法的话,要注意有一些容易发生问题的地方,而且微软对此并没有正式支援,所以在动手之前请先阅读他们提供的[这篇 knowledge base 文件](http://support.microsoft.com/default.aspx?scid=kb;EN- US;257757)。

使用比较简单写入的档案格式。 如果你只是想要利用程式_产生_ Office 文件,你几乎一定能找到比Office 二位进档更好的格式可用,而且可以让Word 和Excel 顺利开启,没有任何问题。

  • 如果你只是想要产生给Excel 使用的表格资料,请考虑使用CSV。
  • 如果你真的需要CSV 不支援的试算表计算功能,WK1 格式(Lotus 1-2-3 的档案格式)比Excel 简单太多了,而且Excel 也能无误地打开它。
  • 如果你真的、真的必须产生Excel 原生档案,请试试古早版本的Excel… Excel 3.0 是个不错的选择。没有什么复合文件的鬼东西,而且只储存你想用的最少功能。使用这样的档案格式,可以将你所需要输出的BIFF 记录降到最低,然后你只要专心研究规格书的这些部份就好。
  • 至于Word 文件,请考虑使用HTML。Word 也能无误地开启。
  • 如果你真的希望在产生出来的Word 文件里使用很炫的格式,你最好的赌注是产生RTF 文件。Word 能做到的所有事情,都能用RTF 表现出来。但RTF 是以文字格式储存的,不是二进位格式,所以你可以在RTF 文件中做些变动,然后Word 依然读得出来。你可以用Word 产生一份格式很漂亮的文件,在将来要修改的地方放些占位文字,存成RTF 格式,然后再利用简单的文字替代,即时地把那些占位文字换成想要的文字。这样一来,你就有一份无论哪个版本的Word 都可以顺利开启的RTF 档案了。

反正,除非你打算要写一套和Office 竞争的软体,并且得要能完美地开启Office 的档案,否则你大可把这几千个人年的工夫省下来。不管你打算解决什么样的问题,直接去读写Office 的二进位档案是最浪费人力的一种方法。