使用 XmlReader 读取文件时更新 XLSX 文件更改
Posted
技术标签:
【中文标题】使用 XmlReader 读取文件时更新 XLSX 文件更改【英文标题】:Update XLSX file changes whilst reading the file with XmlReader 【发布时间】:2021-09-10 23:14:55 【问题描述】:我们有一个代码将 Excel XLSX 文档加载到内存中,对其进行一些修改并将其保存回来。
XmlDocument doc = new XmlDocument();
doc.Load(pp.GetStream());
XmlNode rootNode = doc.DocumentElement;
if (rootNode == null) return;
ProcessNode(rootNode);
if (this.fileModified)
doc.Save(pp.GetStream(FileMode.Create, FileAccess.Write));
这在处理小文件时效果很好,但在处理一些大型 Excel 文件时会引发 OutOfMemory 异常。所以我们决定改变方法,使用XmlReader
类来不一次将文件加载到内存中。
PackagePartCollection ppc = this.Package.GetParts();
foreach (PackagePart pp in ppc)
if (!this.xmlContentTypesXlsx.Contains(pp.ContentType)) continue;
using (XmlReader reader = XmlReader.Create(pp.GetStream()))
reader.MoveToContent();
while (reader.EOF == false)
XmlDocument doc;
XmlNode rootNode;
if (reader.NodeType == XmlNodeType.Element && reader.Name == "hyperlinks")
doc = new XmlDocument();
rootNode = doc.ReadNode(reader);
if (rootNode != null)
doc.AppendChild(rootNode);
ProcessNode(rootNode); // how can I save updated changes back to the file?
else if (reader.NodeType == XmlNodeType.Element && reader.Name == "row")
doc = new XmlDocument();
rootNode = doc.ReadNode(reader);
if (rootNode != null)
doc.AppendChild(rootNode);
ProcessNode(rootNode); // how can I save updated changes back to the file?
else
reader.Read();
这会逐个节点读取文件节点并处理我们需要的节点(并在那里更改一些值)。但是,我不确定如何将这些值更新回原始 Excel 文件。
我尝试将XmlWriter
与XmlReader
一起使用,但无法使其正常工作。有什么想法吗?
更新:
我尝试使用 cmets 部分的@dbc 建议,但对我来说似乎太慢了。对于大文件,它可能不会抛出 OutOfMemory 异常,但处理将需要很长时间。
PackagePartCollection ppc = this.Package.GetParts();
foreach (PackagePart pp in ppc)
if (!this.xmlContentTypesXlsx.Contains(pp.ContentType)) continue;
StringBuilder strBuilder = new StringBuilder();
using (XmlReader reader = XmlReader.Create(pp.GetStream()))
using (XmlWriter writer = this.Package.FileOpenAccess == FileAccess.ReadWrite ? XmlWriter.Create(strBuilder) : null)
reader.MoveToContent();
while (reader.EOF == false)
XmlDocument doc;
XmlNode rootNode;
if (reader.NodeType == XmlNodeType.Element && reader.Name == "hyperlinks")
doc = new XmlDocument();
rootNode = doc.ReadNode(reader);
if (rootNode != null)
doc.AppendChild(rootNode);
ProcessNode(rootNode);
writer?.WriteRaw(rootNode.OuterXml);
else if (reader.NodeType == XmlNodeType.Element && reader.Name == "row")
doc = new XmlDocument();
rootNode = doc.ReadNode(reader);
if (rootNode != null)
doc.AppendChild(rootNode);
ProcessNode(rootNode);
writer?.WriteRaw(rootNode.OuterXml);
else
WriteShallowNode(writer, reader); // Used from the @dbc's suggested *** answers
reader.Read();
writer?.Flush();
注意 1:我正在使用 StringBuilder 进行测试,但计划最终切换到临时文件。 注意 2:我尝试在每 100 个元素后刷新 XmlWriter,但它仍然很慢。
有什么想法吗?
【问题讨论】:
您可以进行从XmlReader
到XmlWriter
的流式转换,例如如Edit a large XML file和Automating replacing tables from external files所示。
XmlReader
是只进的,所以如果你想在必要时创建一个临时输出文件,你需要进行两次传递。
我会摆脱 writer?.WriteRaw(rootNode.OuterXml);
并使用 if (writer != null) rootNode.WriteContentTo(writer);
。 LINQ-to-XML 也比XmlDocument
快一点,所以你可以切换到那个。为什么你允许一个空的writer
?如果你什么都不写,代码的目的是什么?您是否进行了两次通过,一次检查是否会修改任何内容?如果是这样,在您第一次通过时,只要找到需要修改的节点,您就可以返回 true
。
对不起,我的错误,我应该建议XmlElement.WriteTo(XmlWriter)
。我已经习惯了使用XElement
,现在我对旧的 XML DOM 有点生疏了。
您可能应该为此提出另一个问题。有XmlWriterSettings.OmitXmlDeclaration
,因此一种选择可能是使用WriteShallowNode()
手动复制根节点之前的节点。
【参考方案1】:
尝试关注。很长一段时间以来,我一直在使用巨大的 xml 文件,这些文件会导致内存不足
using (XmlReader reader = XmlReader.Create("File Stream", readerSettings))
while (!reader.EOF)
if (reader.Name != "row")
reader.ReadToFollowing("row");
if (!reader.EOF)
XElement row = (XElement)XElement.ReadFrom(reader);
【讨论】:
这段代码和我的“只读”代码有什么区别?这如何解决更新 XML 中的值并将其保存回来的问题? 我知道我的代码解决了内存不足问题。我认为您需要创建一个新的 XDocument 并修改 XElement 行,然后添加到新的 XDocument。【参考方案2】:我在 @dbc 的帮助下做了一些修改,现在它可以按我的意愿工作了。
PackagePartCollection ppc = this.Package.GetParts();
foreach (PackagePart pp in ppc)
try
if (!this.xmlContentTypesXlsx.Contains(pp.ContentType)) continue;
string tempFilePath = GetTempFilePath();
using (XmlReader reader = XmlReader.Create(pp.GetStream()))
using (XmlWriter writer = this.Package.FileOpenAccess == FileAccess.ReadWrite ? XmlWriter.Create(tempFilePath) : null)
while (reader.EOF == false)
if (reader.NodeType == XmlNodeType.Element && reader.Name == "hyperlinks")
XmlDocument doc = new XmlDocument();
XmlNode rootNode = doc.ReadNode(reader);
if (rootNode != null)
ProcessNode(rootNode);
if (writer != null)
rootNode.WriteTo(writer);
else if (reader.NodeType == XmlNodeType.Element && reader.Name == "row")
XmlDocument doc = new XmlDocument();
XmlNode rootNode = doc.ReadNode(reader);
if (rootNode != null)
ProcessNode(rootNode);
if (writer != null)
rootNode.WriteTo(writer);
else
WriteShallowNode(writer, reader); // Used from the @dbc's suggested *** answers
reader.Read();
if (this.packageChanged) // is being set in ProcessNode method
this.packageChanged = false;
using (var tempFile = File.OpenRead(tempFilePath))
tempFile.CopyTo(pp.GetStream(FileMode.Create, FileAccess.Write));
catch (OutOfMemoryException)
throw;
catch (Exception ex)
Log.Exception(ex, @"Failed to process a file."); // our inner log method
finally
if (!string.IsNullOrWhiteSpace(tempFilePath))
// Delete temp file
【讨论】:
以上是关于使用 XmlReader 读取文件时更新 XLSX 文件更改的主要内容,如果未能解决你的问题,请参考以下文章
在 XmlReader .NET 4.0 中加载失败目录文件
PHP XMLReader 读取、编辑节点、编写 XMLWriter
XmlReader - 如何在没有 System.OutOfMemoryException 的情况下读取元素中很长的字符串