Office檔案格式(Office文件格式)

1. Ole物件檔

Office檔案或是Embeded Object,這些檔案都是透過IStorage界面來儲存的,一般稱為OLE物件檔(也稱為Laola檔)。什麼是IStorage界面呢?它是Windows所提供的一個OLE界面,主要是提供給OLE物件做為儲存資料之用。IStorage之所以好用,主要是它提供類似一個目錄/子目錄/檔案的階層式組織,統包在一個檔案裡,如此其他物件便可以在同一個檔案裡,以目錄階層的方式,儲存多種不同的資料。因此要解Office檔,首先必須要弄清IStorage所儲存的OLE物件檔格式。

為了快速存取類似目錄檔案的結構,IStorage模仿了類似實際的目錄檔案結構。它將檔案中每512 byte視為一個單位,稱為大區塊資料(BBD,Big Block Data)。不過說實在的,這些名稱真的很容易令人混淆不清(看過MS的文件就會知道,因為還有很多定義的用字都很接近)。因此這邊我不沿用MS的名稱定義,大家把它想成是一個磁區(sector)就對了,反正IStorage就是在模仿磁碟目錄結構,直接使用磁碟的用詞反而容易懂。而為了管理這些磁區,當然就要有FAT(檔案磁區配置表,MS稱它為大區塊庫,真難懂)。不過檔案一詞在這邊反而會混淆,因此儲存在IStorage的“檔案”,我便沿用MS的名稱,稱為資料串(stream)。以下便開始從檔頭說起:

檔頭當然就是在檔案的開頭處,剛好是一個磁區(512 byte)。由於這個檔頭是固定有的,不能被使用,因此實際的磁區位置,必須從512 byte開始計算起。也就是說Sec#0的位置在512,Sec#1的位置在1024, 以此類推。以下便是檔頭的重要資料:

00h (8 byte):檔頭標記,一開始的前8個byte固定為D0 CF 11 E0 A1 B1 1A E1,否則便不是OLE物件檔
2Ch (long):FAT使用的磁區數
30h (long):檔案目錄結構屬性開始的磁區
3Ch (long):小資料儲存區FAT開始的磁區
44h:額外記錄FAT使用磁區的開始磁區
48h:額外記錄FAT使用磁區的磁區數
4Ch開始:FAT使用的磁區(long),數量由2Ch中的磁區數決定

這邊注意到有一個小資料儲存區。由於每個磁區都是512 byte,拿來放小資料的話,會非常浪費空間,因此IStorage便將較小的資料,統一另行儲存。各位可以將小資料儲存區想成是另一個檔案,這個檔案又是模擬目錄檔案結構,只是每個磁區縮小到64 byte而已。這個部份我待會再談,先將基本512 byte磁區的模擬方式弄懂,小資料儲存區的格式便更容易懂了。

由於在IStorage中,所有資料在磁區的儲存次序,都和FAT有關,因此必須先弄清FAT的配置方式。從檔頭中,我們可以知道FAT使用了那些磁區,將這些磁區的資料組合在一起,便是真正的FAT資料(磁區可能跳來跳去的)。而在FAT資料裡,其實便是記錄著每個磁區的下一個磁區是什麼(每筆資料均為long)。 這邊的磁區值可能為:

0xfffffffd (-3):特殊區塊(FAT使用的磁區便是)
0xfffffffe (-2):結束標記(表示已無下個資料磁區)
0xffffffff (-1):尚未被使用的磁區
其他:下個資料所在的磁區

因此如果知道一個資料串從那個磁區開始,便可以直接參照FAT,看看資料所在的下個磁區是在那裡。例如Sec#10,便查看FAT中第10個(由0編起)的long值磁區編號,便是下一個。如此一路查下去,便可以得到整個資料串所使用的磁區數和次序。應該很簡單吧?

這裡有一個情況必須特別處理的,那就是超大型的OLE檔。由於FAT表使用的磁區是放在Ole檔頭4Ch開始的地方,但因為檔頭只有512 byte而已,因此只能記錄109個磁區(約為7MB左右)。如果Ole檔更大,使得FAT使用的磁區表記錄空間不夠使用時,便必須讀取44h所指的磁區,視為下一個記錄FAT使用磁區的磁區。只是如果還是不夠儲存時,再下一個磁區在那裡呢?其實它是記錄在該磁區內容的最後一個值,以串列形式組成(-1表示結束)。因此在讀取整個FAT表時,必須考慮到此一情形。

接下來我們來看看檔案目錄結構屬性的資料(起始磁區記錄在檔頭)。這個部份也是一個資料串,因此算出來的位置,都是相對於資料串,你必須換算成是第幾個磁區,然後再從FAT裡得到實際所在的磁區。例如所要的資料是在第540 byte處,那麼由資料串開頭算起是Sec#1,如果在FAT表裡查到這個資料串的磁區編號為{9,13,22,41},實際所在的磁區便是Sec#13(offset也要重算)。不懂的話請再想想弄清楚。

檔案目錄結構屬性的資料,每個佔128 byte,並以指標加以串連。以下便是每個結構屬性的值(第一個結構屬性就是根目錄):

00h:共64 byte,記錄資料串名稱(unicode),根目錄一律為"Root Entry"(把它想成是檔名或目錄名就對了)
40h (short):資料串名稱的byte長度,0表這個屬性沒有用到(deleted)
42h (char):本結構屬性的形態,1=目錄,2=檔案,5=根目錄
44h (long):上一個結構屬性指標,-1表沒有
48h (long):下一個結構屬性指標,-1表沒有
4Ch (long):若本結構屬性的形態為目錄或根目錄,則指向本目錄裡各子目錄/檔案的第一個結構屬性(指標)
74h (long):資料所在的起始磁區
78h (long):資料的byte數

大家可以看到,這個結構其實和磁碟的檔案目錄結構沒什麼兩樣,同樣也是樹狀階層式的。其中結構屬性指標指的是第幾個結構屬性值(由0編起),相對於資料串起點的位置,便是指標值*128。

有一點必須特別注意的是,上一個/下一個結構屬性指標並非雙向鏈結,而是隨意鍵結,例如:

attr#3:上一個是#2,下一個是#4
attr#2:上一個是#5,沒有下一個
attr#5:沒有上一個,沒有下一個
attr#4:沒有上一個,沒有下一個

因此共計有2,3,4,5等4個結構屬性。也就是說,你必須將所有鍵結的結構屬性都展開到,才能得知整層的檔案目錄資料。

另外,資料所在的起始磁區,可能指向標準的512 byte磁區,也可能指向小資料儲存區裡的64 byte磁區,其分別在於資料的byte數。若byte數>=4096,便是儲存在512 byte磁區,否則便是儲存在64 byte磁區。 唯一例外的是根目錄,一律儲存在512 byte磁區中。

至於資料串的內容,除了根目錄Root Entry外,其他都是使用者自己訂的。因此每個資料串裡的資料如何安排,表示什麼意思,都必須另行處理,這點無關IStorage的事。IStorage只是盡責地,將使用者要儲存的資料,依照上面的格式儲存下來而已。

接下來開始說明小資料儲存區的部份。在檔頭3Ch的地方記錄了小資料儲存區FAT開始的磁區。小資料儲存區FAT也是一個資料串,必須將整個資料串讀入後才能處理。這個小資料儲存區FAT裡的資料格式,和之前提到FAT資料格式都是完全一模一樣,以串列的方式來記錄各資料串使用的磁區。但小資料磁區裡的資料實際上在那裡呢?其實就是根目錄裡的資料串。這也就是為什麼根目錄的資料,一律都是放在標準512 byte磁區裡。而在根目錄資料串裡,便是以64 byte為一單位,切割成小磁區,供小資料存放使用。於是, 要取得一個資料串的過程,便成為:

(1) 在檔案目錄結構裡,找出該資料串相同名稱的屬性。屬性裡的資料磁區指標和大小,便是資料所在的位置。
(2) 如果資料串的資料,是在標準512 byte磁區中,便到FAT表裡找出該資料串使用的磁區,一個一個依次載入。注意OLE檔可能不是剛好512 byte,如果是最後一個sector,必須只讀取檔案長度剩餘的部份。
(3) 如果資料串的資料,是在小資料儲存區中,便必須載入根目錄的整個資料串,然後到小資料儲存區FAT表裡,找出該資料串使用的小資料磁區,再一個一個從根目錄資料串的小磁區裡載入。

其實整個OLE物件檔的結構應該算是很簡單。各位有空可以去看一下類似的文件( http://user.cs.tu-berlin.de/~schwartz/pmh/guide.html),即使你都已經弄懂了OLE物件檔,還是可能看不懂這些文件。

2. Office檔的摘要內容

Office檔的摘要主要有兩個部份,分別放在"\005DocumentSummaryInformation"和"\005SummaryInformation"這兩個資料串中。以下便分別加以說明(請配合MS Word的摘要設定操作來看比較容易懂):

(1) "\005DocumentSummaryInformation"資料串

18h (long):GUID數目
1Ch開始每20 byte:依次存放{GUID+屬性組資料位置},前者為16 byte,後者為long

在這個資料串裡,可以記錄兩種屬性組,一個是DocumentSummaryInformation,一個是UserDefinedProperties。以下便是這兩個屬性組的GUID:

DocumentSummaryInformation:
0x02,0xD5,0xCD,0xD5,0x9C,0x2E,0x1B,0x10,0x93,0x97,0x08,0x00,0x2B,0x2C,0xF9,0xAE
UserDefinedProperties:
0x05,0xD5,0xCD,0xD5,0x9C,0x2E,0x1B,0x10,0x93,0x97,0x08,0x00,0x2B,0x2C,0xF9,0xAE

使用者自訂的摘要部份待會再說,底下便先針對標準的摘要部份,也就是DocumentSummaryInformation。從上述的GUID比對到後,後面便是指向這個屬性組(Property Set)的資料區塊。屬性組資料區塊的格式如下(這邊的位置都是相對於資料區塊,而非整個資料串):

00h (long):資料區塊大小
04h (long):屬性數目
08h開始每8 byte:屬性編號(long)+屬性資料位置

標準的摘要屬性編號(Property ID)是固定的,以下便是各編號的意義(大部份配合MS Word摘要設定就能懂了):

01: CodePage, long - 屬性組文字資料使用的編碼方式(固定的ProperSet ID),這個部份我最後再來說明
02: Category, 字串 - 類別
03: PresentationTarget, 字串 - 展示方式(印表機/螢幕),PowerPoint在用的
04: Bytes, long - 文件byte數
05: Lines, long - 文件行數
06: Paragraphs, long - 文件段落數
07: Slides, long - 文件Slides數,PowerPoint在用的
08: Notes, long - 有註記的頁數,PowerPoint在用的
09: HiddenSlides, long - 隱藏的Slides數,PowerPoint在用的
0A: MMClips, long - 聲音/影片數,PowerPoint在用的
0B: ScaleCrop, bool - 是否需要縮放Thumbnail,FindFile在用的
0C: HeadingPairs, variant/vector - Office內部在用的,不管它
0D: TitlesofParts, 字串/vector - 所有的文件名稱(如Excel的Sheet名稱,PowerPoint的Slide標題等等)
0E: Manager, 字串 - 主管
0F: Company, 字串 - 公司
10: LinksUpToDate, bool - Office內部在用的,不管它

至於屬性的資料,其格式為:

屬性資料形態(long)+實際屬性資料

由於我們的目的是要建索引,因此只需取出文字的屬性資料即可。文字屬性的資料形態為0x1E,實際的屬性資料則為:

字串資料byte數+字串資料

如果要解使用者自訂的屬性組,只需找出所有的屬性資料,只要是文字屬性的,都加以讀出即可。

(2) "\005SummaryInformation"資料串

其格式和"\005DocumentSummaryInformation"完全一模一樣,差別只有屬性編號的意義。以下我只挑出重要的列一下:

GUID:0xE0,0x85,0x9F,0xF2,0xF9,0x4F,0x68,0x10,0xAB,0x91,0x08,0x00,0x2B,0x27,0xB3,0xD9

02: Title, 字串 - 標題
03: Subject, 字串 - 主旨
04: Author, 字串 - 作者
05: Keyword, 字串 - 關鍵字
06: Commenct, 字串 - 註解

其中若標題沒有在本資料串時,應該到前述DocumentSummaryInformation資訊裡的TitlesofParts中取得。

關於編碼方式(Code Page)的重要值域:

932: 日文
936: 簡體中文
949: 韓文
950: 繁體中文
1200: Unicode
1252: 英文

如果取得的屬性值料是字串的話,便必須依照指定的編碼方式進行轉碼的動作。

3. Word 97的格式

Word檔案的資料,主要是記錄在"WordDocument"資料串與"0Table"/"1Table"資料串中,由於文字資料主要是記錄在"WordDocument"資料串中,因此我們先從此一部份著手。至於Embeded Object是另行記錄的, 這我們最後再來說明。

"WordDocument"資料串一開頭的地方,稱為FIB(File Information Block),裡面記錄了各種重要的資訊與指標。因此要解出文字資料,首先必須弄懂FIB。以下便列出FIB比較重要的部份:

0002h (short):版本
000Ah (short):狀態旗標(以bit0為最低位元)
bit 2 (mask=0x0004):是否為複雜格式
bit 8 (mask=0x0100):檔案是否加密
bit 9 (mask=x00200):0表使用"0Table"資料串, 1表"1Table"資料串
bit 14 (mask=0x4000):是否為遠東版
000Eh (short):加密鍵值
0018h (long):文字資料起始位置(非複雜格式時)
001Ch (long):文字資料結束位置+1(非複雜格式時)
0020h (short):後面的短整數參數數目
0022h開始:短整數參數,比較重要的是第13個(由0編起,003Ch),若為遠東版,則這個參數記錄了語系ID(後述)
????h (short):後面的長整數參數數目
????h開始:長整數參數
#0:資料串長度
#1:建立日期
#2:修改日期
#3:文件(document)文字長度
#4:註腳(footnote)文字長度
#5:頁眉(header)文字長度
#6:巨集(macro)文字長度
#7:annotation文字長度
#8:endnote文字長度
#9:文字塊文字長度
#10:頁眉文字塊文字長度
????h (short):FC/LCB數目
????h (long/long):FC/LCB資料
#33:piece table位置/大小

其中處理起來比較麻煩的是複雜格式,這是當使用者使用快速存檔(Quick Save)時,才會形成的格式,這部份後面再說明。至於加密的文件,目前不予以處理(要自行對Word檔解密,會花太多時間,同時加密文件無法建索引,應該是很正常的)。

關於語系ID的值域,bit 0-9 = 主語系,bit 10-15 = 次語系,相關資料可以參考MSDN,以下是一些辨識方法:

語系ID = 0x0404表繁體中文,0x0804表簡體中文
主語系 = 0x09表英文,0x11表日文,0x12表韓文

這些語系資料可以提供,當Word檔裡面記錄的不是Unicode時,應該如何轉碼。這樣即使外界設錯內碼格式,我們仍能正確轉成Unicode處理。以下便開始說明非複雜非加密格式的Word文件如何抽出文字資料。

Word的文字資料,分成document,footnote,header...等好幾個部份(參見FIB裡的長整數參數),這些文字資料都是相連接在一起儲存的。儲存的起終位置便記錄在FIB的18h,1Ch裡。然而我們卻無法直接到裡面取出文字資料,因為這些文字資料是以512 byte為一單位放在一起,而且語系並不一定相同(可能是ASCII,也可能是Unicode,這樣做的目的當然是要檔案小一些)。因此我們必須藉助piece table的資訊來取得真正的文字資料。至於如何取得,待會再來說明。因為較早期的word檔並沒有piece table,這種情況下表示文字資料是以同一種語系儲存的,如此取得文字資料的方式就很簡單,只需到文字資料起始位置開始,依照FIB長整數參數裡記錄的各部份文字長度,一個一個加以讀出處理便可以了。不過在處理前必須先辨別儲存的文字是ASCII形式,還是Unicode形式。方法就是:

文字byte數=文字資料結束位置-文字資料開始位置
文字總字數=文件文字長度+註腳文字長度+頁眉文字長度+....

如果文字總字數*2=文字byte數(unicode每個字是2 byte),便是unicode形式。不過由於Word有時在文字資料最後面會多加一個段落標記(總字數少了),因此判斷時要以"文字總字數*2<=文字byte數"為準。另外,取出的Word文字資料裡也有一些控制字元必須特別加以處理,這些字元包括(以ASCII字碼10進位列出):

07:cell mark
09:tab
11:break line
12:page break/section mark
13:paragraph end
14:clumn break
19:field begin
20:field seperator
21:field end
30:non-breaking hyphen
31:non-required hyphen
160:non-breaking space

如果字串是ASCII形式,則還會有以下的一些特殊字元:

85h:...
92h:'
93h:"
96h:--
97h:---

其中比較需要注意的是field start(19)/field seperator(20)/field end(21)等三個字元。這些字元是用來夾住word的一些特殊標記文字,例如hyperlink的相關資訊,以及"目錄"等由word自行做出的結構,其中field start到field seperator之間的字串是控制用的(不顯示),field seperator到field end之間的字串則是顯示用的,故前半部的資料應瀘除,後半部的資料要取出。如果沒有特別處理的話,便會出現一堆如"HYPERLINK \l "TOC1899651""等無意義字串。

如果有piece table時,文字資料便不能像前面所說的,直接判斷並加以讀取,必須經由piece table的資訊加以判斷。piece table資訊是記錄在Table資料串,至於是使用"0Table"資料串,還是"1Table"資料串,可由FIB裡的資訊得知。取得Table資料串後,piece table的資訊為:

(byte) 1
(short) grpprl大小
grpprl
(byte) 1
(short) grpprl大小
grpprl
...
(byte) 2
(long) plcfpcd大小
plcfpcd

我們要的是plcfpcd的資訊,因此必須將grpprl全部略過。plcfpcd主要由PLC和PCD兩個陣列所組成,因此必須先算出元素數目:(plcfpcd大小-4)/12。其中PLC元素數目要再加一。PLC陣列的主要目的,是用來記錄累積的文字數,因此第i個文字piece的文字字數,便是PLC[i+1]-PLC[i],這也就是為何PLC元素要多一個的原因。PCD陣列,主要用來取得文字piece的位置,其元素格式如下:

(short) 狀態值
(long) 文字piece的位置,最高的第二個位元(0x40000000)若為1,表示文字是ASCII,否則為Unicode
(short) 記錄PRM或grpprl的索引值(這部份無關重要,可以不管)

因此要解出所有文字資訊,只須依次取得各文字piece的位置/字數,並決定為何種語系,再加以讀出處理即可。不過要注意的是,文字piece的位置,當形式為ASCII時,其位置會x2,因此換算成實際位置時,必須再除以2。

至於複雜格式是什麼呢?其實就是文字資料並沒有集中放在一起,而是隨著編輯過程而分散在各處。要取得這些文字資料,其實只須依照piece table裡面的資訊來取便可以了。

4. Word 95的格式

Word 95的檔案格式,基本上和Word97差不多,然而由於該時期的版本並未支援Unicode,因此檔案中文字的編碼並非Unicode,而是以一種很怪、類似於Unicode的方式儲存。也就是說,中/英文都是2 byte,但中文記錄的不是Unicode,而是它的兩個ASCII字碼。例如"一"的BIG5碼是A440h,它便將A440h視為一個2 byte字碼儲存起來,因此先存40h,再存A4h。於是文字資料讀取以後,還是必須進行轉碼的動作,才能得到實際的Unicode。

5. Word更早期版本的格式

Word更早期版本的檔案格式,本身並非OLE物件檔。事實上,該檔案的內容便是OLE裡的WordDocument資料串。也就是說,當IStorage界面製訂出來以後,word便將整個檔案視為資料串,存到OLE物件檔裡。因此要解這種早期版本,只需直接將它視為WordDocument取出的資料串,然後一樣到18h的地方讀取文字位置和長度,即可解出文字資料。不過這種早期版本的word檔,還沒有Unicode的觀念,因此存的全部都是ASCII碼。

6. RTF檔格式

.doc的檔案,不只是Word檔格式而已,還可能是RTF檔或是純文字檔,因此在處理前必須先行判斷。以下便針對RTF檔的格式進行說明。在說明RTF的格式之前,我們先看一下RTF的一個簡單範例:

{\rtf1\ansi\ansicpg950 {\fonttbl ...} ...}

RTF檔的內容,主要由三個部份所組成,一是命令,也就是以\開頭的字;一是群組,也就是{}括起來的部份;最後一個當然就是資料。RTF命令的格式如下:

\<keyword><number><delimitor>

keyword必須都是英文字母(RTF檔是大小寫有關的),或是單一特殊字元。number可有可無,當有的時候,便做為命令的參數。這個數字可能是負的(以‘-’做開頭)‘而且RTF裡的數字一律為2 byte的短整數。delimitor可以是空白,或者任何一個非英文字母的字元,若是空白便必須將它吃掉,不視為資料處理。

RTF的命令,主要可分為下列三種:

(1) 資料屬性定義命令:用來定義資料的相關屬性,例如語系、字型、版面等等
(2) 資料意義命令:用來說明資料實際的意義,例如內文、註腳、頁眉、字型表等等
(3) 特殊字元命令:用來輸入一些特殊字元

除了上述三種命令之外,還有一個特別的命令\*,這個命令是表示如果後面緊接著的命令不懂的話,可以將其後的資料全數略過。這個命令主要是提供給應用程式,以便植入一些自己定義的命令與資料。因此遇到\*命令時,必須再讀取下個命令,才能決定是要處理,還是要全數略過捨棄。如果命令是在一個群組之中(即在{}之內),則該命令只作用在該群組裡其後的資料(包括下層群組),當離開群組後,資料的屬性必須回復到外層群組的屬性。以下便開始說明這三種RTF的命令(只列出與取文字有關的):

(1) 資料屬性定義命令

\rtf:RTF的檔頭標記,後面的數字為RTF的版本(目前都是1)
\ansi:使用ANSI字集
\mac:使用Apple Macintosh字集(目前不支援,視為錯誤)
\pc:使用IBM PC code page 437字集(目前不支援,視為錯誤)
\pca:使用IBM PC code page 850字集(目前不支援,視為錯誤)
\ansicpg:使用的語系(實際語系需視字型語系而定),後面的數字可為:
932 日文
936 簡體中文
949 韓文
950 繁體中文
\langfenp:使用的字型語系,1028 = 繁體中文,2052 = 簡體中文
\ud:資料採用Unicode編碼(\u命令)
\upr:後面接兩個群組,第一個群組是ANSI編碼,第二個群組是Unicode編碼,可任挑一個做為資料處理(這是為了與無法處理Unicode的RTF Reader相容所設,若能處理Unicode,應以Unicode為準)

(2) 資料意義命令

\info:摘要資訊
\title:標題
\author:作者
\subject:主旨
\manager:主管
\company:公司
\doccomm:文件註解
\comment:註解(無作用,可省略的註解)
\category:類別
\keywords:關鍵字
\userprops:使用者自訂屬性(Word自己的定義)
\propname:使用者自訂屬性的屬性名稱(Word自己的定義)
\staticval:使用者自訂屬性的屬性值(Word自己的定義)
\header:頁眉
\footer:頁腳
\footnote:註腳
\fonttbl:字型表
\colortbl:顏色表
\stylesheet:樣式表
\pict:圖片
\shptxt:方塊文字資料(Word自己的定義)
\shpinst:方塊文字開始(Word自己的定義)

未遇到上述命令前的資料,一律為內文。
(3) 特殊字元命令

\{:'{'字元
\}:'}'字元
\-:'-'字元
\_:'-'字元
\~:空白字元
\':16進位字元,後面接兩個16進位數字,例如\'A4表示A4h字元
\u:Unicode字元,後面的數字即為Unicode字碼,如果命令結束後緊接著一個'?',要將之吃掉
\emspace:空白字元
\enspace:空白字元
\qmspace:空白字元
\emdash:'-'字元
\endash:'-'字元
\lquote:單引號
\rquote:單引號
\ldblquote:雙引號
\rdblquote:雙引號
\tab:TAB字元
\trowd:表格開始
\row:表格行結束
\nestrow:表格行結束
\cell:表格欄結束
\nestcell:表格欄結束
\column:column break
\page:page break
\line:line break
\par:段落結束
\sect:段落/區段結束

因此要抽取RTF文字資料,只需一層一層群組地剖析下去,當遇到資料時,看看資料是什麼意義,是什麼樣屬性,即可抽取出來。但注意在處理資料時,若是遇到換行字元(ASCII 13,ASCII 10),必須將之略去不處理。

7. Word檔剖析時的一些注意事項

(1) 有些RTF或文字檔,檔尾有補一個ASCII 0,因此在辨識是否文字檔時,最後一個byte不應檢查
(2) 有些.doc檔,其實是gif/zip/pdf/html等檔案rename成的,像gif/zip,因有ASCII 0可以簡單辨識,但pdf/html為純文字檔,必須特別處理。html可加以辨識後,轉交html剖析物件處理,pdf則會形成許多看似中文的亂碼,因此必須特別檢查是否合法中文值域,以便略過這些檔案。
(3) 有些.doc檔,裡面會有西歐字元,會造成與中文值域相衝。因其他資料都是好的,只須將西歐字元瀘除即可。但這部份與前一注意事項會衝突,目前的做法是,如果有連續兩個中文值域相衝的碼,整個檔案才不處理,否則便僅瀘除該不合法中文字。
(4) 有些損毀的word檔,前頭為純文字,後面為亂碼,因此除了檔頭外,也必須檢查整個檔案是否有出現ASCII 0,才能得知是否為合法純文字資料。但我碰過一個檔案,裡面恰好有一個ASCII 0字元,其餘部份都是正常的。由於word會瀘除ASCII 0字元(顯示時是"口"),因此這部份的檢驗必須加以調整。目前的條件是,連續出現三個ASCII 0,或是累積達到10個時,整個檔案才不處理。

由於word並不管檔案中是否有亂碼,均全數載入,但編碼方式由使用者決定,因此有時載入後完全都是亂碼,或是局部有一些亂碼,其餘都是正常的。然而在建索引時,這些亂碼資料其實是亳無意義的,因此才會加入這些辨識的部份,避免亂碼資料進入索引資料中。但為避免全部資料只有一些些亂碼,導致整個資料被瀘除的狀況,才會有這種似要瀘除,卻又有條件允許的奇怪條件(確實挺麻煩的)。不過基本上,只要是合法的word檔,幾乎都能正確解出,上面的許多情況大都是一些人為做出的特例。

8. PowerPoint 97的格式

PowerPoint檔主要有三個資料串,"Current User"記錄了最近一次開檔修改的使用者相關資訊,"Pictures"記錄了所有的圖片視訊資料,這兩個資料串均不必處理。主要的內容則記錄在"PowerPoint Document"這個資料串裡,因此底下便只剖析這個資料串。
 
"PowerPoint Document"資料串裡的資料,係以record為單位組成的,而這些record可區分為container和atom兩種。這兩種record的作用,有點像是目錄/檔案結構,atom是實際記錄資料/屬性的地方, container則是用來將record做成樹狀結構。以下便是每個record的格式:

2 byte:最低4 bit為版本,最高12 bit為record次形態值
2 byte:record的形態值
4 byte:record內容的長度
? byte:reocrd內容

因此我們所要做的,便是從資料串開始處,一個一個record讀入,辨識找出所需的record(包括下層的record),然後抽取文字出來。至於需要那些record,主要由record的形態值來加以辨識。以下便是需要的record(後面的數字便是它的record形態值):

Document (1000,container):內含PowerPoint主要的文件內容,以EndDocument做為結束
EndDocument (1002,atom):Document內容結束標記
SlideListWithText (4080,container):內含各Slide的文字資料(只有title和body文字資料)
SlidePersistAtom (1011,atom):記錄Slide相關資訊(可用以區分不同Slide)
MainMaster (1016,container):記錄母片相關資訊
Notes (1008,container):記錄備忘稿相關資訊
Slide (1006,container):內含一個Slide的資料(title/body以外的文字資料放在此處)
SlideAtom (1007,atom):用以得知該Slide是否有備忘稿
HeadersFooters (4057,container):內含頁首頁尾相關資訊
PPDrawing (1036,container):Office藝術家資料(文字方塊置於此處)
TextHeaderAtom (3999,atom):用以表示其後的文字atom的意義(可用以區分文字是否分隔成不同區塊)
TextBytesAtom (4008,atom):Unicode均<256的文字內容
TextCharsAtom (4000,atom):Unicode文字內容
CString (4026,atom):Unicode文字內容
ExOleObjStg (4113,atom):儲存embeded object實際內容(以後再說明)
Unknown (0,atom):不明形態(可用以辨別是否結束)
 
我們先看Document的結構形態(以下的內容都已經將不必要的record瀘除):

Document
   SlideListWithText
     SlidePersistAtom -- 第1個slide
     TextHeaderAtom
     <text>
     TextHeaderAtom
     <text>
     SlidePersistAtom -- 第2個slide
     SlidePersistAtom -- 第3個slide
     TextHeaderAtom
     TextHeaderAtom
     <text>
     SlidePersistAtom -- 第4個slide
     TextHeaderAtom
     TextHeaderAtom
   EndDocument

由於Document必是第一個record,因此如果要抓取title/body的文字,只要找到SlideListWithText,然後依次往下抓,便可以取到所有的title/body文字(還要考慮SlideListWithText的次型態值,這點後面再說明)。然而每個Slide並不一定只有title/body,裡面可能有許多的表格或文字方塊,這些資料並不放在Document中。此外,PowerPoint的資料還包括母片與備忘稿等,因此我們再看一下整個檔案的結構形態:

Document
   <...>
   EndDocument
MainMaster
   PPDrawing
Slide -------------- 第1個slide
   HeadersFooters
     <text>
   SlideAtom
   PPDrawing
Slide -------------- 第2個slide
   HeadersFooters
     <text>
   SlideAtom
   PPDrawing
...
Notes
   PPDrawing
Notes
   PPDrawing
...
Unknown

母片的資料便放在MainMaster中,每一個Slide的頁首頁尾與其他文字,則放在Slide裡,至於備忘稿,便放在Notes裡。Slide的數目,通常會和SlideListWithText相同,但也會有一些如標題Slide會放於此處,分辨方法便是檢查SlideAtom offset 0的長整數(4 byte), 如果是2,表示是title master slide,屬於母片的slide,否則便是正常的Slide。至於Notes的數目並不一定,應配屬於那個Slide,我們後面再說明。

這邊需要注意的是,所有非title/body的文字,都是儲存在PPDrawing這個container裡,但其格式依文件是Office藝術家的格式,其結構並未說明。但據我測試結果,和PowerPoint的record結構沒什麼兩樣,其文字資料是放在:

PPDrawing
  -4094 (containter)
   -4093 (containter)
    -4092 (containter)
     -4083 (containter)
     <text>

至於這些container是什麼意思,便不去管那麼多了(抽得到文字便可以了)。但在處理PPDrawing時,有一些事項是必須加以注意的:

(1) -4092 container裡可能再含有-4093 container,對於這種多層次樹狀的文字方塊,不必往下層展開,只需展開第一層取文字即可(否則取到的文字並沒有顯示在PowerPoint上,用搜尋也找不到)。
(2) PowerPoint的群組物件闗係,是以-4093這個record type來組成的。也就是說,-4093裡還可能有-4093,形成層次式的群組結構關係,因此這裡的-4093 container是必須往下層展開的。

另外,母片中的title/body文字資料沒有意義,不必加以抽取,因此必須經由判斷TextHeaderAtom來判斷。TextHeaderAtom offset 0的長整數(4 byte),表示後面文字的意義:

0 = Title
1 = Body
2 = Notes
3 = Not Used
4 = Other (Text in a shape)
5 = Center body (subtitle in title slide)
6 = Center title (title in title slide)
7 = Half body (body in two-column slide)
8 = Quarter body (body in four-body slide)

因此除了4以外的情況都不必處理。
現在說明一下備忘錄Notes record的Slide配屬問題。在Notes container裡都會有一個NotesAtom (type=1009),其offset 0的長整數,記錄的便是所屬的Slide ID,而這個Slide ID則記錄在SlideListWithText container裡的SlidePersistAtom offset 12的長整數(注意PowerPoint裡面有非常多的Slide ID,各有不同的意義)。不過一個Document container裡,可能有多個SlideListWithText container,這時必須檢查其次型態值,0表示notes slide,1表示master slide,2表示body slide。也就是說,我們要抓title/body文字時,應該到subtype=2的SlideListWithText container抓,如果是要取備忘稿(notes)的Slide ID,則必須到subtype=0的SlideListWithText container取。不同的subtype,取到的Slide ID是不一樣的。因此在剖析Document container時,我們必須先取到各個Slide對應於備忘稿的Slide ID,當遇到Notes container時,再抓取NotesAtom裡面的id進行搜尋,便能知道是屬於那個slide了。然而有些Slide是沒有備忘錄的,因此也必須判斷Slide裡面的SlideAtom,若offset 16的長整數(4 byte)若為0,表示無備忘稿,否則便是有,此時Notes的Slide ID才有意義。

PowerPoint檔的另一種較複雜結構(有點像是Word檔快速存檔的complex格式),會形成多個Document/Slide:

Document
Slide*
Notes*
Document
Slide*
Notes*
Document
Slide*
Notes*
...

第一個Document/Slide結構存的是最舊版本的資料,其後的結構為修改過程中新增/修改的部份。由於Document中主要是取title/body文字,且均一次存足,因此取時只需以最後一個為準。但Slide便不同了,必須判斷Slide/PPDrawing是否相同,才能決定要異動到那個Slide。然而我檢驗過Document和Slide各atom,並沒有相關的資訊,猜測應該是含在PPDrawing這個container內。而PPDrawing是Office Art File Format,找遍MSDN與微軟網站,甚至用Google都找不到相關文件,只好自己解解看。首先將PPDrawing展開,發現它只含一個"-4094"的container,繼續對這個container展開,得到:

type=-4094,size=1778
   type=-4088,size=8
   type=-4093,size=1674
     type=-4092,size=196
     type=-4092,size=272
     ...
   type=-4092,size=72

由於描述container屬性的,大都是第一個,因此觀察"-4088"這個atom裡面的值(8 byte剛好2個長整數), 可得知,第一個長整數表示"-4093"這個container裡有幾個"-4092"的container,第二個長整數似乎是某種ID,規律性目前不明。於是展開裡面的"-4092" container,得到:
 
type=-4092,size=196
   type=-4086,size=8
   type=-4085,size=48
   type=-4080,size=8
   type=-4083,size=100
     <text>

由於<text>裡的文字,恰好是每個文字方塊的文字,因此"-4092" container,應該是用來儲存每個文字方塊的container,而"-4086"則應該是這個文字方塊的相關屬性。觀察裡面的值(包括比對異動的PPDrawing資料),發現第一個長整數便是這個文字方塊的ID。再回頭檢驗"-4088" atom,發現第二個長整數就是最大的文字方塊ID,而-4093裡第一個-4092裡的-4086 atom記錄的文字方塊ID,永遠是最小的。因此規則已經很明顯:

(1) 當產生一個PPDrawing container時,-4093裡會先產生一個虛擬的文字方塊,裡面的-4086則記錄整組文字方塊的起始ID。
(2) -4088裡會記錄文字方塊數,以及最後一個文字方塊ID(可能不存在被刪除了)。如果新增一個文字方塊,則其ID便是該ID+1。

也就是說,每個PPDrawing裡的文字方塊ID,必定介於第一個-4092裡-4086 atom的文字方塊ID,與-4088 atom所記錄的文字方塊ID之間。由於第一個-4092裡-4086 atom的文字方塊ID是起始ID,且不會變動,而Slide裡只會有一個PPDrawing container,因此這個ID便可以做為Slide的標誌,當ID相同時,便可直接將整個非title/body文字替換掉即可。

至於備忘錄的異動問題,由於後者應覆蓋前者,因此在決定備忘稿對應的Slide的同時,應將舊資料清除後再重抓備忘稿資料。另外還要考慮備忘稿刪除的情況,也就是遇到異動的Slide container時,仍要檢查SlideAtom offset 16的長整數值,若是0(沒有備忘稿),便要將舊資料清除掉。

9. PowerPoint 95的格式

PowerPoint 95的檔案格式,基本上和97相同,一樣是container/atom的record結構,所不同的是,record header為16 byte,成為下列形式:

4 byte:record的形態值
4 byte:版本
4 byte:record內容的長度
4 byte:次形態值
? byte:reocrd內容

container/atom的結構組織也不同,整個資料是一個type=3的container,裡面再包一個Document container,所有的資料,包括title/body、文字方塊、母片等等,都是放在這個Document container裡。Document container裡有幾個List container(type=2000),必須藉由次形態值來辨識是那種串列,以便加以剖析讀取相關資料。以下便是我們所需要的List container(由於PowerPoint 95的許多形態值的意義和97並不相同,因此以下便直接以形態值代表container與atom):
 
(1) List container, subtype=10

裡面的Slide container便存放了各Slide的資料。Slide的結構如下:

Slide
   1008
     3000
     3010 (備忘稿資料)
   HeadersFooters (頁首頁尾資料)
   3000
     3010 (title/body資料)
     3010 (title/body資料)
     3008 (文字方塊資料)
     3008 (文字方塊資料)
     3008 (文字方塊資料)
     ...

注意後面3010 container裡的文字,可能是title,也可能是body,次序並不一定。檢驗的方法,便是取3010裡的3011 atom,offset 4的長整數若為0表示title,若為1表示body。至於實際文字資料放得很深,其位置便是3010或3008 container裡面的4001 container裡面的4002 container裡面的4064 container裡面的2003 atom。2003 atom是放文字的atom,其格式是ANSI字串,和97不同(95還沒支援Unicode)。 至於頁首頁尾的資料,則是在HeadersFooters container(type=4057)裡的CString atom(type=4026)。 要注意這個CString atom存的不是Unicode,而是ANSI字串。

(2) List container, subtype=11

裡面的MainMaster container儲放的是母片資料,其結構和Slide幾乎完全一模一樣。注意取時,應略過title/body資料。

10. PowerPoint 4.0的格式

PowerPoint 4.0也是Ole物件檔,但它的資料串名稱不是"PowerPoint Document",而是"PP40"。這個資料串裡的資料內容,和PowerPoint 95以後的格式完全不同,並非採用record的形式。由於目前找不到任何相關的文件,因此解析起來頗為困難。以下是我解析的檔頭結構(共計84h byte):

4 byte:固定為ED DE AD 0B
8 byte:不明
2 byte:1748h開始的資料數 (假設是n)
2 byte:不明
4 byte:整體資料長度
? byte:不明

從84h開始,應該是字型相關的資訊,長度固定。從1748h開始,應該是字型參用的相關資訊,但長度不定,實際長度為檔頭0Ch的短整數*8。也就是說,實際資料是從1748h+n*8的地方開始。實際資料裡的格式,是一個Slide接著一個Slide,依序置放,然而各Slide裡的資料格式與意義頗為難解,同時也不想花太多時間在這地方,因此目前只抓出文字區塊的規律如下:

00 FF FF 64 00 00 00 00 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 00 01 00 size 00 00 content

其中??表示可為任何數字,size為2 byte的短整數,content的字串格式為ANSI。也就是說,我們只需針對整個檔案資料逐一比對,找出符合上述位元組資料順序者,便可以找到各slide的文字資料。雖然簡單,但也有一些副作用,那就是一些隱藏在PowerPoint裡的文字都會被抓出來,例如"Click here to edit master slide"等字串。由於我們略掉了很多關於文字塊屬性作用的資料,因此並無法判斷這個文字塊的文字資料是否真的要顯示,目前也只能暫時都將之抓取出來建索引。

11. Excel 97/95的格式

Excel 97的文件內容,主要是存放在"Workbook"的資料串裡面(Excel 95則是"Book"資料串,其內容和97完全相同),因此底下便針對這個資料串加以剖析說明。

"Workbook"這個資料串,主要是由record所組成,但它和PowerPoint不同的是,整個record的結構為一扁平結構,而非樹狀結構。record的形式如下:

2 byte:形態值
2 byte:record內容長度
? byte:record內容

至於record的組織結構,主要以BOF(type=0809h)與EOF(type=000Ah)做為分隔,也就是:

BOF
<公用資料>
EOF
BOF
<Sheet資料>
EOF
BOF
<Sheet資料>
EOF
...

當EOF之後不是緊接著BOF時,表示整個檔案結束。在公用資料區裡,比較重要的有下列幾個record:

(1) type = 85h (BOUNDSHEET: Sheet Information) - 記錄了各Sheet的相關資料,其格式為:

4 byte:該Sheet資料的起始位置(相對於資料串,位置一開始必是BOF record)
1 byte:Sheet形態, 其值如下:
0 = worksheet or dialog sheet
1 = Microsoft Excel 4.0 macro sheet
2 = chart
6 = Visual Basic module
1 byte:Sheet旗標, 其值如下:
bit 0-1:Hidden狀態, 0=可見, 1=隱藏, 2=絕對隱藏(只能用VBA方式清除)
bit 2-7:保留
2 byte:Sheet名稱的長度(若是"Book"資料串,則只有1 byte)
? byte:Sheet名稱

(2) type = FCh (SST: Shared String Table) - 記錄了各Sheet使用的字串,其格式為:

4 byte:Shared String Table與Extended String Table的字串總數
4 byte:Shared String Table的字串總數
? byte:字串陣列

必須注意的是,Excel的字串格式頗特別,記錄方式如下:

2 byte:字串的字數
1 byte:字串資料旗標
bit 0 (01h):0表示字串資料的Unicode高位元組都是0,只存低位元組的資料
bit 2 (04h):1表示有Extended String(記錄語系相關資訊)
bit 3 (08h):1表示有Rich String(記錄字型/字色等資訊)
2 byte:formatting runs的數目(有Rich String時才有這個資料,實際資料大小=數目*4 byte)
4 byte:Extended String的資料大小(有Extended String時才有這個資料)
? byte:字串資料
? byte:formatting runs資料
? byte:Extended String資料

其中我們需要的,只有字串資料而已,其他的資料可以不用管它。

也就是說,在取得Excel資料前,我們必須剖析完公用資料區,得知有那些Sheet要處理,以及整個共享字串表,然後才能處理Sheet資料。不過共享資料表的資料可能很多,而Excel每個record的大小有一定的限制,這時共享資料表的資料會被切成數個record,也就是在SST record後面會接了許多的Continue record(type=3Ch),而Continue record裡的資料便是SST被切出來的資料。因此在讀取SST時,必須考慮此一情形。由於切開的點可能在任何位置,在處理時頗為麻煩。也許有人認為,只需將所有資料全部讀取接合後,再一併處理即可,但這是錯的。當切開的點是字串資料時,Excel會在Continue record的第一個資料裡多放一個字串資料旗標,以便將剩餘的字串資料做做佳的編碼方式(亦即換個record後,同一字串的編碼方式可能改變了),因此不能全部接合後再處理。總之,這個部份需要特別處理才行。

另外,如果公用資料區裡遇到FILEPASS record(type=2Fh),表示其後的record內容是加密過的,必須經過解密才能讀取。由於不知Excel的加密方法,因此目前並不處理(活頁簿保護時會產生此一現象)。由於Excel檔仍可開啟且不必輸入密碼,因此容易讓使用者誤以為我們沒將它解出來,但目前也沒有什麼好方法可處理。

要取得各Sheet的文字資料,有兩種方法,一種是將record全部掃過一遍(較慢),一種是利用INDEX/DBCELL/ROW的結構快速取得。然而Excel是允許檔案中無INDEX/DBCELL/ROW結構,為了避免出問題,我們還是採用全部掃過一遍的方式處理。對於一個Sheet資料而言,Cell的內容是記錄在下面record中的:

(1) type = FDh (LABELSST: String Constant/SST) - 記錄文字串(在共享資料表裡)的Cell,格式為:

2 byte:第幾行(由0編起)
2 byte:第幾列(由0編起)
2 byte:XF record索引值
4 byte:共享字串表的索引值

(2) type = 204h (LABEL: String Constant) - 記錄文字串的Cell(舊版),格式為:

2 byte:第幾行(由0編起)
2 byte:第幾列(由0編起)
2 byte:XF record索引值
2 byte:字串byte數
? byte:ANSI字串

(3) type = D6h (RSTRING: Character Formatting) - 記錄文字串的Cell(舊版),格式為:

2 byte:第幾行(由0編起)
2 byte:第幾列(由0編起)
2 byte:XF record索引值
2 byte:字串byte數
? byte:ANSI字串
? byte:formatting資料

(4) type = 27Eh (RK: RK Number) - 記錄數字的Cell,格式為:

2 byte:第幾行(由0編起)
2 byte:第幾列(由0編起)
2 byte:XF record索引值
4 byte:RK數值(後述)
 
(5) type = BDh (MULRK: Multiple RK Cells) - 記錄數字(多個連續列)的Cell,格式為:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
? byte:RK Cell資訊(每個6 byte)
2 byte:XF record索引值
4 byte:RK數值(後述)
2 byte:結束列(由0編起)

(6) type = 203h (NUMBER: Floating-Point Number) - 記錄浮點數的Cell,格式為:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
2 byte:XF record索引值
8 byte:IIIE 8 byte浮點數(後述)
 
(7) type = 6h (FORMULA: Cell Formula) – 記錄公式的Cell,格式為:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
2 byte:XF record索引值
8 byte:IIIE 8 byte浮點數,若公式有誤,或公式計算結果是字串/布林值時,最高2 byte為FFFFh。字串結果會記錄在緊跟著的STRING record裡。
2 byte:旗標值 (略)
? byte:計算公式字串資料

(8) type = 207h (STRING: String Value of a Formula) - 記錄公式結果(字串),格式為:

? byte:字串資料(參見前面的字串格式說明)

以下開始解釋幾個Excel數值的編碼方式:
 
(1) RK數值格式

最低的2 bit做為數值形態,表示最高的30 bit數值意義:

0 = IEEE浮點數(後述)
1 = IEEE浮點數*100(也就是解出來的值要再除以100)
2 = 整數
3 = 整數*100

(2) IEEE浮點數(30 bit)

最高bit用以代表負數,接下來的11 bit為exponent,最後的18 bit為mantissa。exponent由3FFh開始表示0次方(400h=1次方、3FEh=-1次方)。mantissa注意只記錄小數部份(整數部份必須為1)。唯一例外的是,整個所有bit若是0,則表示數字0。不懂的話,請去看計算機概論裡相關的說明。

(3) IEEE浮點數(8 byte,64 bit)

和30 bit格式相同,只是mantissa由18 bit擴增至52 bit而已(精準度增加)。

至於XF record索引值,係指向該Cell的format record(type=E0h,較舊的版本type=43h),這些record是放在公用資料區裡。如果索引值<=49,表示係Excel的內定顯示格式。由於顯示格式眾多(50個內定格式+其他user自定格式),要一一處理起來很麻煩,而Excel裡的數字資料裡通常不會被用來檢索,因此目前數字形態的資料,便不加以處理,直接略過(若確實有需要再說)。

最後要說明的是Cell裡面的註解,它是放在Sheet資料的TXO record(Text Object,type=1B6h),每個註解一個TXO record。其格式如下:

2 byte:旗標
2 byte:文字走向
6 byte:保留
2 byte:文字字數
2 byte:formatting runs數目
4 byte:保留

實際文字是記錄在緊跟著TXO record的第一個Continue record,裡面的字串資料不含文字字數(已記錄在TXO record中)。

12. BIFF4的格式

前面提到的Excel 97/95檔案格式(也稱為BIFF格式),可適用於下列版本:
 
BIFF5 - Microsoft Excel version 5
BIFF7 - Microsoft Excel 95(亦稱為Microsoft Excel version 7)
BIFF8 - Microsoft Excel 97

然而BIFF4以前的版本並非Ole檔,而格式也稍有不同,必須特別另行處理。record的形式如下:

1 byte:形態值
1 byte:次形態值
2 byte:record內容長度
? byte:record內容

由於BIFF4只有一個Sheet,因此沒有BOF/EOF的結構,也沒有公用資料區,其第一個record固定是type=9,然後一直剖析到檔案結束為止。以下是幾個需要處理的record:

(1) type = 4,存放字串資料,格式為:

次形態值=0時:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
3 byte:不明
1 byte:字串byte數
? byte:ANSI字串

次形態值<>0時:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
2 byte:不明
2 byte:字串byte數
? byte:ANSI字串

(2) type = 2,存放短正整數資料,格式為:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
3 byte:不明
2 byte:短正整數

(3) type = 3,存放浮點數資料,格式為:

2 byte:第幾行(由0編起)
2 byte:起始列(由0編起)
3 byte:不明
8 byte:IEEE 8 byte浮點數

至於數字的顯示格式,目前不處理。

13. Embeded Object格式
13. Embeded Object格式

Embeded Object的格式,會隨著Embeded所在的軟體,以及要Embeded進去的物件的不同,而有許多差異。首先我們先來看看在Notes文件的Embeded Object格式(只看重要資料串的目錄結構即可):

(1) 在Notes裡Embeded一個文字檔或zip檔

<Root Entry>
    <>
     <\001Ole10Native>

原始的檔案資料便是放在"\001Ole10Native"這個資料串裡(第一個byte是ASCII 01字元),裡面的資料格式為:

整個資料串大小(long)+2 byte+文件顯示名稱+文件處理程式名稱+4 byte+全路徑檔名長度+全路徑檔名+檔案大小(long)+檔案內容

這裡的名稱和檔名均採用ASCIIZ格式,也就是以ASCII 00字元為結束。由此可簡單抽出原來的Embeded檔案內容。然而有些非檔案型的資料也是放在"\001Ole10Native"資料串裡,因此在抽取時,必須要判斷檔名是否合法,若是非法表示它Embeded的不是一個檔案。

(2) 在Notes裡Embeded一個word檔

<Root Entry>
    <Word.Document.8>
     <ObjectPool>
     <WordDocument>

我們再看一下Word檔的資料串目錄結構:

<Root Entry>
    <WordDocument>
    <ObjectPool>

也就是說,Word檔被Embeded之後,整個目錄的資料串都被往下移一層了(以目錄/檔案結構的眼光來看,這是理所當然的事)。

綜合上述,在非Ole物件軟體裡的Embeded Object,其內容為:

<Root Entry>
    <物件辨識名稱>
     物件資料

以純檔案而言,物件辨識名稱為空字串,以Office檔案而言,辨識名稱便是Word.Document.?、 PowerPoint.Show.?、Excel.Sheet.?,其中?是一個數字,表示該Office物件資料的版本(註:由於Office的Ole物件名稱有很多種,例如Word.Picture.?,因此最好只檢查前面的部份字串)。要解這類的Embeded Object,只需辨識主目錄名稱,再決定如何處理即可。其中必須注意的是PDF檔,若以一般檔案插入時(也就是封裝式的物件),其格式和文字檔或Zip檔是相同的,但若是以新建物件方式插入時(必須有PDF Writer時才有此一功能),則Embeded的結果會變成和Office類似的形式:

<Root Entry>
<AcroExch.Document>
    <Contents>

此時Contents裡記錄的便是PDF的原始資料。注意其他Ole物件也可能使用Contents資料串,因此如果沒有物件辨識名稱,而只以Contens資料串來辨識是否為PDF檔時,必須再檢查檔頭是否為”%PDF-“才行。
接著再來看一下Word Embeded Object後的資料串結構:

(1) Embeded一個純文字檔

<Root Entry>
    <WordDocument>
    <ObjectPool>
     <_1132414719>
     <\001Ole10Native>

(2) Embeded一個word檔

<Root Entry>
    <WordDocument>
    <ObjectPool>
     <_1132414639>
     <ObjectPool>
     <WordDocument>

由此即可看出,Word的Embeded Object都是放在ObjectPool這個目錄底下,同時各Object均還賦予一個特殊編號的子目錄,實際資料就放在該子目錄裡,其格式和之前提的Notes Embeded Object格式沒什麼兩樣。知道它的結構後,要將資料解出來就簡單了。以下則是Excel的狀況:

(1) Embeded一個純文字檔

<Root Entry>
    <MBD04419A08>
     <\001Ole10Native>
    <Workbook>

(2) Embeded一個word檔

<Root Entry>
    <MBD044182E4>
     <ObjectPool>
     <WordDocument>
    <Workbook>

亦即每個物件都是放在與"Workbook"資料串同一層,其子目錄均以MBD做為開頭。

至於PowerPoint的Embeded Object比較麻煩,實際的物件資料並不是放在IStorage的目錄檔案結構裡,而是自行放在"PowerPoint Document"資料串裡的ExOleObjStg record中(type=4113),其內容為:

4 byte:OleID(沒有用處)
? byte:壓縮的Ole物件資料

注意在ExOleObjStg裡的物件資料是有壓縮的,其壓縮法便是ZIP檔中的LZW壓縮,因此可利用zlib裡的inflate函數來解壓。解壓後的資料,便是原來的Office檔(若是文字檔等,則是Ole的封裝格式,把它想成是Notes Embeded Object拿掉第二層目錄就對了)。由於PowerPoint的Embeded Object是放在文件中,自然也需要考慮到Ole物件的異動狀況並加以處理。最簡單的方法,便是從Document record裡的ExObjList container(type=1033)中找出各物件資料的所在位置,並加以讀出處理即可。各物件資料的所在位置,是放在ExEmbed container(type=4044)裡的ExOleObjAtom中(type=4035)的第5個長整數(offset=16)。不過這個數值記錄的是一個索引值,必須參用到PersistPtr才能得到Ole物件資料實際所在的ExOleObjStg atom位置。PersistPtr是放在PersistPtrIncrementalBlock atom裡(type=6002),其內容為:

位置數目與起始編號:長整數,最高12 bit為數目,最低20 bit為起始編號(即PersisPtr的索引值,由0編起)
檔案位置:均為長整數,讀取後置入PersisPtr中
位置數目與起始編號
檔案位置

由於PersistPtrIncrementalBlock atom會隨著編輯的過程而累增,因此必須讀取整個資料串中所有的PersistPtrIncrementalBlock atom並加以整合,才能得到完整的PersistPtr。綜合上述,要取得PowerPoint檔裡的Ole物件,其程序為:

(1) 處理所有PersistPtrIncrementalBlock atom,得到PersisPtr值
(2) 尋找現行Document container裡的ExObjList container,由其內的ExEmbed container裡的ExOleObjAtom得到各Ole物件資料的PersisPtr索引值,再參照PersisPtr得到實際所在的位置
(3) 到Ole物件資料所在的位置,取得ExOleObjStg atom,將裡面的物件資料解壓,便是實際的Ole物件內容了

最後再來看一下RTF的Embeded Object格式。當Word資料存成RTF檔時,其中的Embeded Object會存在\object命令裡,實際資料則在\objdata命令裡。\objdata後面會接著一串16進位數字字串,將這些數字字串還原成Binary的形式,再解析其格式如下:

8 byte:不明
4 byte:物件辨識名稱長度
? byte:物件辨識名稱,ANSI字串,純檔案為Package,Office檔仍為Word.Document.?之類的名稱
8 byte:不明
? byte:實際物件資料

實際物件資料,若是純檔案(如文字檔、zip檔,則為"\001Ole10Native"資料串的資料,否則為下列格式:

4 byte:物件資料byte數
? byte:Ole檔資料(將它存檔起來就是完整的Office檔案)




posted @ 2006-09-06 11:29  Max Woods  阅读(2536)  评论(1编辑  收藏  举报