Office文件的奥秘——.NET平台下不借助Office完成Word、Powerpoint等公事的辨析(二)

【题外话】

上篇文章很雅观被NPOI的大神回复了,同时也修正了自作者二个题材,就是NPOI其实是有doc文件的分析,只不过一直未曾跟随正式版发布过,要取得那有的代码,可以运动CodePlex(http://npoi.codeplex.com/),访问在SourceCode中的NPOI.ScratchPad中即可看出。给大家造成的难堪在此表示抱歉。

 

【序列索引】 

  1. Office文件的精深——.NET平台下不借助Office完成Word、Powerpoint等公事的辨析(一) 得到Office二进制文档的DocumentSummaryInformation以及SummaryInformation
  2. Office文件的精深——.NET平台下不借助Office达成Word、Powerpoint等公事的分析(二) 得到Word二进制文档(.doc)的文字内容(包含正文、页眉、页脚、批注等等)
  3. Office文件的深邃——.NET平台下不借助Office达成Word、Powerpoint等公事的分析(三)
    详尽介绍Office二进制文档中的存储结构,以及得到PowerPoint二进制文档(.ppt)的文字内容
  4. Office文件的深邃——.NET平台下不借助Office落成Word、Powerpoint等文件的分析(完)
    介绍Office Open
    XML文档(.docx、.pptx)怎么样进展分析以及解析Office文件常见开源类库

 

【小说索引】

  1. WordDocument和FIB
  2. Table Stream中的Piece Table
  3. 业内拿到文字内容
  4. 有关链接

 

【一、WordDocument和FIB】

我们跟着第贰篇的代码继续,不知大家有没有翻动过Directory获取到的情节,比如上次的文档摘要SummaryInformation和DocumentSummaryInformation,除此之外还有越发储存文档内容的DirectoryEntry,比如Word的为“WordDocument”和“1Table”,PowerPoint的为“PowerPoint
Document”,Excel的为“Workbook”。

我们先从WordDocument说起。不知大家发现了并未,其实无论是哪位Word文件,WordDocument那些DirectoryEntry的SectorID总是0,相当于说,WordDocument其实就是Header之后的第一个Sector。对于WordDocument,其最根本的应当是里面富含的FIB(File
Information
Block)了,FIB位于WordDocument的始发,其涵盖着Word文件尤其主要的参数,诸如文件的加密方法、文字的编码等等。

对此二个FIB,官方文档中就是可变长的,其中FIB中最开端的为稳定32字节长的FibBase:

  1. 从000H到001H的2字节UInt16,是稳定为0xA5EC,声明文档为Word二进制文件。
  2. 从002H到003H的2字节UInt16,是Word格式的版本(nFib),但其实那里一般为0xC1,即Word97的格式,真实的版本在此后会并发。
  3. 从00AH到00BH的2字节UInt16,其实那几个UInt16其实被分为了13部分,除了第⑥有些占了4bit外,其余12有的各站1bit,总结16bit,大家可以通过位运算分别读取每一bit的值,比如Boolean
    isDot = ((n & 0x1) ==
    1),就能够读取最低位是或不是为真了。插张图来表明下13有些是哪些分配的,最左为UInt16的最低位。
    图片 1

    • A(第0位),为文档是或不是是.Dot文件(Word模板文件)
    • B(第③人),没驾驭这几个人存的是哪些。
    • C(第一位),为文档是或不是是复杂格式(火速保存时生成的格式)。
    • D(第二位),为文档是不是带有图表。
    • E(第④-5位),当nFib小于0x00D9时为高速保存(Quick
      Save)的次数,当大于0x00D9时一贯为0x0F。
    • F(第捌个人),为文档是还是不是加密。
    • G(第12人),为1时文字存储于1Table,为0时文字存储于0Table。
    • H(第7位),为是不是“提议以只读格局打开文档”(保存时采纳“工具”->“常规选项”可以设置该属性)。
    • I(第九四位),为是或不是有写爱慕密码。
    • J(第7位),为固定值1。
    • K(第贰几个人),为是不是要用应用程序的言语暗中认同值覆盖段落格式中定义的语言和字体。
    • L(第柒几人),为文档语言是不是为南亚语言。
    • M(第②5位),当文档加密时,文档即使应用XOLacrosse混淆则为1,否则为0;文档不加密时马虎该属性。
  4. 从00CH到00DH的2字节UInt16,为定位的0x00BF或0x00C1(有个别语言的Word97会为0x00C1)
  5. 从00EH到011H的4字节UInt32,当文档加密并且混淆,则为混淆的密钥;即便加密不混淆,则为加密头的尺寸;否则应置0。
  6. 从012H到012H的1字节Byte,应当置0,并且忽略。
  7. 从013H到013H的1字节Byte,被分割为6局部,除了第⑤局地占3bit之外,其他各占1bit。
    • 第3位,必须置0,并且忽略。
    • 第②个人,通过右键菜单->新建->新建Word文件创立的空文件为1,其余应当为0。
    • 第①人,为是不是要用应用程序的默许值覆盖页面中的页面大小、页面方向、页边距等。
    • 第④个人和第⑨个人,未定义,应当忽略。
    • 第伍-6位,未定义,应当忽略。
  8. 从014H到015H和016H到017H的各2字节,应当置0,并且忽略。
  9. 从018H到01BH和01CH到01FH的各4字节,未定义,应当忽略。

那FibBase之后呢?其实FIB包涵众多的始末,从FibBase开首按顺序分别是:

  1. 2字节的UInt16,为其后Fib奔驰G级gW97块中拾肆位整数的个数,固定为0x000E。
  2. 28字节的FibRgW97块,包含14个UInt16。
  3. 2字节的UInt16,为今后Fib奇骏gLw97块中叁拾1个人整数的个数,固定为0x0016。
  4. 88字节的FibRgLw97块,包含22个UInt32。
  5. 2字节的UInt16,为未来FibQX56gFcLcb块中60人整数的个数(但Fib帕杰罗gFcLcb实际存储的是三十人整数)。
  • 若是文档为Word97,该项为0x005D。
  • 要是文档为Word三千,该项为0x006C。
  • 借使文档为Word二零零二,该项为0x0088。
  • 假若文档为Word2002,该项为0x00Accord。
  • 假设文档为Word二〇〇五,该项为0x00B7。

不定长的FibEvoquegFcLcb块,包括不定个数的叁十几个人UInt32(数量相当于上述个数的2倍),但可知至少存有18肆个。

2字节的UInt16,为后来Fib汉兰达gCswNew块中十五人整数的个数。

  • 设若文档为Word97,该项为0x00(实际上不带有FibRAV4gCswNew)。
  • 借使文档为Word2000-二零零零,该项为0x02。
  • 如若文档为Word2006,该项为0x05。

不定长的FibWranglergCswNew块,首先是定点长度的UInt16即Word文档的忠实版本nFibNew,然后二个UInt16象征文档在整机存档后急迅存档的次数,之后假设是Word二零零六则还有三个UInt16文档说并未定义且须求忽略(大囧)。

看完FIB结构后大家先来看下nFib与公事版本对应的景况:

  1. 0x00C1(nFib)表示文件为Word97(或者为更高版本的文档)。
  2. 0x00D9(nFibNew)表示文件为Word两千。
  3. 0x0101(nFibNew)表示文件为Word二〇〇三。
  4. 0x010C(nFibNew)表示文件为Word二〇〇〇。
  5. 0x0112(nFibNew)表示文件为Word二零零六。

是因为FIB中内容实在太多了,之后的有个别就不再介绍了,但是为了读取文档的内容大家还相应看看如下的故事情节(当然也不肯定都利用)。

  1. FibSportagegW97中的十五个UInt16,为文档的言语(lidFE),比如0x0804为简体普通话。即便文档是Unicode存储的本来无所谓,如若是ANSI码存储的那么就要求拿到那一个了。
  2. FibLX570gLw97中的第二个Int32,为Word Document中有含义的字节数(即Word
    Document之后的字节数都得以忽略)。
  3. Fib卡宴gLw97中的第⑤个Int32,为文档中正文(Main document)的总字数。
  4. FibCR-VgLw97中的第四个Int32,为文档中页脚(Footnote
    subdocument)的总字数。
  5. Fib奥迪Q5gLw97中的第⑥个Int32,为文档中页眉(Header
    subdocument)的总篇幅。
  6. Fib奥迪Q5gLw97中的第伍个Int32,为文档中批注(Comment
    subdocument)的总字数。
  7. FibLacrossegLw97中的第八个Int32,为文档中尾注(Endnote
    subdocument)的总字数。
  8. Fib安德拉gLw97中的第一3个Int32,为文档汉语本框(Textbox
    subdocument)的总篇幅。
  9. Fib安德拉gLw97中的第七壹个Int32,为文档中页眉文本框(Textbox Subdocument of
    the header)的总篇幅。
  10. FibSportagegFcLcb中的第六7个UInt32,为Piece Table在Table
    Stream中的偏移(fcClx)。
  11. FibRAV4gFcLcb中的第陆十七个UInt32,为Piece Table的字节数(lcbClx)。

如上这么些音信大家可以编写如下代码获取:

图片 2图片 3View Code

  1 #region 字段
  2 private UInt16 m_nFib;
  3 private Boolean m_isComplexFile;
  4 private Boolean m_hasPictures;
  5 private Boolean m_isEncrypted;
  6 private Boolean m_is1Table;
  7 
  8 private UInt16 m_lidFE;
  9 
 10 private Int32 m_cbMac;
 11 private Int32 m_ccpText;
 12 private Int32 m_ccpFtn;
 13 private Int32 m_ccpHdd;
 14 private Int32 m_ccpAtn;
 15 private Int32 m_ccpEdn;
 16 private Int32 m_ccpTxbx;
 17 private Int32 m_ccpHdrTxbx;
 18 
 19 private UInt32 m_fcClx;
 20 private UInt32 m_lcbClx;
 21 #endregion
 22 
 23 #region 读取WordDocument
 24 private void ReadWordDocument()
 25 {
 26     DirectoryEntry entry = this.m_dirRootEntry.GetChild("WordDocument");
 27 
 28     if (entry == null)
 29     {
 30         return;
 31     }
 32 
 33     Int64 entryStart = this.GetSectorOffset(entry.SectorID);
 34     this.m_stream.Seek(entryStart, SeekOrigin.Begin);
 35 
 36     this.ReadFileInformationBlock();
 37 }
 38 
 39 #region 读取FileInformationBlock
 40 private void ReadFileInformationBlock()
 41 {
 42     this.ReadFibBase();
 43     this.ReadFibRgW97();
 44     this.ReadFibRgLw97();
 45     this.ReadFibRgFcLcb();
 46     this.ReadFibRgCswNew();
 47 }
 48 
 49 #region FibBase
 50 private void ReadFibBase()
 51 {
 52     UInt16 wIdent = this.m_reader.ReadUInt16();
 53     if (wIdent != 0xA5EC)
 54     {
 55         throw new Exception("该文件不是Word文件!");
 56     }
 57 
 58     this.m_nFib = this.m_reader.ReadUInt16();
 59     this.m_reader.ReadUInt16();//unused
 60     this.m_reader.ReadUInt16();//lid
 61     this.m_reader.ReadUInt16();//pnNext
 62 
 63     UInt16 flags = this.m_reader.ReadUInt16();
 64     this.m_isComplexFile = this.GetBitFromInteger(flags, 2);
 65     this.m_hasPictures = this.GetBitFromInteger(flags, 3);
 66     this.m_isEncrypted = this.GetBitFromInteger(flags, 8);
 67     this.m_is1Table = this.GetBitFromInteger(flags, 9);
 68 
 69     if (this.m_isComplexFile)
 70     {
 71         throw new Exception("不支持复杂文件的读取!");
 72     }
 73 
 74     if (this.m_isEncrypted)
 75     {
 76         throw new Exception("不支持加密文件的读取!");
 77     }
 78 
 79     this.m_stream.Seek(32 - 12, SeekOrigin.Current);
 80 }
 81 #endregion
 82 
 83 #region FibRgW97
 84 private void ReadFibRgW97()
 85 {
 86     UInt16 count = this.m_reader.ReadUInt16();
 87 
 88     if (count != 0x000E)
 89     {
 90         throw new Exception("FibRgW97长度错误!");
 91     }
 92 
 93     this.m_stream.Seek(26, SeekOrigin.Current);
 94     this.m_lidFE = this.m_reader.ReadUInt16();
 95 }
 96 #endregion
 97 
 98 #region FibRgLw97
 99 private void ReadFibRgLw97()
100 {
101     UInt16 count = this.m_reader.ReadUInt16();
102 
103     if (count != 0x0016)
104     {
105         throw new Exception("FibRgLw97长度错误!");
106     }
107 
108     this.m_cbMac = this.m_reader.ReadInt32();
109     this.m_reader.ReadInt32();//reserved1
110     this.m_reader.ReadInt32();//reserved2
111     this.m_ccpText = this.m_reader.ReadInt32();
112     this.m_ccpFtn = this.m_reader.ReadInt32();
113     this.m_ccpHdd = this.m_reader.ReadInt32();
114     this.m_reader.ReadInt32();//reserved3
115     this.m_ccpAtn = this.m_reader.ReadInt32();
116     this.m_ccpEdn = this.m_reader.ReadInt32();
117     this.m_ccpTxbx = this.m_reader.ReadInt32();
118     this.m_ccpHdrTxbx = this.m_reader.ReadInt32();
119 
120     this.m_stream.Seek(44, SeekOrigin.Current);
121 }
122 #endregion
123 
124 #region FibRgFcLcb
125 private void ReadFibRgFcLcb()
126 {
127     UInt16 count = this.m_reader.ReadUInt16();
128     this.m_stream.Seek(66 * 4, SeekOrigin.Current);
129 
130     this.m_fcClx = this.m_reader.ReadUInt32();
131     this.m_lcbClx = this.m_reader.ReadUInt32();
132 
133     this.m_stream.Seek((count * 2 - 68) * 4, SeekOrigin.Current);
134 }
135 #endregion
136 
137 #region FibRgCswNew
138 private void ReadFibRgCswNew()
139 {
140     UInt16 count = this.m_reader.ReadUInt16();
141     this.m_nFib = this.m_reader.ReadUInt16();
142     this.m_stream.Seek((count - 1) * 2, SeekOrigin.Current);
143 }
144 #endregion
145 #endregion
146 #endregion
147 
148 private Boolean GetBitFromInteger(Int32 integer, Int32 bitIndex)
149 {
150     Int32 num = (Int32)Math.Pow(2, bitIndex);
151     return (integer & num) == num;
152 }

 

【二、Table Stream中的Piece Table】

Table
Stream其实就是1Table要么0Table的总称,具体文字存在卓殊Table中还要依据FIB中的消息。由于复合文件是以2个个Sector方式储存的,所以大家第②需求取得文字存储在怎么着个Sector中。实际上,文本的存储是由Piece
Element(临时这么叫吧)控制着,包含是或不是启用Unicode、每块的岗位等等,那个情节都存放于Table
Stream中的Piece Table中,Piece Table绝对Table
Stream的偏移量可以从FIB中获得到。

至于Piece Element,官方是这么描述的:

图片 4

看起来这么多,其实大家必要的仅是fc中定义的是否使用Unicode存储文本(fc中第③拾壹人为0则为Unicode,为1则为Ansi),以及文本相对于WordDocument的偏移量(fc中没有三十六人),我们先是对Piece
Element定义多个类,可以看来,五个Piece Element的大小实际为2 + 4 + 2 =
8字节:

图片 5图片 6View Code

 1 public class PieceElement
 2 {
 3     #region 字段
 4     private UInt16 m_info;
 5     private UInt32 m_fc;
 6     private UInt16 m_prm;
 7     private Boolean m_isUnicode;
 8     #endregion
 9 
10     #region 属性
11     /// <summary>
12     /// 获取是否以Unicode形式存储文本
13     /// </summary>
14     public Boolean IsUnicode
15     {
16         get { return this.m_isUnicode; }
17     }
18 
19     /// <summary>
20     /// 获取文本偏移量
21     /// </summary>
22     public UInt32 Offset
23     {
24         get { return this.m_fc; }
25     }
26     #endregion
27 
28     #region 构造函数
29     public PieceElement(UInt16 info, UInt32 fcCompressed, UInt16 prm)
30     {
31         this.m_info = info;
32         this.m_fc = fcCompressed & 0x3FFFFFFF;//后30位
33         this.m_prm = prm;
34         this.m_isUnicode = (fcCompressed & 0x40000000) == 0;//第31位
35 
36         if (!this.m_isUnicode) this.m_fc = this.m_fc / 2;
37     }
38     #endregion
39 }

 

下一场我们来看Piece Table,其协会为:

  1. 从000H到000H的1字节Byte,是Piece Table的标识,为一定的0x02。
  2. 从001H到004H的4字节UInt32,是Piece
    Table的尺寸(即存储文字的Sector的数额)。
    法定给了2个Piece Table中个数的计算公式
    图片 7

    其间,cbPlc即Piece Table的大大小小,cbData为2个Piece
    Element的分寸,所以Piece Table中的个数实际为n = (size – 4) / 12。

  3. 之后4*(n + 1)个字节,是各个Piece
    Element存储的文本的起第四位置(截止地方即下一个的始发地方)。
  4. 之后8*n个字节,是各种Piece Element的有关消息。

Piece Table新闻我们得以编制如下代码获取:

图片 8图片 9View Code

 1 private void ReadTableStream()
 2 {
 3     DirectoryEntry entry = this.m_dirRootEntry.GetChild((this.m_is1Table ? "1Table" : "0Table"));
 4 
 5     if (entry == null)
 6     {
 7         return;
 8     }
 9 
10     Int64 pieceTableStart = this.GetSectorOffset(entry.SectorID) + this.m_fcClx;
11     Int64 pieceTableEnd = pieceTableStart + this.m_lcbClx;
12     this.m_stream.Seek(pieceTableStart, SeekOrigin.Begin);
13 
14     Byte clxt = this.m_reader.ReadByte();
15     Int32 prcLen = 0;
16 
17     //判断如果是Prc不是Pcdt
18     while (clxt == 0x01 && this.m_stream.Position < pieceTableEnd)
19     {
20         this.m_stream.Seek(prcLen, SeekOrigin.Current);
21         clxt = this.m_reader.ReadByte();
22         prcLen = this.m_reader.ReadInt32();
23     }
24 
25     if (clxt != 0x02)
26     {
27         throw new Exception("该文件不存在内容!");
28     }
29 
30     UInt32 size = this.m_reader.ReadUInt32();
31     UInt32 count = (size - 4) / 12;
32 
33     this.m_lstPieceStartPosition = new List<UInt32>();
34     this.m_lstPieceEndPosition = new List<UInt32>();
35     this.m_lstPieceElement = new List<PieceElement>();
36 
37     for (Int32 i = 0; i < count; i++)
38     {
39         this.m_lstPieceStartPosition.Add(this.m_reader.ReadUInt32());
40         this.m_lstPieceEndPosition.Add(this.m_reader.ReadUInt32());
41         this.m_stream.Seek(-4, SeekOrigin.Current);
42     }
43 
44     this.m_stream.Seek(4, SeekOrigin.Current);
45 
46     for (Int32 i = 0; i < count; i++)
47     {
48         UInt16 info = this.m_reader.ReadUInt16();
49         UInt32 fcCompressed = this.m_reader.ReadUInt32();
50         UInt16 prm = this.m_reader.ReadUInt16();
51 
52         this.m_lstPieceElement.Add(new PieceElement(info, fcCompressed, prm));
53     }
54 }

 

【三 、正式拿到文本内容】

地点大家得以博得到Word汉语本的发端和了结地点,其实二个Word文档中,文字是按如下顺序存储的:

  1. 本文内容(Main document)
  2. 脚注(Footnote subdocument)
  3. 页眉和页脚(Header subdocument)
  4. 批注(Comment subdocument)
  5. 尾注(Endnote subdocument)
  6. 文本框(Textbox subdocument)
  7. 页眉文本框(Textbox Subdocument of the header)

由此,我们得以按照Fib奥德赛gLw97中拿走的每一片段的字数以及Piece
Table中开局的地方来拿到每一部分的文字。

例如正文内容的职位为[0, ccpText],页脚的岗位为[ccpText + 1, ccpText +
1 + ccpFtn]……

所以我们编辑如下代码获取:

图片 10图片 11View Code

  1 #region 字段
  2 private String m_paragraphText;
  3 private String m_footnoteText;
  4 private String m_headerText;
  5 private String m_commentText;
  6 private String m_endnoteText;
  7 private String m_textboxText;
  8 private String m_headerTextboxText;
  9 #endregion
 10 
 11 #region 属性
 12 /// <summary>
 13 /// 获取文档正文内容
 14 /// </summary>
 15 public String ParagraphText
 16 {
 17     get { return this.m_paragraphText; }
 18 }
 19 
 20 /// <summary>
 21 /// 获取文档页脚内容
 22 /// </summary>
 23 public String FootnoteText
 24 {
 25     get { return this.m_footnoteText; }
 26 }
 27 
 28 /// <summary>
 29 /// 获取文档页眉内容
 30 /// </summary>
 31 public String HeaderText
 32 {
 33     get { return this.m_headerText; }
 34 }
 35 
 36 /// <summary>
 37 /// 获取文档批注内容
 38 /// </summary>
 39 public String CommentText
 40 {
 41     get { return this.m_commentText; }
 42 }
 43 
 44 /// <summary>
 45 /// 获取文档尾注内容
 46 /// </summary>
 47 public String EndnoteText
 48 {
 49     get { return this.m_endnoteText; }
 50 }
 51 
 52 /// <summary>
 53 /// 获取文档文本框内容
 54 /// </summary>
 55 public String TextboxText
 56 {
 57     get { return this.m_textboxText; }
 58 }
 59 
 60 /// <summary>
 61 /// 获取文档页眉文本框内容
 62 /// </summary>
 63 public String HeaderTextboxText
 64 {
 65     get { return this.m_headerTextboxText; }
 66 }
 67 #endregion
 68 
 69 #region 读取文本内容
 70 private void ReadPieceText()
 71 {
 72     StringBuilder sb = new StringBuilder();
 73     DirectoryEntry entry = this.m_dirRootEntry.GetChild("WordDocument");
 74 
 75     for (Int32 i = 0; i < this.m_lstPieceElement.Count; i++)
 76     {
 77         Int64 pieceStart = this.GetSectorOffset(entry.SectorID) + this.m_lstPieceElement[i].Offset;
 78         this.m_stream.Seek(pieceStart, SeekOrigin.Begin);
 79 
 80         Int32 length = (Int32)((this.m_lstPieceElement[i].IsUnicode ? 2 : 1) * (this.m_lstPieceEndPosition[i] - this.m_lstPieceStartPosition[i]));
 81         Byte[] data = this.m_reader.ReadBytes(length);
 82         String content = GetString(this.m_lstPieceElement[i].IsUnicode, data);
 83         sb.Append(content);
 84     }
 85 
 86     String allContent = sb.ToString();
 87     Int32 paragraphEnd = this.m_ccpText;
 88     Int32 footnoteEnd = paragraphEnd + this.m_ccpFtn;
 89     Int32 headerEnd = footnoteEnd + this.m_ccpHdd;
 90     Int32 commentEnd = headerEnd + this.m_ccpAtn;
 91     Int32 endnoteEnd = commentEnd + this.m_ccpEdn;
 92     Int32 textboxEnd = endnoteEnd + this.m_ccpTxbx;
 93     Int32 headerTextboxEnd = textboxEnd + this.m_ccpHdrTxbx;
 94 
 95     this.m_paragraphText = allContent.Substring(0, this.m_ccpText);
 96     this.m_footnoteText = allContent.Substring(paragraphEnd, this.m_ccpFtn);
 97     this.m_headerText = allContent.Substring(footnoteEnd, this.m_ccpHdd);
 98     this.m_commentText = allContent.Substring(headerEnd, this.m_ccpAtn);
 99     this.m_endnoteText = allContent.Substring(commentEnd, this.m_ccpEdn);
100     this.m_textboxText = allContent.Substring(endnoteEnd, this.m_ccpTxbx);
101     this.m_headerTextboxText = allContent.Substring(textboxEnd, this.m_ccpHdrTxbx);
102 }
103 #endregion
104 
105 private String GetString(Boolean isUnicode, Byte[] data)
106 {
107     if (isUnicode)
108     {
109         return Encoding.Unicode.GetString(data);
110     }
111     else
112     {
113         return Encoding.GetEncoding("Windows-1252").GetString(data);
114     }
115 }

不过需求专注的是,由于Word文档中的换行为“\r”(CLX570),而Windows中的换行符为“\r\n”(C奥迪Q5+LF),所以取得文字后须要将“\r”替换为“\r\n”,否则换行将不能平常突显,除此之外,还有其他的一部分特殊字符也急需替换或拍卖。

附,本文全部代码下载:https://github.com/mayswind/SimpleOfficeReader

 

【四 、相关链接】

*1、Microsoft Open
Specifications:http://www.microsoft.com/openspecifications/en/us/programs/osp/default.aspx
2、用PHP读取MS
Word(.doc)中的文字:https://imethan.com/post-2009-10-06-17-59.html **
三 、Office檔案格式:http://www.programmer-club.com.tw/ShowSameTitleN/general/2681.html** 4、LAOLA file
system:http://stuff.mit.edu/afs/athena/astaff/project/mimeutils/share/laola/guide.html*

 

【后记】

本还想星期日夜晚发出去,结果要么没写完。希望这一次的小说能对我们有用。若是您觉得好就点下推荐呗。