显示标签为“FastExcel”的博文。显示所有博文
显示标签为“FastExcel”的博文。显示所有博文

2009年3月27日星期五

FastExcel阅读笔记

FastExcel: Version 0.3.3

一、Compound File类设计
和复合文档有关类存放在如下几个package:
edu.npu.fastexcel.compound.io; 把对操作系统文件的IO操作封装成Reader和Writer,主要基于RandomAccessFile。为了提高文件读取速度,使用了Memory Mapped File,基于MappedByteBuffer。
edu.npu.fastexcel.compound.stream; 将Reader和Writer封装成ReadableStream和WritableStream。这里的Stream类就是对应于复合文档里的Stream。
edu.npu.fastexcel.compound; 将Stream包装成StreamReader和StreamWriter,比如SectorStreamReader就是读取复合文档中的扇区。最后提供复合文档的FileReader和FileWriter。
这部分的类设计和Java的IO类设计很类似,再查看一下Java的IO类设计,体味一下,加深理解和印象。
此外这部分还包含一个SummaryInformation类,这个类有点独立,对于Excel读写来说是可有可无的。当初作者可能一时兴起,把它也写出来了。Office文档一般都有Summary Information Stream。加上这个类,整个Compound File类体系就比较完整了。


二、BIFF8类设计
1)BIFF8中比较有用的Record类别:
BOF,EOF:Substream的开始,结尾
SST:Shared String Table,存放所有出现在Worksheet中的字符串
SHEET:存在于Workbook Globals Substream,代表Workbook中的一个Sheet,描述了该Sheet的名字,类别,状态,偏移位置等信息。
DIMENSION:包含当前Sheet的有效范围,起止行号,起止列号。
LABELSST:单元格的字符串内容通过索引指向SST中的某个字符串。
2)Record类设计:
所有支持的BIFF8 Record类别都有对应的Record和Parser(RecordParser)类。比如SSTRecord,SSTParser。Record类用来处理写入,Parser类用来处理读取。
Record类组织成如下几个package:
edu.npu.fastexcel.biff.record; 处理通用的Record类别,可能出现各个Substream里,比如BOF
edu.npu.fastexcel.biff.record.globals; 处理那些只会出现在Globals Substream里的Record类别,比如SHEET
edu.npu.fastexcel.biff.record.sheet; 处理那些只会出现在Worksheet Substream里的Record类别,比如DIMENSION
edu.npu.fastexcel.biff.record.cell; 处理和单元格密切相关的Record类别,比如LABELSST
RecordFactory提供方法返回各种Record类的实例。RecordFactory使用Singleton模式。实现Singleton模式的方法是采用静态内嵌类的静态字段,不知道为什么这么实现?有什么好处?实现代码片段如下:


static class InstanceHolder {
static RecordFactory instance = new RecordFactory();
}

类似Parser类也组织成如下几个package:
edu.npu.fastexcel.biff.parser; 处理通用的Record类别,可能出现各个Substream里,比如BOF
edu.npu.fastexcel.biff.parser.globals; 处理那些只会出现在Workbook Globals Substream里的Record类别,比如SHEET
edu.npu.fastexcel.biff.parser.sheet; 处理那些只会出现在Worksheet Substream里的Record类别,比如DIMENSION
edu.npu.fastexcel.biff.parser.cell; 处理和单元格密切相关的Record类别,比如LABELSST
所有的Parser类都在ParserFactory里登记,存放在一个Map里,Map的键就是Record的类型号,值就是一个Parser对象。这里没有使用Singleton模式,反而有点Factory模式的味道。读取Worksheet Substream时有两种模式:全读取模式和事件模式。全读取模式就是在访问某个Sheet里的某个单元格之前,该Sheet的内容都已经读取完毕,访问时,就是简单的返回内容。事件模式就是在解析一个Sheet的过程中,解析出一个单元格就触发事件,客户程序可以按意愿监听事件。这两种模式有点类似XML的两种解析器,DOM和SAX。显然,事件模式采用监听器模式。监听器接口是SheetReadListener。抽象类SheetReadAdapter implements SheetReadListener,对监听器提供默认实现,这种实现方法应该叫 Adapter模式吧,类似于Java AWT里的MouseEvent,MouseEventListener,MouseAdapter。EventBasedSheetStream是事件的触发器,它有一个字段保存当前的监听器。监听器的设置是通过get和set方法,而不是add和remove方法,可见只支持一个监听器,不支持事件的多播。也许这个情境下的多播没啥意义。
3)BIFF8 File类设计
主要是三个package:
edu.npu.fastexcel.biff; 读取和写入都公用的一些东西,这里只有所有的Record Type常量,放在Types类中。
edu.npu.fastexcel.biff.write; 写入相关的几个Writer。Worksheet Substream和Globals Substream都有各自的Writer。BIFFWriter包含一个WorkBookGlobalsStreamWriter,而WorkBookGlobalsStreamWriter又包含一个SheetStreamWriters的list。BIFFWriter就是利用各个Substream的Writer来实现写入的。当然这些BIFF的Writer最终都要写入到一个复合文档的一个Worksheet Stream里,复合文档的Worksheet Stream的Writer又通过Java IO写入物理文件。也就是说BIFF的Writer使用了Compound File的Writer。这里还没有Excel文件和复合文档的概念,有的还只是Writer和Reader的概念。Excel文件是一个复合文档,看起来设计时要采用继承。而这里只有Writer和Reader的概念,使用的却是组合。我想用继承也是可以的,不过这里用的是组合,组合有组合的好处。有哪些好处呢?
edu.npu.fastexcel.biff.read; 读取相关的类,设计思路和write部分相似。一般来说read部分的类会稍微比较多,处理稍微复杂,因为一般要读取的文件都是MS Excel产生的标准xls文件,内容比较全,包含各种额外的信息,比如格式信息,字体信息。写入生成一个xls文档就比较简单了,只把文本内容写入就行。MS Excel打开这样的文档会用默认的格式展现数据,重要的是文本内容显示出来了,而并不在意文本显示出来是怎样的。


三、用户API设计
这里说的API就是指提供给最终用户使用哪些类。最终用户可能不关心底层的实现细节,他想要了解的是,一个Excel有几个Sheet,每个Sheet有什么内容。因此最高层的API包装应该只是提供诸如Workbook,Sheet等概念。API在edu.npu.fastexcel这个package里,Workbook,Sheet都是接口,FastExcel类的设计应该使用了Facade模式,仅仅提供两个方法,创建一个可读取Workbook和创建一个可写入Workbook,把具体如何使用Writer和Reader实现所需功能的细节全部隐藏起来。这个package里还包含监听器接口SheetReadListener,因为这个接口也是用户所关心的。如果用户程序打算监听事件,就需要实现这个接口。在这个package还有ReadableWorkbook、WritableWorkbook、ReadableSheet、WritableSheet类,我感觉可以把它们放到其他package里更好。


四、建议
1)在每个package下面都包含一个描述文件,描述这个package里的类是干嘛的,就更清晰了。
2)我感觉可以把edu.npu.fastexcel这个package下的ReadableWorkbook、WritableWorkbook、ReadableSheet、WritableSheet类,放到其他package里更好。
3)是否应该使用一个示意为私有package的前缀,从而将一些包含具体实现细节,不易于最终用户重用的package标明为私有,比如在edu.npu.fastexcel下建立一个edu.npu.fastexcel.private子package,然后让Compound,BIFF8等实现全部放到edu.npu.fastexcel.private包下。因为提供诸如可重用的Compound File类并不是作者的意图。

2009年3月26日星期四

Excel文件格式(BIFF)

Excel的文件格式是BIFF(Binary Interchange File Format)。从Excel97 - Excel2003使用的是BIFF version 8,可是说BIFF8是目前最广泛使用的Excel版本。BIFF8基于微软的复合文档格式。Excel文档的内容存放在一个Stream里。一个新建的空白Excel文件,一般包含Root Storge,Workbook Stream,<05H>SummaryInformation Stream,<05H>DocumentSummaryInformation Stream。Workbook Stream就存储了Excel的内容。把整个Excel文件叫Workbook,Workbook里至少有一个Worksheet。Excel文件不仅存放Excel的文本内容,还存放一些格式,比如字体信息,颜色。Excel的逻辑结构可以用下图表示:

Workbook globals里就记录了整个Workbook公用的东西,比如格式,字符串常量,文档保护等。worksheets,workbook globals都是存储在Workbook Stream里,可以把它们看做是substream。各个substream按一定顺序出现在Workbook Stream里,如下图所示:

图中所示,Globals Substream是必须的,而且至少保护一个Sheet Substream。那么这些Substream是如何区分开的,如何知道一个Substream从文件的哪个位置开始,哪里结束呢?这得从BIFF8描述信息所采用的格式来说。这里说的"信息",包含很多,比如格式信息,位置信息,文本信息,图片信息,宏代码信息,等等。这么多信息要记录,BIFF8是采用Record的方式来记录的。一个Record描述一个信息。因此有很多种Record。因此Record Type肯定是Record数据结构的组成部分。Record是不定长的,种类非常多,各个种类的内容格式千差万别。但是基本遵循如下的结构:

结构包含两部分,Record Header和Record Data。Record Header部分的前2字节是Record Type,然后是2字节描述Record Data的长度。文档给出了几乎所有的Record Type,针对每种Record,描述了Record Data里各字节的含义。Substream的开始和结束,就是采用BOF和EOF这两个Record Type。也就是说在Workbook Stream是Record序列,一个Record接着一个Record,有些BOF的record表示接下来要描述一个Substream了。因此处理程序可以一个Record一个Record的读取,先检查读入的Record的Type,如果不认识,那么就不处理Record data部分,如果认识就解析Record data部分的含义。Excel的版本是不断升级的,补丁是不断更新的,新的版本,新的build就可能有新的Record Type。因此旧版本的Excel打开高版本的xls文件,有些Record就无法识别,但是不影响基本数据的读取。FastExcel就是根据这个原理,只处理一些和文本内容相关的必需的Record Type,像格式之类的Record就不处理了,因此就Fast了。
当一个Cell的内容为字符串常量时,BIFF8一般把这个字符串常量用SST这种Record记录在Global Substream,然后在Sheet Substream里使用LabelSST这种Record引用这个字符串。LabelSST这种Record的Record Data部分就包含了行号,列号,字符串索引号(index)等信息。查看文档,可知LabelSST类型的Record Header的Identifier是00FD(hex),size是10,Record data结构如下图所示:

FastExcel的项目主页也说了它们只实现了如下的Parser来处理一些Record,仅仅是针对文本内容,BIFF8的Record类型可是几百个。这里的BOFParser就是处理BOF类型的Record,LabelSSTParser处理LabelSST类型的Record。
* BOFParser
* EOFParser
* BoundSheetParser
* SSTParser
* IndexParser
* DimensionParser
* RowParser
* LabelSSTParser
* RKParser
* MulRKParser
* LabelParser
* BoolerrParser
* XFParser
* FormatParser
* DateModeParser
* StyleParser
* MulBlankParser
* NumberParser
* RStringParser
BIFF8里多字节数据,比如4字节整数,2字节整数,Unicode都是采用Little-Endian的存储方式。

2009年3月25日星期三

微软复合文档格式 (Compound Document)

今天看到一个FastExcel的开源项目,它能够使用纯Java代码读取Excel文件的文本内容以及写入内容保存为Excel文件。这项目的作者应该是一个中国人,因为项目源代码中包含的JUnit测试代码里居然有中文数据。把项目的源代码导入到Eclipse编译,运行了附带的测试用例。源代码里的package开头是edu.npu。baidu一下,npu好像是西北工业大学。作者在项目的简介里介绍了代码的工作原理是依照Excel的文件格式,直接进行读取。而Excel的文件格式是基于复合文档(Compound Document)的。项目主页上有链接给出复合文档格式和Excel文件格式,都是OpenOffice社区整理的。
复合文档简单的说就是在一个文件里可以内嵌其他各种文档,这些内嵌的文档还具有目录结构,可以说复合文档格式就是在一个文件里面实现一个文件系统。从逻辑上说复合文档包含Storage和Stream。Storge相当于我们熟知的操作系统文件系统里的目录,Stream相当于文件。下图就是复合文档的逻辑示意图。和操作系统的文件系统类似,同一个Storge下的Stream不可以重名,不同Storage下的则可以。

其实这个图的内容,很久之前我就了解了,而且很好理解。当初看MFC书籍讲复合文档的时候就在这个层次上。自己也没深究过要实现这样的逻辑结构,物理上该如何存储。今天特地阅读了这个文档格式,25页,不是很长,边阅读边查看一个Excel文件的物理存储结构,这样更好理解。
物理结构图:

开始是一个文件头(Header),固定长度512字节。然后是一个接一个的扇区(Sector),大小是可变的,一般是512字节。这里的Sector可以类比为磁盘上的扇区,反正就是一块文件空间。一个复合文档,除了文件头就是扇区。因此复合文档的内容数据主要是存放在扇区里的。此外有些扇区得用来存储一些元数据,描述扇区和扇区之间的关系,哪些扇区是用来干嘛的,那几个扇区组成一个Stream。每个扇区有一个标识符,也就是编号,从0开始,依次递增。扇区的编号用有符号的4字节整数来表示,这样在内部就可以引用了。扇区号可以是负数,几个负扇区号的特殊含义是:

就像磁盘文件系统,一个文件有多个块组成,在复合文档里,一个Stream也是由多个Sector组成。那么多个Sector的顺序是怎么样的?一个Stream到底有几个Sector组成呢?这是通过Sector Allocation Table(SAT)来实现的。SAT顾名思义就是跟踪Sector的分配情况。SAT也得需要空间来存储啊!当然也是存储在Sector里的。可以说SAT也是一种特殊的Stream,它描述一个Stream拥有哪些Sector。那么谁描述它自己拥有哪些Sector呢?是MSAT,Master Sector Allocation Table。MSAT描述了哪些Sector是被SAT占用的。那谁描述MSAT存放在哪些Sector?好像无休止了!MSAT是存放在Header里的。这下摆脱溯源的烦恼了。但是Header大小是固定512字节的,因此能存的信息是有限制的。如果SAT占用的Sector很多很多,Header里都描述不下,怎么办?还是有办法的,扩展!当Header里的空间不够用时,就用Sector,在Header里有字段记录哪个Sector是MSAT扩展空间的第一个扇区,在MSAT专用的扇区里的最后4字节给出下一个MSAT专用的Sector,这样一个链表就现成了。因此一个MSAT扇区最多可以记录(512-4)/4=127个AST扇区。

通过MSAT就可以找出所有被SAT占用的Sector。这些SAT的Sector就描述了哪个几个Sector是属于一个Stream的以及Sector之间的顺序。这似乎又需要一个链表。对的。这次的链表结构是这样的:



把SAT看做一个数组,数组的元素就是扇区编号,扇区编号用4个字节来存储。比如SAT[i] = j,表示SAT数组的第i个元素是扇区编号j。如果把数组索引就当做扇区编号,SAT[i] = j,SAT[j] = k ... SAT[x] = -2,那么就有一串扇区编号了,i -> j -> k ...-> -2,扇区号-2表示串的结尾。这样,Stream就可以用这么一串扇区序列来描述的,关键是能有一个元数据描述这个Stream,并记录这个Stream的起始扇区号。知道了起始扇区号,就可以通过SAT找到所有其他Sector了。可见SAT也是一个链表结构。好像学数据结构的时候学过这种结构,具体叫啥忘记了。
那怎么存储Stream元数据呢?毫无疑问,是存放在Sector里。SAT就是存储了一些扇号串,看不出哪一串是用来描述Stream的元数据的。于是复合文档就有另一个概念了,目录(Directory)。Directory包含一个描述Stream和Storge的元数据的数据结构。如下图所示,目录里面有目录项,目录项也有编号,从0开始。
每个目录项就描述该项是Stream还是Storge,或者其他。如果是Stream,那么就记录起始扇区号是多少,如果是Storge,包含记录它有哪些Storge和Stream。一个Storge下可能有很多个Storge和Stream。复合文档是把这些子Stream和Storge组成一棵红黑树。这样Storge就可以只记录这颗红黑树的根节点就达到记录所有子Storge和Stream的目的。当然每个目录项得有字段里维护这么一颗红黑树。显然这种方式下,一个Stream或Storge只能在一棵红黑树里。为啥使用红黑树,有什么优点?慢慢思考中!
此外,复合文档还有其他一些概念,比如Short-Stream。Short-Stream一般存储在Root Storge这个目录项里。