在 Office 中,可以在 PPT 裡面插入表格,插入表格有好多不同的方法,對應 OpenXML 文件儲存的更多不同的方式。本文來介紹如何讀取 PPT 內嵌 ole 格式的 xls+ 表格的方法
在 Office 的 PPT 中,插入表格可以對應多個不同的方式:
- 通過 GraphicData 內嵌到 PPTX 頁面裡面
- 通過嵌入檔案方式
- 通過 SmartArt 模擬的表格,本質上就是 SmartArt 元素
其中通過嵌入檔案方式可以分為以下不同的嵌入方式:
- 通過外嵌 Microsoft_Excel_Worksheet.xlsx 格式,此格式可以解析。這是在 Office 2019 的預設
- 通過外嵌 oleObject1.bin 格式,此格式是 ole 格式,裡面包含 xls+ 格式
- 通過外嵌 oleObject1.bin 格式,此格式是 ole 格式,裡面包含了 xls 格式
什麼是 xls+ 格式?其實這個名字我沒有找到權威的文件來說明。大概是在 Office 2016 的預設行為是如此,點選表格,插入 Excel 電子表格時嵌入的文件就是此格式。這個格式存放方式是 ole 格式,在此 OLE 檔案裡面,將存放 OpenXML 格式的 xlsx 格式的表格檔案,以下將詳細告訴大家此格式
在 Slide.xml 頁面裡面,存放的是在 GraphicFrame 下的內容,簡化的 OpenXML 文件如下
<p:graphicFrame>
<p:nvGraphicFramePr>
<p:cNvPr id="9" name="表格 1" />
</p:nvGraphicFramePr>
<p:xfrm>
<a:off x="5405438" y="3241675" />
<a:ext cx="3438525" cy="2009775" />
</p:xfrm>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/presentationml/2006/ole">
<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<mc:Choice xmlns:v="urn:schemas-microsoft-com:vml" Requires="v">
<p:oleObj spid="_x0000_s1026" name="工作表" r:id="rId3" imgW="3438630" imgH="2009788" progId="Excel.Sheet.12">
<p:embed />
</p:oleObj>
</mc:Choice>
<mc:Fallback>
<!-- 忽略 -->
</mc:Fallback>
</mc:AlternateContent>
</a:graphicData>
</a:graphic>
</p:graphicFrame>
以上邏輯核心的就是存放的嵌入的 oleObj 物件,可以在 Slide.xml.rels 檔案裡面找到如下定義內容
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="../embeddings/oleObject1.bin" />
</Relationships>
也就是說嵌入的表格是放在 embeddings 資料夾下的 oleObject1.bin 檔案,這是一個 OLE 檔案。本質上來說 OLE 和 ZIP 等壓縮格式是同等級的,是用來做儲存的,也就是說 OLE 格式本身不是特定給 Excel 表格使用的,僅僅只是用來做儲存而已。大家是否還記得 ppt 和 pptx 的差別,上古(2003)的時候,採用的是格式是 ppt 格式,此格式的儲存就是 OLE 儲存方式,也可以這樣認為,古時候的 xls 和 ppt 等都是 OLE 檔案。但是新版本的 pptx 和 xlsx 等都是 OpenXML 格式
嵌入到 PPT 的 oleObject1.bin 也就是 OLE 檔案,對應上古的格式。但是有一些不同的是,此檔案不屬於 xls 檔案格式,而是細分為兩個類別,其中一個是在 OLE 裡面存放 xls 的,另一個存放的是 xlsx 的。也就是說需要將 oleObject1.bin 展開,才可以獲取裡面的表格檔案。本文將在 OLE 裡面存放 xlsx 格式的嵌入方式稱為 xls+ 格式
先來開始從 OpenXML 文件讀取到 OLE 嵌入檔案的邏輯
和通用的 PPTX 檔案解析相同的邏輯,先讀取檔案,我的測試檔案在首頁就嵌入了表格。本文所有的程式碼和測試檔案都可以在本文末尾找到下載方式
var file = new FileInfo("Test.pptx");
using var presentationDocument = PresentationDocument.Open(file.FullName, false);
var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;
接下來獲取 GraphicFrame 和裡層的資訊
var graphicFrame = slide.CommonSlideData!.ShapeTree!.GetFirstChild<GraphicFrame>()!;
var graphic = graphicFrame.Graphic!;
var graphicData = graphic.GraphicData!;
如上述文件,在 GraphicData 裡面存放的是 AlternateContent 元素,此元素裡面再嵌入 OLE 檔案
var alternateContent = graphicData.GetFirstChild<AlternateContent>()!;
var choice = alternateContent.GetFirstChild<AlternateContentChoice>()!;
var oleObject = choice.GetFirstChild<OleObject>()!;
Debug.Assert(oleObject.GetFirstChild<OleObjectEmbed>() != null);
通過以上邏輯即可獲取到對應的 OleObject 物件。本文上面的例子程式碼僅僅只是用於本文的測試檔案,對於其他檔案不確定是否存在表格的,還請自行判斷空,而不是採用本文的斷言方式。本文的例子裡的程式碼為了清晰,就不新增其他分支判斷
以上程式碼拿到了 OleObject 即可獲取到對應的 oleObject1.bin 檔案。在 OpenXML SDK 裡面,不會真的將 PPTX 檔案解壓縮,原因有兩個:第一個是效能考慮,第二個是有一些內容解壓縮之後會丟失資訊(不是使用檔案存放的,只是相容zip格式而已)而導致了嘗試使用路徑讀取 oleObject1.bin 檔案是不可行的。通過 dotnet OpenXML 為什麼資源使用 Relationship 引用 部落格瞭解到,讀取方法如下
var id = oleObject.Id!;
var part = slide.SlidePart!.GetPartById(id!);
Debug.Assert(part.ContentType== "application/vnd.openxmlformats-officedocument.oleObject");
使用 part.GetStream(FileMode.Open)
就可以開啟 oleObject1.bin 對應的 Stream 物件
然而這是一個 OLE 物件,為了解析此檔案,我們需要引入一個基於 MPL 協議(寬鬆,可商業,無須開源)的 Open MCDF 庫,這是一個完全由 C# 實現的讀取 OLE 格式文件的庫,在我做 VisualStudio 外掛時也用到,請看 dotnet Roslyn 通過讀取 suo 檔案獲取解決方案的啟動專案
在 csproj 上新增如下程式碼進行安裝 Open MCDF 庫
<PackageReference Include="OpenMcdf" Version="2.2.1.9" />
當前的 csproj 專案檔案程式碼如下
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<None Update="Test.pptx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="dotnetCampus.OpenXmlUnitConverter" Version="1.7.1" />
<PackageReference Include="DocumentFormat.OpenXml" Version="2.13.1" />
<PackageReference Include="OpenMcdf" Version="2.2.1.9" />
</ItemGroup>
</Project>
儘管在 Open MCDF 庫提供了 CompoundFile 的建構函式可以傳入 Stream 物件,但是因為在 OpenXML 的 Part 取出的 Stream 是不可隨機訪問的(為了解決 N 多的坑,在 System.IO.Packaging 限制)因此以下程式碼是不可用的
var compoundFile = new CompoundFile(part.GetStream(FileMode.Open));
執行上面程式碼將會提示 OpenMcdf.CFException:“Cannot load a non-seekable Stream”
而失敗
為了使用 Open MCDF 庫讀取,需要先存放到本地檔案,程式碼如下
var tempFolder = System.IO.Path.GetTempPath();
var oleFile = System.IO.Path.Combine(tempFolder, System.IO.Path.GetRandomFileName());
using (var fileStream = File.OpenWrite(oleFile))
{
using var stream = part.GetStream(FileMode.Open);
stream.CopyTo(fileStream);
}
開啟此 OLE 檔案程式碼如下
var compoundFile = new CompoundFile(oleFile);
從此 OLE 檔案讀取出 xlsx 檔案的程式碼如下
var packageStream = compoundFile.RootStorage.GetStream("Package");
var xlsxFile = System.IO.Path.Combine(tempFolder, System.IO.Path.GetRandomFileName()+".xlsx");
using (var fileStream = File.OpenWrite(xlsxFile))
{
fileStream.Write(packageStream.GetData().AsSpan());
}
在獲取到 xlsxFile 檔案之後,即可進行 Excel 解析,讀取裡面的資訊
using var spreadsheetDocument = SpreadsheetDocument.Open(xlsxFile,false);
var sheets = spreadsheetDocument.WorkbookPart!.Workbook.Sheets;
更多讀取 Excel 的方法請看 C# dotnet WPF 使用 OpenXml 解析 Excel 檔案
本文不再詳細告訴大家如何讀取此 Excel 內容
本文以上的測試檔案和程式碼放在github 和 gitee 歡迎訪問
可以通過如下方式獲取本文的原始碼,先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 976b039620120286bed59eda5363a87b592941ca
以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
獲取程式碼之後,進入 Pptx 資料夾
更多請看 Office 使用 OpenXML SDK 解析文件部落格目錄
更多參考:
- [MS-OFFDI].pdf
- [MS-XLS].pdf
- [MS-OI 29500].pdf