使用C#高效地识别CSV文件中的更改字段

5
这比我想象的更加困难。基本上,每天系统都会将客户主列表的快照转储为CSV格式。它包含大约120,000条记录和60个字段。大小约为25MB。不管怎样,我想报告在一个快照和另一个快照之间发生变化的值。这不是计划文件差异,因为它必须匹配最左边的列值,其中包含客户的唯一编号。行可以被插入/删除等。所有字段都是字符串,包括参考号。
我已经用LINQ编写了一个解决方案,但处理更大的数据集时会死机。对于10000条记录,需要17秒。对于120000条记录,需要近2小时来比较两个文件。现在它使用了优秀且免费的“filehelpers”http://www.filehelpers.com/ 来加载数据,这只需要几秒钟,但检测哪些记录已更改则更加棘手。以下是需要花费2小时的查询:
    var changednames = from f in fffiltered
                       from s in sffiltered
                       where f.CustomerRef == s.CustomerRef &&
                       f.Customer_Name != s.Customer_Name
                       select new { f, s };

我应该采用什么方法?我想立即将列表“修剪”到某种变化的那些人,然后对这个小子集应用更具体的比较。我的一些想法是:
a)使用字典或哈希集-尽管早期测试并没有真正显示出改进。
b)将操作分隔开-使用客户参考字段中的第一个字符,并仅与具有相同字符的人匹配。虽然这可能需要创建许多单独的集合,但似乎相当不雅。
c)远离类型化数据排列,并使用数组完成。同样,好处未知。
有什么想法吗?
谢谢!
5个回答

4

为了以下讨论的目的,我假设您已经有一种将CSV文件读入类中的方法。我将称这个类为MyRecord

将文件加载到单独的列表中,称其为NewListOldList

List<MyRecord> NewList = LoadFile("newFilename");
List<MyRecord> OldList = LoadFile("oldFilename");

也许使用LINQ可以更加优雅地完成这个任务,但是基本思路是进行一次直接合并。首先你需要对这两个列表进行排序。你的MyRecord类要么实现了IComparable接口,要么你提供自己的比较委托:

NewList.Sort(/* delegate here */);
OldList.Sort(/* delegate here */);

如果MyRecord实现了IComparable,则可以跳过委托。

现在这是一个直接的合并。

int ixNew = 0;
int ixOld = 0;
while (ixNew < NewList.Count && ixOld < OldList.Count)
{
    // Again with the comparison delegate.
    // I'll assume that MyRecord implements IComparable
    int cmpRslt = OldList[ixOld].CompareTo(NewList[ixNew]);
    if (cmpRslt == 0)
    {
        // records have the same customer id.
        // compare for changes.
        ++ixNew;
        ++ixOld;
    }
    else if (cmpRslt < 0)
    {
        // this old record is not in the new file.  It's been deleted.
        ++ixOld;
    }
    else
    {
        // this new record is not in the old file.  It was added.
        ++ixNew;
    }
}

// At this point, one of the lists might still have items.
while (ixNew < NewList.Count)
{
    // NewList[ixNew] is an added record
    ++ixNew;
}

while (ixOld < OldList.Count)
{
    // OldList[ixOld] is a deleted record
}

仅有12万条记录,执行速度应该非常快。如果合并所需的时间与从磁盘加载数据的时间一样长,我会非常惊讶。
编辑:一个LINQ解决方案
我思考如何使用LINQ来完成这个任务。我不能完全像上面的合并那样做,但我可以得到添加、删除和更改的项目分别存储在不同的集合中。 为了使其正常工作,MyRecord必须实现IEquatable并重写GetHashCode。
var AddedItems = NewList.Except(OldList);
var RemovedItems = OldList.Except(NewList);

var OldListLookup = OldList.ToLookup(t => t.Id);
var ItemsInBothLists =
    from newThing in NewList
    let oldThing = OldListLookup[newThing.Id].FirstOrDefault()
    where oldThing != null
    select new { oldThing = oldThing, newThing = newThing };

在上面的例子中,我假设MyRecord有一个唯一的Id属性。
如果您只想获取更改的项目而不是两个列表中都存在的所有项目:
var ChangedItems =
    from newThing in NewList
    let oldThing = OldListLookup[newThing.Id].FirstOrDefault()
    where oldThing != null && CompareItems(oldThing, newThing) != 0
    select new { oldThing = oldThing, newThing = newThing };

假设CompareItems方法将深度比较这两个项目,并在它们相等时返回0,如果有变化则返回非零值。

+1. 12万根本不算什么。即使每行有60个字段,总共也只有大约700万个字段。请参考我的答案中的测试代码。 - mike
我无法用言语来表达这有多酷以及我有多感激!一遍排序一直在我脑海中,但是错过记录的想法让我认为它太复杂了。但是上面就是它,优雅至极!只需要一次而不是 120,000 次有效。再次感谢 Jim。注意:未来任何人使用时,我认为 CompareTo(NewList[ixOld]) 应该更新为 NewList[ixNew]。顺便说一句,加载和比较两个 25MB 文件只需 <10 秒,你是对的,加载本身大约需要 7 秒钟。2 小时变成 2 秒 FTW! - Glinkot
事实上,那个小错误让我更加敬畏它的好处,因为显然你只是随意地编写它,没有编译/测试的好处! - Glinkot
@Glinkot:这些年我已经用10多种不同的语言写了几十次合并。这是那种在你的“技能库”中拥有的好东西。 - Jim Mischel
@Jim:...现在已经添加到我的了! :) - Glinkot
显示剩余3条评论

2

可以在数据库中完成这个任务,而不是在代码中处理。创建两个表,当前和旧的,将CSV文件中的数据导入到正确的表中,并使用一系列SQL查询来生成输出。


谢谢,我会尝试这个方法。我本来希望不必将它们导入到数据库中就能进行比较,但考虑到性能问题,我需要做些什么!我仍然很难理解为什么 LINQ 这样的东西不能使用类似于数据库的方法来执行连接操作;你会认为在内存中运行会更有优势。像索引这样的东西应该在两种情况下都可以完成。无论如何,感谢你的提示,伙计。 - Glinkot

0

延伸Jim的回答,一个基本的例子:

public class MyRecord
{
  public MyRecord(int id)
  {
    Id = id;
    Fields = new int[60];
  }

  public int Id;
  public int[] Fields;
}

然后测试代码:

var recordsOld = new List<MyRecord>();
var recordsNew = new List<MyRecord>();

for (int i = 0; i < 120000; i++)
{
  recordsOld.Add(new MyRecord(i));
  recordsNew.Add(new MyRecord(i));
}

var watch = new System.Diagnostics.Stopwatch();
int j = 0;

watch.Start();
for (int i = 0; i < recordsOld.Count; i++)
{
  while (recordsOld[i].Id != recordsNew[j].Id)
  {
    j++;
  }

  for (int k = 0; k < recordsOld[i].Fields.Length; k++)
  {
    if (recordsOld[i].Fields[k] != recordsNew[j].Fields[k])
    {
      // do your stuff here
    }
  }
}
watch.Stop();
string time = watch.ToString();

假设列表已经排序,运行时间为200毫秒。现在,我相信这段代码有很多错误,但从最基本的意义上讲,处理器执行数百万次迭代并不需要太长时间。你可能有一些复杂的比较检查,或者某些代码非常低效。


谢谢这个。是的,原始方法确实效率低下,主要是因为它的迭代性质(基本上是一个嵌套的foreach吧)。干杯! - Glinkot

0
其他人已经提供了很好的答案,我只是为您提供一些不同的考虑。
伪代码:
Read 1000 from each source.
Compare the records.
If changed, store in list of changed records.
If not changed, discard from list.
If not exists, keep in list.
Repeat until all records are exhausted.

这段代码假设记录未排序。

另一种选择是:

Read all the records and determine what are all the first characters.
Then for each character,
    Read and find records starting with that character.
    Perform comparison as necessary

一个比上面更好的改进是,如果使用的记录超过了某个阈值,就写入一个新文件。例如:
Read all the records and determine what are all the first characters and the number of occurrence.
Sort by characters with the highest occurrence.
Then for each character,
    Read and find records starting with that character.
    If number of occurrence exceed a certain limit, write records that doesn't start with the character into a new file. // this reduces the amount of data that must be read from file
    Perform comparison as necessary

1
这确实是一个有趣的方法。它有点类似于我之前想到的分隔思路,但是在尝试过这种类型的算法后,往往会因为各种if和异常而失控。感谢您提供的想法,我会记在心里的! - Glinkot

0

你从哪里导出了那个CSV?

你的原始数据来源是数据库吗?如果是,为什么不能直接对数据库运行查询?这比任何LINQ实现都更高效。


这来自一个我无法直接访问的可怕ERP系统。如果真的需要,我可以将其导入到临时表中。出于某种原因,我认为内存性能可能更好,但你可能是对的! - Glinkot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接