解析大量 Excel 文件失败

Posted

技术标签:

【中文标题】解析大量 Excel 文件失败【英文标题】:Parsing Large List of Excel Files Failing 【发布时间】:2014-06-27 20:26:43 【问题描述】:

这是一个 C#/VSTO 程序。我一直在做一个数据采集项目。范围基本上是“处理各种第三方公司发送的 Excel 文件”。实际上,这意味着:

    通过搜索方法找到包含我想要的数据的列。 从工作簿中获取数据 清理数据、运行一些计算等 将清理后的数据输出到新工作簿中

我编写的程序非常适合中小型数据集,大约 25 个工作簿,总共有大约 1000 行相关数据。我从这些工作簿中抓取了 7 列数据。不过,我遇到的一个极端情况是,有时我需要运行一个更大的数据集,大约 50 个工作簿,总共有大约 8,000 行相关数据(可能还有大约 2000 个我还必须删除的重复数据) )。

我目前正在通过Parallel.ForEach 循环放置文件列表,在循环内部我打开new Excel.Application() 来处理具有多个ActiveSheets 的每个文件。并行过程在较小的数据集上运行比依次处理每个数据集要快得多。但在更大的数据集上,我似乎碰壁了。

我开始收到消息:Microsoft Excel is waiting for another application to complete an OLE action,最终它失败了。切换回顺序foreach 确实可以让程序完成,但它只是在磨合——从并行中型数据集的 1-3 分钟到顺序大型数据集的 20 多分钟。如果我将ParallelOptions.MaxDegreeOfParallelism 设置为 10,它将完成循环,但仍需要 15 分钟。如果我将它设置为 15,它会失败。如果我不需要的话,我也真的不喜欢弄乱 TPL 设置。我还尝试插入 Thread.Sleep 来手动减慢速度,但这只会让失败发生得更远。

我关闭工作簿,退出应用程序,然后 ReleaseComObject 到 Excel 对象,GC.CollectGC.WaitForPendingFinalizers 在每个循环结束时。

我目前的想法是:

    将列表分成两半并分别运行 并行打开一些new Excel.Application(),但在该 Excel 实例中按顺序运行文件列表(有点像 #1,但使用不同的路径) 按文件大小分隔列表,并独立/按顺序运行一小组非常大的文件,其余部分照常运行

我希望得到一些帮助的事情:

    关于确保我的记忆被清除的建议(也许Process.Id 在所有的开头和结尾都被扭曲了?) 关于订购并行流程的建议 - 我想知道是否可以先让“大”人加入,这将使运行时间更长的流程更加稳定。

我一直在查看:http://reedcopsey.com/2010/01/26/parallelism-in-net-part-5-partitioning-of-work/,他说“根据您的工作的先验知识,可能比默认分区器更有意义地对数据进行分区。”但是我很难真正知道什么/如果分区是有意义的。

非常感谢任何见解!

更新

因此,作为一般规则,我会针对 Excel 2010 进行测试,因为我们这里同时使用了 2010 和 2013。我在 2013 年运行它,它运行良好 - 运行时间大约 4 分钟,这与我预期的差不多。在我放弃 2010 兼容性之前,还有其他想法吗? 2010机器是64位机器加64位Office,2013机器是64位机器加32位Office。这有关系吗?

【问题讨论】:

我想知道死锁是不是因为你已经用尽了线程池中的所有可用线程?您可以尝试使用 ThreadPool.SetMaxThreads 增加限制,看看是否有帮助。 如果每个工作簿包含少于 10,000 行(根据您的问题定义),则要读取 50 个 Excel 文件实际上并不是什么大问题。尝试优化您的文件阅读器,特别是将 Excel 范围转换为 C# 数组;一旦您将 Excel 范围转换为 C# 数组,其余处理应该运行得非常快(通常)。 Rgds, 谢谢!刚开始工作 - 让我研究一下这两个想法并报告 @AlexBell 所以你推荐使用object[,] values = range.Value as object[,] 并从那里处理它。我的想法是让我快速获取数据并让 Excel 实例不碍事? 是的,像这样:如果你知道数据类型,那么数组可以是强类型的(例如 string[] 或 string[,] 或 string[][] 哪个是最好的适合您的情况)。 Rgds, 【参考方案1】:

几年前,我使用 excel 文件和自动化。然后我在任务管理器中有僵尸进程的问题。虽然我们的程序结束了,我认为我正确退出了 excel,但进程并没有退出。

我不喜欢这个解决方案,但它很有效。我可以这样总结解决方案。

1) 永远不要连续使用两个点,例如:

workBook.ActiveSheet.PageSetup

改为使用变量.. 完成后释放它们并将它们归零。

示例:而不是这样做:

m_currentWorkBook.ActiveSheet.PageSetup.LeftFooter = str.ToString();

遵循此功能中的做法。 (此功能在excel页脚添加条码。)

    private bool SetBarcode(string text)
    
            Excel._Worksheet sheet;
            sheet = (Excel._Worksheet)m_currentWorkbook.ActiveSheet;
            try
            
                StringBuilder str = new StringBuilder();
                str.Append(@"&""IDAutomationHC39M,Regular""&22(");
                str.Append(text);
                str.Append(")");

                Excel.PageSetup setup;
                setup = sheet.PageSetup;
                try
                
                    setup.LeftFooter = str.ToString();
                
                finally
                
                    RemoveReference(setup);
                    setup = null;
                
            
            finally
            
                RemoveReference(sheet);
                sheet = null;
            

            return true;

    

这里是 RemoveReference 函数(在这个函数中放置 null 不起作用)

    private void RemoveReference(object o)
    
        try
        
            System.Runtime.InteropServices.Marshal.ReleaseComObject(o);
        
        catch
         
        finally
        
            o = null;
        
    

如果你在任何地方都遵循这种模式,它可以保证没有泄漏、没有僵尸进程等。

2) 为了创建 excel 文件,您可以使用 excel 应用程序,但是要从 excel 中获取数据,我建议使用 OleDB。您可以像数据库一样使用 excel,并通过 sql 查询、数据表等从中获取数据。

示例代码:(可以使用datareader代替填充数据集来提高内存性能)

    private List<DataTable> getMovieTables()
    
        List<DataTable> movieTables = new List<DataTable>();
        var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + excelFilePath + ";Extended Properties=\"Excel 12.0;IMEX=1;HDR=NO;TypeGuessRows=0;ImportMixedTypes=Text\""; ;
        using (var conn = new OleDbConnection(connectionString))
        
            conn.Open();

            DataRowCollection sheets = conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, new object[]  null, null, null, "TABLE" ).Rows;

            foreach (DataRow sheet in sheets)
            

                using (var cmd = conn.CreateCommand())
                
                    cmd.CommandText = "SELECT * FROM [" + sheet["TABLE_NAME"].ToString() + "] ";

                    var adapter = new OleDbDataAdapter(cmd);
                    var ds = new DataSet();
                    try
                    
                        adapter.Fill(ds);
                        movieTables.Add(ds.Tables[0]);
                    
                    catch (Exception ex)
                    
                        //Debug.WriteLine(ex.ToString());
                        continue;
                    
                
            
        
        return movieTables;
    

【讨论】:

我有很多结构很差的数据,而且我并不总是知道我的数据在哪里,因此我的第一步是定位数据。为了确保我遵循,您的建议是我通过调用所有这些电子表格(每个文件的 connectionString 1 次迭代)来构建我的数据库/数据集,然后执行我的 4 个步骤?【参考方案2】:

作为@Mustafa Düman 提出的替代解决方案,我建议您使用Version 4 beta of EPPlus。我在几个项目中使用它没有问题。

优点:

快速 没有内存泄漏(对于版本 不需要在您使用 Office 的机器上安装 Office

缺点:

只能用于 .xlsx 文件(Excel 2007 / 2010)

我在 20 个 12.5 MB 左右的 excel 文件(每个文件中有超过 50k 条记录)上使用以下代码对其进行了测试,我认为提及它没有崩溃就足够了 :)

 Console.Write("Path: ");
 var path = Console.ReadLine();
 var dirInfo = new DirectoryInfo(path);

 while (string.IsNullOrWhiteSpace(path) || !dirInfo.Exists)
 
     Console.WriteLine("Invalid path");
     Console.Write("Path: ");
     path = Console.ReadLine();
     dirInfo = new DirectoryInfo(path);
 

 string[] files = null;
 try
 
     files = Directory.GetFiles(path, "*.xlsx", SearchOption.AllDirectories);
 
 catch (Exception ex)
 
     Console.WriteLine(ex.Message);
     Console.ReadLine();
     return;
 

 Console.WriteLine("0 files found.", files.Length);

 if (files.Length == 0)
 
     Console.ReadLine();
     return;
 

 int succeded = 0;
 int failed = 0;


 Action<string> LoadToDataSet = (filePath) =>
 
     try
     
         FileInfo fileInfo = new FileInfo(filePath);
         using (ExcelPackage excel = new ExcelPackage(fileInfo))
         using (DataSet dataSet = new DataSet())
         
             int workSheetCount = excel.Workbook.Worksheets.Count;

             for (int i = 1; i <= workSheetCount; i++)
             
                 var worksheet = excel.Workbook.Worksheets[i];

                 var dimension = worksheet.Dimension;
                 if (dimension == null)
                     continue;

                 bool hasData = dimension.End.Row >= 1;

                 if (!hasData)
                     continue;

                 DataTable dataTable = new DataTable();

                 //add columns
                 foreach (var firstRowCell in worksheet.Cells[1, 1, 1, dimension.End.Column])
                 dataTable.Columns.Add(firstRowCell.Start.Address);

                 for (int j = 0; j < dimension.End.Row; j++)
                     dataTable.Rows.Add(worksheet.Cells[j + 1, 1, j + 1, dimension.End.Column].Select(erb => erb.Value).ToArray());

                 dataSet.Tables.Add(dataTable);
             

             dataSet.Clear();
             dataSet.Tables.Clear();
         

         Interlocked.Increment(ref succeded);
     
     catch (Exception)
     
         Interlocked.Increment(ref failed);
     
 ;

 Stopwatch sw = new Stopwatch();

 sw.Start();
 files.AsParallel().ForAll(LoadToDataSet);
 sw.Stop();

 Console.WriteLine("0 succeded, 1 failed in 2 seconds", succeded, failed, sw.Elapsed.TotalSeconds);
 Console.ReadLine();

【讨论】:

嘿-我很抱歉没有在这里回复。非常酷的包。由于其他一些参数,我不能将它用于这个特定的项目,但我以后会把它放在我的后兜里。谢谢!

以上是关于解析大量 Excel 文件失败的主要内容,如果未能解决你的问题,请参考以下文章

fastadmin 中 Excel导入失败的原因

由于数字格式为文本,将 Excel 文件读取到 Python 失败

编写大型 Excel 电子表格

Ajax方式实现文件下载失败

导出到excel失败,错误(控件加载失败)

H5用安卓手机浏览器下载excel文件失败