PHPOffice文件的深——.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组成部分,除了第5部分占用了4bit异,其余12有些各站1bit,总计16bit,我们可通过各类运算分别读取每一bit的值,比如Boolean
    isDot = ((n & 0x1) ==
    1),就得读取最低位是否为确实了。插张图来说明下13片段是怎么样分配的,最荒唐为UInt16的最低位。
    PHP 1

    • A(第0各项),为文档是否是.Dot文件(Word模板文件)
    • B(第1个),没了解就同一员存的凡什么。
    • C(第2号),为文档是否是复杂格式(快速保存时变的格式)。
    • D(第3个),为文档是否包含图表。
    • E(第4-7号),当nFib小于0x00D9时吗快保存(Quick
      Save)的次数,当大于0x00D9时始终为0x0F。
    • F(第8个),为文档是否加密。
    • G(第9员),为1时言存储于1Table,为0时字存储于0Table。
    • H(第10各项),为是否“建议以单纯读方式打开文档”(保存时选择“工具”->“常规选项”可以安装该属性)。
    • I(第11各类),为是否有描绘保护密码。
    • J(第12各),为固定值1。
    • K(第13个),为是否如因此应用程序的语言默认值覆盖段落格式中定义之言语与字体。
    • L(第14各类),为文档语言是否也东亚语言。
    • M(第15各),当文档加密时,文档如果采用XOR混淆则也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片段,除了第6组成部分占用3bit以外,其余各占1bit。
    • 第1各项,必须置0,并且忽略。
    • 第2个,通过右键菜单->新建->新建Word文件创建的空文件为1,其余应当为0。
    • 第3号,为是否如就此应用程序的默认值覆盖页面中之页面大小、页面方向、页边距等。
    • 第4各和第5个,未定义,应当忽略。
    • 第6-8员,未定义,应当忽略。
  8. 从014H到015H和016H到017H的诸2字节,应当置0,并且忽略。
  9. 从018H到01BH和01CH到01FH的各国4字节,未定义,应当忽略。

这就是说FibBase之后为?其实FIB包含多之始末,从FibBase开始遵循梯次分别是:

  1. 2字节的UInt16,为今后FibRgW97片被16各项整数的个数,固定为0x000E。
  2. 28字节的FibRgW97块,包含14个UInt16。
  3. 2字节的UInt16,为下FibRgLw97块被32员整数的个数,固定啊0x0016。
  4. 88字节的FibRgLw97块,包含22个UInt32。
  5. 2字节的UInt16,为以后FibRgFcLcb块被64各项整数的个数(但FibRgFcLcb实际存储的凡32个整数)。

  6. 使文档为Word97,该项为0x005D。

  7. 一旦文档为Word2000,该项为0x006C。
  8. 若果文档为Word2002,该项为0x0088。
  9. 只要文档为Word2003,该项为0x00A4。
  10. 如果文档为Word2007,该项为0x00B7。

勿必然长之FibRgFcLcb块,包含无必然个数的32位UInt32(数量为即是上述个数的2倍增),但可见至少有186独。

2字节的UInt16,为后来FibRgCswNew块中16各整数的个数。

  • 若是文档为Word97,该项为0x00(实际上不分包FibRgCswNew)。
  • 比方文档为Word2000-2003,该项为0x02。
  • 万一文档为Word2007,该项为0x05。

切莫必然长之FibRgCswNew块,首先是稳定长度的UInt16哪怕Word文档的实版nFibNew,然后一个UInt16代表文档在总体存档后速存档的次数,之后要是Word2007虽然还有3独UInt16文档说并未概念且要求忽略(大囧)。

看了FIB结构后我们事先来拘禁下nFib与公事版本对应之图景:

  1. 0x00C1(nFib)表示文件也Word97(或者也重复胜似版本的文档)。
  2. 0x00D9(nFibNew)表示文件为Word2000。
  3. 0x0101(nFibNew)表示文件呢Word2002。
  4. 0x010C(nFibNew)表示文件为Word2003。
  5. 0x0112(nFibNew)表示文件也Word2007。

鉴于FIB中内容其实太多矣,之后的有些就不再介绍了,不过为读取文档的情节我们尚相应看如下的情(当然为无必然都以)。

  1. FibRgW97中之14只UInt16,为文档的语言(lidFE),比如0x0804啊简体中文。如果文档是Unicode存储的当然无所谓,如果是ANSI码存储的那即便用获得这了。
  2. FibRgLw97中之第1单Int32,为Word Document中产生义之字节数(即Word
    Document之后的字节数都可以忽略)。
  3. FibRgLw97中之第4个Int32,为文档中正文(Main document)的总篇幅。
  4. FibRgLw97中的第5独Int32,为文档中页脚(Footnote
    subdocument)的终究篇幅。
  5. FibRgLw97中的第6单Int32,为文档中页眉(Header
    subdocument)的总篇幅。
  6. FibRgLw97中之第7个Int32,为文档中批注(Comment
    subdocument)的终究字数。
  7. FibRgLw97中之第8独Int32,为文档中尾注(Endnote
    subdocument)的毕竟篇幅。
  8. FibRgLw97中的第10单Int32,为文档中文本框(Textbox
    subdocument)的终究字数。
  9. FibRgLw97中之第11个Int32,为文档中页眉文本框(Textbox Subdocument of
    the header)的毕竟字数。
  10. FibRgFcLcb中的第67独UInt32,为Piece Table在Table
    Stream中之摆(fcClx)。
  11. FibRgFcLcb中的第68单UInt32,为Piece Table的字节数(lcbClx)。

以上这些消息我们得编制如下代码获取:

PHP 2PHP 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中的消息。由于复合文件是因一个个Sector形式储存的,所以我们首先需取得文字存储于安个Sector中。实际上,文本的存储是由于Piece
Element(暂且这么给吧)控制着,包括是否启用Unicode、每块的位置等等,这些情节都存放于Table
Stream中的Piece Table中,Piece Table相对Table
Stream的偏移量可以于FIB中获得到。

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

PHP 4

看起来这么多,其实我们要之单纯是fc中定义的是不是以Unicode存储文本(fc中第31号吗0虽为Unicode,为1虽也Ansi),以及文本相对于WordDocument的偏移量(fc中没有30各),我们第一对Piece
Element定义一个像样,可以看到,一个Piece Element的分寸实际为2 + 4 + 2 =
8字节:

PHP 5PHP 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的数)。
    官给了一个Piece Table中个数的计算公式
    PHP 7

    中,cbPlc即Piece Table的大小,cbData为一个Piece
    Element的轻重缓急,所以Piece Table中的个数实际也n = (size – 4) / 12。

  3. 之后4*(n + 1)个字节,是每个Piece
    Element存储的文本的始位置(结束位置就下一个之启位置)。
  4. 之后8*n个字节,是每个Piece Element的连带消息。

Piece Table信息我们可编制如下代码获取:

PHP 8PHP 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)

就此,我们可以依据FibRgLw97中得之各级一样有的字数以及Piece
Table中开始之岗位来赢得每一样片的文。

按部就班正文内容之职位吗[0, ccpText],页脚的位置为[ccpText + 1, ccpText +
1 + ccpFtn]……

故我们编辑如下代码获取:

PHP 10PHP 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”(CR),而Windows中之换行符为“\r\n”(CR+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 **
3、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*

 

【后记】

按照还眷恋周日晚作出去,结果要没写了。希望这次的文章能针对大家有用。如果您觉得好就点下推荐呗。

相关文章