【问题标题】:Make file IO with LINQ more efficient with large numbers of small XML files?使用大量小型 XML 文件使用 LINQ 提高文件 IO 的效率?
【发布时间】:2013-08-27 15:02:10
【问题描述】:

我有一批大约 13,000 个 XML 文件(每天可能增长数百个),我需要使用 LINQ 过滤和将数据转换为我需要的数据进行处理,并将七种可能的事件类型中的每一种聚合到一个单一的事件类型文件(见下文)。所以,13k 文件分成 7 个文件。事件类型在 XML 中被很好地描述,因此过滤和聚合相对容易。然后,这些聚合文件将用于使用我已经编写的脚本为我们的数据库创建一个 MySQL 插入语句,该脚本也可以正常工作。

我有功能代码,它正在处理文件,但到目前为止它已经运行了 23 多个小时,看起来可能只完成了大约一半(?)。我忽略了放入文件计数器,所以我真的不知道,我不愿意再次重新启动它。我可以根据原始文件的大小(360mb 左右)与处理后的文件大小(180mb 左右)来做出有根据的猜测。我预计可能要运行大约六次,直到我们转储这种数据收集方法(使用 XML 文件作为数据库)并过渡到只使用 MySQL,所以我希望我能找到一种更有效的方法来处理文件。如果没有必要,我真的不想每次执行都花费 2 天以上的时间。

它在我的机器上本地运行,但仅在 1 HD(我认为是 10k RPM 梭子鱼)上运行。从一个驱动器读取并写入单独的驱动器可能会更快吗?我很确定我的瓶颈是由文件 IO 引起的,我正在打开和关闭文件数千次。也许我可以重构只打开一次阅读并在内存中做所有事情?我知道这会更快,但如果出现问题,我可能会丢失整个文件的数据。我仍然必须打开每个 13k 文件来读取它们、处理它们并写出到 XElement。

这是我正在运行的代码。我正在使用 LINQPad 并将代码作为 C# 语句运行,但如有必要,我可以将其转换为真正的可执行文件。 LINQPad 对于这样的原型制作非常方便!请让我知道 XML 的示例是否会使这更容易理解,但乍一看,它似乎并不密切。文件大小从 2k 到 285k 不等,但只有 300 个左右超过 100k,大多数在 25 - 50k 范围内。

string sourceDir = @"C:\splitXML\results\XML\";//source for the 13k files
string xmlDestDir = @"C:\results\XMLSorted\";//destination for the resultant 7 files
List<string> sourceList = new List<string>();
sourceList = Directory.EnumerateFiles(sourceDir, "*.xml", SearchOption.AllDirectories).ToList();
string destFile = null;
string[] events = { "Creation", "Assignment", "Modification", "Repair", "RepairReview", "Termination", "Test" };
foreach(string eventItem in events)
{
try
{
        //this should only happen once the first time through and 
        //shouldn't be a continuing problem
        destFile = Path.Combine(xmlDestDir, eventItem + "Uber.xml");
    if (!File.Exists(destFile))
    {
        XmlTextWriter writer = new XmlTextWriter( destFile, null );
        writer.WriteStartElement( "PCBDatabase" );
        writer.WriteEndElement();
        writer.Close();
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex);
}
}

foreach(var file in sourceList) //roughly 13k files
{
    XDocument xd = XDocument.Load(file);    
    var actionEvents =
        from e in xd.Descendants("PCBDatabase").Elements()
    select e;
foreach(XElement actionEvent in actionEvents)
{
    //this is where I think it's bogging down, it's constant file IO
        var eventName =
    from e in actionEvents.Elements()
    select e.Name;
    var eventType = eventName.First();
    destFile = Path.Combine(xmlDestDir, eventType + "Uber.xml");
        //another bottle neck opening each file thousands of times
    XElement xeDoc = XElement.Load(destFile);
    xeDoc.Add(actionEvent);
        //and last bottle neck, closing each file thousands of times
        xeDoc.Save(destFile);
    }
}

【问题讨论】:

  • 写入结果文件(更重要的是,每次你想添加一个元素时都加载它)确实是让你死的原因。将您想要写入的所有数据存储在内存中也是有问题的,如果没有其他原因,那么您可能没有没有足够的内存来执行此操作。你需要一个中间立场,这意味着批处理。读入几百个元素,将它们存储在内存中的结构中,然后一旦它变得足够大(尝试更改批处理大小以查看最有效的方法)将它们全部写入输出文件。跨度>
  • 在这里大声思考,但我想知道如果您将Uber 设为MemoryMappedFile,您是否会获得更好的性能。 MMF 上的一些 more info
  • 感谢大家的回答,我会尝试实施您的想法,看看我能得到什么样的效率改进。 FWIW,我有一台 8 proc XEON 3.33 GHz,16GB 机器(专门为这个项目购买的正是这种类型的问题)
  • 您是否总是希望附加到“Uber”xml 文件中?使用您的代码,Uber 文件仅在第一次创建,然后每次您运行它时,所有内容都会再次附加。这似乎很奇怪。我原以为您要么每次都想覆盖它们,要么只将新项目附加到 uber xml 文件中。
  • @mikez 是的,我总是需要追加,而不是写入整个文件。我错过了像 xeDoc.Append(destFile); 这样愚蠢的东西吗? ?

标签: c# xml linq file-io


【解决方案1】:

您花费大量时间重新打开 xml 文件并将它们解析为 XDocument 对象。由于这些 Uber 文件将非常大,您要做的就是打开它们一次并以仅向前的方式写入。下面的代码是您将如何进行的示例。我还将eventType 移出内部循环(因为它不依赖于内部循环变量)。

请注意,此示例每次都会从头开始重新创建 Uber 文件。如果这不是您要做的,我建议不要将它们读入XDocument,而是使用下面的代码创建“临时”文件,然后使用两个XmlReader 实例来读取文件并将内容与XmlWriter.

using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Linq;

public static void Main(string[] args)
{
    string sourceDir = @"C:\splitXML\results\XML\";
    string xmlDestDir = @"C:\results\XMLSorted\";
    string[] events = { "Creation", "Assignment", "Modification", "Repair", "RepairReview", "Termination", "Test" };
    Dictionary<string, XmlWriter> writers = events.ToDictionary(e => e, e => XmlWriter.Create(Path.Combine(xmlDestDir, e + "Uber.xml")));

    foreach(var writer in writers.Values)
    {
        writer.WriteStartDocument();
        writer.WriteStartElement("PCBDatabase");
    }

    foreach(var file in Directory.EnumerateFiles(sourceDir, "*.xml", SearchOption.AllDirectories)) //roughly 13k files
    {
        XDocument xd = XDocument.Load(file);    
        var actionEvents = from e in xd.Descendants("PCBDatabase").Elements() select e;
        string eventType = (from e in actionEvents.Elements() select e.Name.ToString()).First();

        foreach(XElement actionEvent in actionEvents)
        {
            actionEvent.WriteTo(writers[eventType]);
        }    
    }

    foreach(var writer in writers.Values)
    {
        writer.WriteEndElement();
        writer.WriteEndDocument();
        writer.Close();
    }            
}

【讨论】:

  • 哇,哇。你的方法不到两分钟就写出了整个300+GB的数据结构!我必须做一些细微的调整,因为我希望数据是人类可读的,所以我在 XMLWriter 中添加了一些格式。从所有建议中可以学到很多东西,但是我选择了您的解决方案,因为我能够立即实施它并且它的工作效果比我的要好得多(当我在测试后杀死它以运行您的解决方案时,它已经运行了近 30 个小时)多次针对较小的数据集)。感谢您的帮助,我真的很感激,我学到了一些很酷的新 LINQ 用法。
  • 事实证明,在过去的几天里,我不得不运行数十次(错误数据)。没有您的解决方案,我将成为 SOL。只是想再次说声谢谢。
  • @delliottg 很高兴我能提供帮助。
【解决方案2】:

你已经做了一个经典的反模式:Schlemiel the Painter

对于每个文件,您都重新读取其中一个 uber XML,对其进行修改并完全重新编写...因此,您已经处理的文件越多,处理新文件的速度就越慢。考虑到文件的总大小,将 uber 文件保存在内存中并仅在进程结束时写入它们可能会更好。

另一种可能的解决方案是保持打开各种XmlWriter(s),每个 uber 文件一个,然后写入它们。它们是基于流的,因此您可以随时追加新项目,如果它们由 FileStream 支持,这些编写器将保存到文件中。

【讨论】:

  • 考虑到他正在处理的数据量,他可能没有没有足够的内存来执行此操作。
  • @Servy 添加了另一个解决方案。
  • 哈! Schlemiel the Painter 正是我所做的!谢谢你的轶事。
  • @xanatos 感谢您的帮助,我很感激,尤其是这个故事(今天早上我能够在站立时使用它来让我的团队笑)。
【解决方案3】:

写入结果文件(更重要的是,每次您想添加元素时都加载它)确实是要杀死您的原因。将您想要写入的所有数据存储在内存中也是有问题的,如果没有其他原因,那么您可能没有足够的内存来执行此操作。你需要一个中间立场,这意味着批处理。读入几百个元素,将它们存储在内存中的结构中,然后一旦它变得足够大(尝试更改批处理大小以查看最有效的方法)将它们全部写入输出文件。

因此,我们将从批处理 IEnumerableBatch 函数开始:

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
{
    List<T> buffer = new List<T>(batchSize);

    foreach (T item in source)
    {
        buffer.Add(item);

        if (buffer.Count >= batchSize)
        {
            yield return buffer;
            buffer = new List<T>(batchSize);
        }
    }
    if (buffer.Count >= 0)
    {
        yield return buffer;
    }
}

接下来,您正在使用的查询实际上可以重构以更有效地使用 LINQ。您有几个选择实际上并没有做任何事情,并且可以使用 SelectMany 而不是显式的 foreach 循环将它们全部拉入一个查询中。

var batchesToWrite = sourceList.SelectMany(file =>
        XDocument.Load(file).Descendants("PCBDatabase").Elements())
    .Select((element, index) => new
    {
        element,
        index,
        file = Path.Combine(xmlDestDir, element.Elements().First().Name + "Uber.xml"),
    })
    .Batch(batchsize)
    .Select(batch => batch.GroupBy(element => element.file));

然后只需写出每个批次:

foreach (var batch in batchesToWrite)
{
    foreach (var group in batch)
    {
        WriteElementsToFile(group.Select(element => element.element), group.Key);
    }
}

至于实际将元素写入文件,我已将其提取到一个方法中,因为可能有不同的方式来编写您的输出。你可以从你正在使用的实现开始,看看你是怎么做的:

private static void WriteElementsToFile(IEnumerable<XElement> elements, string path)
{
    XElement xeDoc = XElement.Load(path);
    foreach (var element in elements)
        xeDoc.Add(element);
    xeDoc.Save(path);
}

但是您仍然有一个问题,即您在整个输入文件中读取只是为了将元素附加到末尾。单独的批处理可能已经为您的目的减轻了这一点,但如果没有,您可能希望单独解决此方法,可能使用 LINQ to XML 以外的其他方法来编写结果,这样您就不需要加载整个文件到内存中只是为了创建这个文档。

【讨论】:

  • 感谢您的帮助,当 mikez 提出他的解决方案时,我正在研究您的建议,试图让它们发挥作用,而我几乎可以立即实施。我也计划研究你的方法,因为我认为其中有一些很好的概念可供我学习,尤其是我以前从未见过的 SelectMany LINQ。再次感谢。
猜你喜欢
  • 1970-01-01
  • 2012-06-08
  • 1970-01-01
  • 2020-06-29
  • 1970-01-01
  • 2017-03-25
  • 1970-01-01
  • 1970-01-01
  • 2011-02-20
相关资源
最近更新 更多