使用 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 文件。 我尝试将XmlWriterXmlReader 一起使用,但无法使其正常工作。有什么想法吗?

更新:

我尝试使用 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,但它仍然很慢。

有什么想法吗?

【问题讨论】:

您可以进行从XmlReaderXmlWriter 的流式转换,例如如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 读取属性值

在 XmlReader .NET 4.0 中加载失败目录文件

PHP XMLReader 读取、编辑节点、编写 XMLWriter

php xml 文件读取 XMLReader

XmlReader - 如何在没有 System.OutOfMemoryException 的情况下读取元素中很长的字符串

XMLReader 是 SAX 解析器、DOM 解析器,还是两者都不是?