4

我有一批大约 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);
    }
}
4

3 回答 3

2

您已经完成了一个经典的反模式:Schlemiel the Painter

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

另一种可能的解决方案是保持打开各种XmlWriter(s),一个用于每个超级文件,并写入它们。它们是基于流的,因此您始终可以附加新项目,并且如果它们由 a 支持FileStream,这些编写器将保存到文件中。

于 2013-08-27T15:10:27.687 回答
2

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

因此,我们将从Batch批量输出的函数开始IEnumerable

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 以外的其他方法来编写结果,这样您就不需要加载整个文件到内存中只是为了创建这个文件。

于 2013-08-27T15:29:58.963 回答
2

您正在花费大量时间重新打开 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();
    }            
}
于 2013-08-27T18:32:38.827 回答