在处理文档数据时,经常会遇到需要从.doc文件中提取文本的情况。Microsoft Word的.doc文件格式是一种流行的富文本表示格式,它使用OLE(对象链接与嵌入)技术来实现文档的复杂格式、节和表格。然而,使用OLE技术编程处理Word文档存在一些缺点,如速度慢、需要安装Microsoft Office以及文档模型不便于操作。为了解决这些问题,可以直接读取和写入.doc文件,这样可以提高速度并允许使用自定义的文档模型。但这种方法的主要难点在于需要了解二进制文件格式。
Word文档的文件结构被表示为文件内的文件系统。这种技术称为OLE结构化存储,它允许在单个文档中存储多种对象。OLE结构化存储由两种对象类型组成:存储和流。
WinAPI函数StgOpenStorage提供了访问结构化存储系统中根存储对象的接口。以下是该函数的声明:
C# [DllImport(Ole32Dll, CharSet = CharSet.Unicode)] public static extern int StgOpenStorage(string pwcsName, IStorage pstgPriority, int grfMode, IntPtr snbExclude, int reserved, out IStorage ppstgOpen);
在这个声明中,主要使用pwcsName(要打开的存储对象的文件路径)和ppstgOpen(用于操作根文件存储的IStorage实现者)。以下是使用StgOpenStorage函数的示例代码:
C# const int _DefaultFlags = (int)(STGMFlags.STGM_READWRITE | STGMFlags.STGM_SHARE_EXCLUSIVE); public static IStorage OpenRootStorage(string path) { IStorage storage; int result = NativeMethods.StgOpenStorage(path, null, _DefaultFlags, IntPtr.Zero, 0, out storage); if (result != 0) return null; return storage; }
IStorage接口的翻译和一些必要结构的实现可以在文章的源代码中找到。
根Word文件存储包含文档和表格流,将使用这些流来读取文档文本。文档流包含Word文件的头部(FIB - 文件信息块)、文档文本和格式信息。文档文本及其格式存储为一组片段,每个片段在文档流中都有一个偏移量。表格流包含关于文本位置的信息,表示为一组片段描述符的集合。
要访问根存储中的任何流,将使用IStorage接口提供的OpenStream WinAPI函数。以下是该函数的声明:
C# int OpenStream(string pwcsName, IntPtr reserved1, int grfMode, int reserved2, out UCOMIStream ppstm);
在这个声明中,pwcsName是存储的名称,ppstm是指向结果流的指针。以下是获取文件文档和表格流的代码:
C# const int _DefaultFlags = (int)(STGMFlags.STGM_READWRITE | STGMFlags.STGM_SHARE_EXCLUSIVE); UCOMIStream OpenStream(IStorage storage, string name) { UCOMIStream stream; int result = storage.OpenStream(name, IntPtr.Zero, _DefaultFlags, 0, out stream); if (result != 0) return null; return stream; }
以下是获取流数据的方法:
C# byte[] GetStreamData(UCOMIStream stream) { STATSTG stat; stream.Stat(out stat, 0); long size = stat.cbSize; byte[] buffer = new byte[size]; stream.Read(buffer, (int)size, IntPtr.Zero); return buffer; }
以下是获取流读取器的方法:
C# BinaryReader GetStreamReader(UCOMIStream stream) { if (stream == null) return null; byte[] streamData = GetStreamData(stream); MemoryStream memoryStream = new MemoryStream(streamData); return new BinaryReader(memoryStream); }
以下是获取流数据的方法:
C# void GetStreamsData(string path, out BinaryReader documentStreamReader, out BinaryReader tableStreamReader) { IStorage rootStorage = OpenRootStorage(path); UCOMIStream documentStream = OpenStream(rootStorage, "WordDocument"); UCOMIStream tableStream = OpenStream(rootStorage, "0Table"); documentStreamReader = GetStreamReader(documentStream); tableStreamReader = GetStreamReader(tableStream); }
现在已经访问了主要的文件流,可以读取有关文档文本位置的信息。这些信息可以从表格流的fib.clxOffset和fib.clxLength中获得:
C# void GetDataFromFib(BinaryReader tableStreamReader, out int pieceCollOffset, out uint pieceCollLength) { tableStreamReader.BaseStream.Seek(418, SeekOrigin.Begin); pieceCollOffset = reader.ReadInt32(); pieceCollLength = reader.ReadUInt32(); }
有了这些信息,可以从表格流中读取所有的片段描述符。每个片段描述符包含有关存储在文档中的文本部分的信息。以下是代码示例:
C# PieceDescriptorCollection GetPieceDescriptors(BinaryReader tableStreamReader, int pieceCollOffset, uint pieceCollLength) { PieceDescriptorCollection result = new PieceDescriptorCollection(pieceCollOffset, pieceCollLength); result.Read(tableStreamReader); return result; }
请注意,所有读取片段描述符的工作都在PieceDescriptorCollection类中完成。完整的实现可以在本文的源代码中找到。
最后一步是读取文档文本。以下是如何做到这一点的方法:
C# string LoadText(BinaryReader documentReader, PieceDescriptorCollection pieces) { string text = string.Empty; if (documentReader == null || pieces == null) return text; int count = pieces.Count; for (int i = 0; i < count; i++) { uint pieceStart; uint pieceEnd; bool isUnicode = pieces.GetPieceFileBounds(i, out pieceStart, out pieceEnd); documentReader.BaseStream.Seek(pieceStart, SeekOrigin.Begin); text += ReadString(documentReader, pieceEnd - pieceStart, isUnicode); } return text; }
LoadText方法遍历所有文档片段,获取每个片段的边界并读取文档文本。ReadString方法很简单:
C# string ReadString(BinaryReader reader, uint length, bool isUnicode) { if (length == 0) return string.Empty; if (isUnicode) length = length / 2; string result = string.Empty; for (int i = 0; i < length; i++) { object ch = isUnicode ? reader.ReadInt16() : reader.ReadByte(); result += (char)ch; } return result; }