本主题演示如何使用 Open XML SDK for Office 以编程方式复制使用 SAX (简单 API for XML) 的大型工作表。 有关文档的基本结构 SpreadsheetML 的详细信息,请参阅 SpreadsheetML 文档的结构。
为何使用 SAX 方法?
Open XML SDK 提供两种分析 Office Open XML 文件的方法:文档对象模型 (DOM) 和简单 API for XML (SAX) 。 DOM 方法旨在使用强类型类轻松查询和分析 Open XML 文件。 但是,DOM 方法需要将整个 Open XML 部件加载到内存中,这可能会导致处理速度变慢, Out of Memory 并在处理非常大的部件时出现异常。
SAX 方法一次在 Open XML 部件中读取一个元素中的 XML,而无需将整个部分读取到内存中,从而提供对 XML 数据的非缓存、仅向前访问权限,这使得它在读取非常大的部分(例如具有数十万行的 ) WorksheetPart 时是更好的选择。
使用 DOM 方法
使用 DOM 方法,我们可以利用 Open XML SDK 的强类型类。 第一步是访问包的 WorksheetPart 并确保它不为 null。
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(path, true))
{
// Get the first sheet
WorksheetPart? worksheetPart = spreadsheetDocument.WorkbookPart?.WorksheetParts?.FirstOrDefault();
if (worksheetPart is not null)
确定 WorksheetPart 要复制的 不为 null 后,请添加一个新的 WorksheetPart 以将其复制到其中。
然后克隆 的 WorksheetPartWorksheet ,并将克隆Worksheet的 分配给新的 WorksheetPart工作表 属性。
// Add a new WorksheetPart
WorksheetPart newWorksheetPart = spreadsheetDocument.WorkbookPart!.AddNewPart<WorksheetPart>();
// Make a copy of the original worksheet
Worksheet newWorksheet = (Worksheet)worksheetPart.Worksheet.Clone();
// Add the new worksheet to the new worksheet part
newWorksheetPart.Worksheet = newWorksheet;
此时, WorksheetPart 新已添加,但必须将新 Sheet 元素添加到 WorkbookPart的 Sheets子元素中才能显示。 为此,请首先查找新的 WorksheetPartID,并通过将计数递增 Sheets 1 来创建新的工作表 ID,然后将新的 Sheet 子级追加到 Sheets 元素。 这样,复制的工作表将添加到文件中。
// Find the new WorksheetPart's Id and create a new sheet id
string id = spreadsheetDocument.WorkbookPart.GetIdOfPart(newWorksheetPart);
uint newSheetId = (uint)(sheets!.ChildElements.Count + 1);
// Append a new Sheet with the WorksheetPart's Id and sheet id to the Sheets element
sheets.AppendChild(new Sheet() { Name = "My New Sheet", SheetId = newSheetId, Id = id });
使用 SAX 方法
SAX 方法适用于部件,因此使用 SAX 方法时,第一步是相同的。 访问包的 WorksheetPart 并确保它不为 null。
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(path, true))
{
// Get the first sheet
WorksheetPart? worksheetPart = spreadsheetDocument.WorkbookPart?.WorksheetParts?.FirstOrDefault();
if (worksheetPart is not null)
使用 SAX 时,我们无权访问 Clone 方法。 因此,请改为从向 中添加一个新的 WorksheetPartWorkbookPart开始。
WorksheetPart newWorksheetPart = spreadsheetDocument.WorkbookPart!.AddNewPart<WorksheetPart>();
然后, OpenXmlPartReader 使用原始工作表部件创建 实例, OpenXmlPartWriter 使用新创建的工作表部件创建 实例。
using (OpenXmlReader reader = OpenXmlPartReader.Create(worksheetPart))
using (OpenXmlWriter writer = OpenXmlPartWriter.Create(newWorksheetPart))
然后使用 方法逐个 Read 读取元素。 如果元素是内部 CellValue 文本,则需要使用 GetText 方法显式添加以读取文本,因为 WriteStartElement 不会写入元素的内部文本。 对于其他元素,我们只需要使用 WriteStartElement 方法,因为我们不需要其他元素的内部文本。
// Write the XML declaration with the version "1.0".
writer.WriteStartDocument();
// Read the elements from the original worksheet part
while (reader.Read())
{
// If the ElementType is CellValue it's necessary to explicitly add the inner text of the element
// or the CellValue element will be empty
if (reader.ElementType == typeof(CellValue))
{
if (reader.IsStartElement)
{
writer.WriteStartElement(reader);
writer.WriteString(reader.GetText());
}
else if (reader.IsEndElement)
{
writer.WriteEndElement();
}
}
// For other elements write the start and end elements
else
{
if (reader.IsStartElement)
{
writer.WriteStartElement(reader);
}
else if (reader.IsEndElement)
{
writer.WriteEndElement();
}
}
}
此时,工作表部件已复制到新添加的部件,但与 DOM 方法一样,我们仍需要向 的 Sheets 元素添加 。SheetWorkbook 由于 SAX 方法提供对 XML 数据的非缓存、 仅向前 访问权限,因此只能在元素子元素前面添加,在这种情况下,这会将新工作表添加到开头而不是末尾,从而更改工作表的顺序。 因此,此处需要 DOM 方法,因为我们希望不在新的前面追加 Sheet ,并且由于 WorkbookPart 通常不是很大部分,因此性能提升将微乎其微。
Sheets? sheets = spreadsheetDocument.WorkbookPart.Workbook.GetFirstChild<Sheets>();
if (sheets is null)
{
spreadsheetDocument.WorkbookPart.Workbook.AddChild(new Sheets());
}
string id = spreadsheetDocument.WorkbookPart.GetIdOfPart(newWorksheetPart);
uint newSheetId = (uint)(sheets!.ChildElements.Count + 1);
sheets.AppendChild(new Sheet() { Name = "My New Sheet", SheetId = newSheetId, Id = id });
示例代码
下面是 DOM 和 SAX 方法的示例代码,用于将数据从一个工作表复制到新工作表并将其添加到电子表格文档。 虽然 DOM 方法更简单,并且在许多情况下是首选选择,但对于非常大的文档,SAX 方法更好,因为它速度更快,并且可以防止 Out of Memory 异常。 若要查看差异,请创建包含多个 (10,000 多个) 行的Stopwatch电子表格文档,并检查 的结果,以检查执行时间的差异。 将行数增加到 100,000 多行,以查看更显著的性能提升。
DOM 方法
void CopySheetDOM(string path)
{
Console.WriteLine("Starting DOM method");
Stopwatch sw = new();
sw.Start();
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(path, true))
{
// Get the first sheet
WorksheetPart? worksheetPart = spreadsheetDocument.WorkbookPart?.WorksheetParts?.FirstOrDefault();
if (worksheetPart is not null)
{
// Add a new WorksheetPart
WorksheetPart newWorksheetPart = spreadsheetDocument.WorkbookPart!.AddNewPart<WorksheetPart>();
// Make a copy of the original worksheet
Worksheet newWorksheet = (Worksheet)worksheetPart.Worksheet.Clone();
// Add the new worksheet to the new worksheet part
newWorksheetPart.Worksheet = newWorksheet;
Sheets? sheets = spreadsheetDocument.WorkbookPart.Workbook.GetFirstChild<Sheets>();
if (sheets is null)
{
spreadsheetDocument.WorkbookPart.Workbook.AddChild(new Sheets());
}
// Find the new WorksheetPart's Id and create a new sheet id
string id = spreadsheetDocument.WorkbookPart.GetIdOfPart(newWorksheetPart);
uint newSheetId = (uint)(sheets!.ChildElements.Count + 1);
// Append a new Sheet with the WorksheetPart's Id and sheet id to the Sheets element
sheets.AppendChild(new Sheet() { Name = "My New Sheet", SheetId = newSheetId, Id = id });
}
}
sw.Stop();
Console.WriteLine($"DOM method took {sw.Elapsed.TotalSeconds} seconds");
}
SAX 方法
void CopySheetSAX(string path)
{
Console.WriteLine("Starting SAX method");
Stopwatch sw = new();
sw.Start();
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(path, true))
{
// Get the first sheet
WorksheetPart? worksheetPart = spreadsheetDocument.WorkbookPart?.WorksheetParts?.FirstOrDefault();
if (worksheetPart is not null)
{
WorksheetPart newWorksheetPart = spreadsheetDocument.WorkbookPart!.AddNewPart<WorksheetPart>();
using (OpenXmlReader reader = OpenXmlPartReader.Create(worksheetPart))
using (OpenXmlWriter writer = OpenXmlPartWriter.Create(newWorksheetPart))
{
// Write the XML declaration with the version "1.0".
writer.WriteStartDocument();
// Read the elements from the original worksheet part
while (reader.Read())
{
// If the ElementType is CellValue it's necessary to explicitly add the inner text of the element
// or the CellValue element will be empty
if (reader.ElementType == typeof(CellValue))
{
if (reader.IsStartElement)
{
writer.WriteStartElement(reader);
writer.WriteString(reader.GetText());
}
else if (reader.IsEndElement)
{
writer.WriteEndElement();
}
}
// For other elements write the start and end elements
else
{
if (reader.IsStartElement)
{
writer.WriteStartElement(reader);
}
else if (reader.IsEndElement)
{
writer.WriteEndElement();
}
}
}
}
Sheets? sheets = spreadsheetDocument.WorkbookPart.Workbook.GetFirstChild<Sheets>();
if (sheets is null)
{
spreadsheetDocument.WorkbookPart.Workbook.AddChild(new Sheets());
}
string id = spreadsheetDocument.WorkbookPart.GetIdOfPart(newWorksheetPart);
uint newSheetId = (uint)(sheets!.ChildElements.Count + 1);
sheets.AppendChild(new Sheet() { Name = "My New Sheet", SheetId = newSheetId, Id = id });
sw.Stop();
Console.WriteLine($"SAX method took {sw.Elapsed.TotalSeconds} seconds");
}
}
}