Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(二)

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 原文 http://www.cnblogs.com/mayswind/archive/2013/03/25/2968964.html   【题外话】 上篇文章很荣幸被NPOI的大神回复了,同时也纠正了我一个问题,就是NPOI其实是有doc文件的解析,只不过一直没有跟随正式版发布过,要获取这部分代码,可以移步CodePlex(http://npoi.codeplex.com/),访问在SourceCode中的NPOI.ScratchPad中即可看到。

原文 http://www.cnblogs.com/mayswind/archive/2013/03/25/2968964.html

 

【题外话】

上篇文章很荣幸被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的最低位。

    • 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位整数)。
  • 如果文档为Word97,该项为0x005D。
  • 如果文档为Word2000,该项为0x006C。
  • 如果文档为Word2002,该项为0x0088。
  • 如果文档为Word2003,该项为0x00A4。
  • 如果文档为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)。

以上这些信息我们可以编写如下代码获取:

View 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,官方是这么描述的:

看上去这么多,其实我们需要的仅是fc中定义的是否使用Unicode存储文本(fc中第31位为0则为Unicode,为1则为Ansi),以及 文本相对于WordDocument的偏移量(fc中低位30位),我们首先对Piece Element定义一个类,可以看出,一个Piece Element的大小实际为2 + 4 + 2 = 8字节:

View 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中个数的计算公式
    其中,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信息我们可以编写如下代码获取:

View 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]……

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

View Code

不过需要注意的是,由于Word文档中的换行为“\r”(CR),而Windows中的换行符为“\r\n”(CR+LF),所以获取文字后需要将“\r”替换为“\r\n”,否则换行将无法正常显示,除此之外,还有其他的一些特殊字符也需要替换或处理。

附,本文所有代码下载:http://files.cnblogs.com/mayswind/DotMaysWind.OfficeReader_2.rar

 

【四、相关链接】

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

 

【后记】

本还想周日晚上发出来,结果还是没写完。希望这次的文章能对大家有用。如果您觉得好就点下推荐呗。


版权声明:本文发布于 博客园,作者为 大魔王mAysWINd,文章欢迎转载,但请保留此段版权声明和原始链接,谢谢合作!

目录
相关文章
|
29天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
67 2
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
151 3
|
2月前
|
自然语言处理 数据处理 Python
python操作和解析ppt文件 | python小知识
本文将带你从零开始,了解PPT解析的工具、工作原理以及常用的基本操作,并提供具体的代码示例和必要的说明【10月更文挑战第4天】
434 60
|
25天前
|
消息中间件 存储 Java
RocketMQ文件刷盘机制深度解析与Java模拟实现
【11月更文挑战第22天】在现代分布式系统中,消息队列(Message Queue, MQ)作为一种重要的中间件,扮演着连接不同服务、实现异步通信和消息解耦的关键角色。Apache RocketMQ作为一款高性能的分布式消息中间件,广泛应用于实时数据流处理、日志流处理等场景。为了保证消息的可靠性,RocketMQ引入了一种称为“刷盘”的机制,将消息从内存写入到磁盘中,确保消息持久化。本文将从底层原理、业务场景、概念、功能点等方面深入解析RocketMQ的文件刷盘机制,并使用Java模拟实现类似的功能。
40 3
|
27天前
|
机器学习/深度学习 人工智能 Cloud Native
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台。本文深入解析 .NET 的核心优势,探讨其在企业级应用、Web 开发及移动应用等领域的应用案例,并展望未来在人工智能、云原生等方面的发展趋势。
32 3
|
1月前
|
存储 设计模式 编解码
.NET 8.0 通用管理平台,支持模块化、WinForms 和 WPF
【11月更文挑战第5天】本文分析了.NET 8.0 通用管理平台在模块化、WinForms 和 WPF 方面的优势。模块化设计提升了系统的可维护性和可扩展性,提高了代码复用性;WinForms 提供了丰富的控件库和简单易用的开发模式,技术成熟稳定;WPF 支持强大的数据绑定和 MVVM 模式,具备丰富的图形和动画功能,以及灵活的布局系统。
|
1月前
|
存储
文件太大不能拷贝到U盘怎么办?实用解决方案全解析
当我们试图将一个大文件拷贝到U盘时,却突然跳出提示“对于目标文件系统目标文件过大”。这种情况让人感到迷茫,尤其是在急需备份或传输数据的时候。那么,文件太大为什么会无法拷贝到U盘?又该如何解决?本文将详细分析这背后的原因,并提供几个实用的方法,帮助你顺利将文件传输到U盘。
|
1月前
|
C#
【Azure App Service】使用Microsoft.Office.Interop.Word来操作Word文档,部署到App Service后报错COMException
System.Runtime.InteropServices.COMException (0x80040154): Retrieving the COM class factory for component with CLSID {000209FF-0000-0000-C000-000000000046} failed due to the following error: 80040154 Class not registered (0x80040154 (REGDB_E_CLASSNOTREG)).
|
2月前
|
测试技术 API 开发者
精通.NET单元测试:MSTest、xUnit、NUnit全面解析
【10月更文挑战第15天】本文介绍了.NET生态系统中最流行的三种单元测试框架:MSTest、xUnit和NUnit。通过示例代码展示了每种框架的基本用法和特点,帮助开发者根据项目需求和个人偏好选择合适的测试工具。
44 3
|
2月前
|
数据安全/隐私保护 流计算 开发者
python知识点100篇系列(18)-解析m3u8文件的下载视频
【10月更文挑战第6天】m3u8是苹果公司推出的一种视频播放标准,采用UTF-8编码,主要用于记录视频的网络地址。HLS(Http Live Streaming)是苹果公司提出的一种基于HTTP的流媒体传输协议,通过m3u8索引文件按序访问ts文件,实现音视频播放。本文介绍了如何通过浏览器找到m3u8文件,解析m3u8文件获取ts文件地址,下载ts文件并解密(如有必要),最后使用ffmpeg合并ts文件为mp4文件。

推荐镜像

更多