在.NET中处理大型csv的最有效方法

请原谅我的愚蠢,但我只需要一些指导,我找不到另一个能够解决这个问题的问题。 我有一个相当大的csv文件(约300k行),我需要确定给定的输入,csv中的任何行是否以该输入开头。 我按字母顺序对csv进行了排序,但我不知道:

1)如何处理csv中的行 – 我应该将其作为列表/集合读取,还是使用OLEDB,嵌入式数据库或其他?

2)如何从字母顺序列表中有效地找到一些东西(使用它排序的事实来加快速度,而不是搜索整个列表)

你没有提供足够的细节给你一个具体的答案,但……


如果CSV文件经常更改,则使用OLEDB并根据您的输入更改SQL查询。

 string sql = @"SELECT * FROM [" + fileName + "] WHERE Column1 LIKE 'blah%'"; using(OleDbConnection connection = new OleDbConnection( @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + fileDirectoryPath + ";Extended Properties=\"Text;HDR=" + hasHeaderRow + "\"")) 

如果CSV文件不经常更改并且您对它运行了很多“查询”,请将其加载到内存中并每次快速搜索它。

如果希望搜索与列完全匹配,请使用字典,其中键是要匹配的列,值是行数据。

 Dictionary Rows = new Dictionar(); ... if(Rows.ContainsKey(search)) ... 

如果你想让你的搜索成为像StartsWith这样的部分匹配,那么有1个数组包含你的可搜索数据(即:第一列)和另一个包含行数据的列表或数组。 然后使用C#内置的二进制搜索http://msdn.microsoft.com/en-us/library/2cy9f6wb.aspx

 string[] SortedSearchables = new string[]; List SortedRows = new List(); ... string result = null; int foundIdx = Array.BinarySearch(SortedSearchables, searchTerm); if(foundIdx < 0) { foundIdx = ~foundIdx; if(foundIdx < SortedRows.Count && SortedSearchables[foundIdx].StartsWith(searchTerm)) { result = SortedRows[foundIdx]; } } else { result = SortedRows[foundIdx]; } 

注意代码是在浏览器窗口中编写的,可能包含语法错误,因为它未经过测试。

如果您可以将数据缓存在内存中,并且只需要在一个主键列上搜索列表,我建议将数据作为Dictionary对象存储在内存中。 Dictionary类将数据作为键/值对存储在哈希表中。 您可以使用主键列作为字典中的键,然后使用其余列作为字典中的值。 在哈希表中按键查找项目通常非常快。

例如,您可以将数据加载到字典中,如下所示:

 Dictionary data = new Dictionary(); using (TextFieldParser parser = new TextFieldParser("C:\test.csv")) { parser.TextFieldType = FieldType.Delimited; parser.SetDelimiters(","); while (!parser.EndOfData) { try { string[] fields = parser.ReadFields(); data[fields[0]] = fields; } catch (MalformedLineException ex) { // ... } } } 

然后你可以获得任何项目的数据,如下所示:

 string fields[] = data["key I'm looking for"]; 

如果你每次运行程序只执行一次,这看起来非常快。 (根据以下评论更新为使用StreamReader而不是FileStream)

  static string FindRecordBinary(string search, string fileName) { using (StreamReader fs = new StreamReader(fileName)) { long min = 0; // TODO: What about header row? long max = fs.BaseStream.Length; while (min <= max) { long mid = (min + max) / 2; fs.BaseStream.Position = mid; fs.DiscardBufferedData(); if (mid != 0) fs.ReadLine(); string line = fs.ReadLine(); if (line == null) { min = mid+1; continue; } int compareResult; if (line.Length > search.Length) compareResult = String.Compare( line, 0, search, 0, search.Length, false ); else compareResult = String.Compare(line, search); if (0 == compareResult) return line; else if (compareResult > 0) max = mid-1; else min = mid+1; } } return null; } 

这个运行时间为0.007秒,600,000记录测试文件为50兆。 相比之下,文件扫描平均超过半秒,具体取决于记录的位置。 (100倍的差异)

显然,如果你不止一次这样做,缓存会加快速度。 一种简单的部分缓存方法是保持StreamReader打开并重新使用它,每次只重置最小值和最大值。 这样可以节省您在内存中存储50兆的时间。

编辑:添加了knaki02的建议修复。

鉴于CSV已排序 – 如果您可以将整个内容加载到内存中(如果您需要执行的唯一处理是每行上的.StartsWith()) – 您可以使用二进制搜索来进行exception快速的搜索。

也许这样的事情(没试过!):

 var csv = File.ReadAllLines(@"c:\file.csv").ToList(); var exists = csv.BinarySearch("StringToFind", new StartsWithComparer()); 

 public class StartsWithComparer: IComparer { public int Compare(string x, string y) { if(x.StartsWith(y)) return 0; else return x.CompareTo(y); } } 

如果您的文件在内存中 (例如,因为您进行了排序)并将其保存为字符串(行)数组,那么您可以使用简单的二分搜索方法。 您可以从CodeReview上的此问题的代码开始,只需将比较器更改为使用string而不是int并仅检查每行的开头。

如果您每次都必须重新读取文件,因为它可能会被更改或者由另一个程序保存/排序,那么最简单的算法是最好的算法:

 using (var stream = File.OpenText(path)) { // Replace this with you comparison, CSV splitting if (stream.ReadLine().StartsWith("...")) { // The file contains the line with required input } } 

当然你可以每次都读取内存中的整个文件 (使用LINQ或List.BinarySearch() ),但这远非最佳 (即使你可能需要检查几行,你也会阅读所有内容)和文件本身甚至可能太大。

如果你真的需要更多的东西,并且由于排序而没有你的文件在内存中(但你应该根据你的要求描述你的实际性能),你必须实现更好的搜索算法,例如Boyer-Moore算法 。

OP表示真的只需要基于线搜索。

然后问题就是把线条记在内存中。

如果行1 k然后300 MB的内存。
如果一行是1兆,那么300 GB的内存。

Stream.Readline将具有较低的内存配置文件
由于它已经排序,你可以在它大于时停止查看。

如果你把它放在内存中那么简单

 List 

LINQ将起作用。
LINQ不够智能,无法利用排序,但300K仍然会很快。

BinarySearch将利用这种排序。

我快速写了这篇文章,可以改进……

定义列号:

 private enum CsvCols { PupilReference = 0, PupilName = 1, PupilSurname = 2, PupilHouse = 3, PupilYear = 4, } 

定义模型

 public class ImportModel { public string PupilReference { get; set; } public string PupilName { get; set; } public string PupilSurname { get; set; } public string PupilHouse { get; set; } public string PupilYear { get; set; } } 

导入并填充模型列表:

  var rows = File.ReadLines(csvfilePath).Select(p => p.Split(',')).Skip(1).ToArray(); var pupils = rows.Select(x => new ImportModel { PupilReference = x[(int) CsvCols.PupilReference], PupilName = x[(int) CsvCols.PupilName], PupilSurname = x[(int) CsvCols.PupilSurname], PupilHouse = x[(int) CsvCols.PupilHouse], PupilYear = x[(int) CsvCols.PupilYear], }).ToList(); 

返回强类型对象的列表

试试免费的CSV阅读器 。 无需一遍又一遍地发明轮子;)

1)如果您不需要存储结果,只需迭代CSV – 处理每一行并忘记它。 如果您需要反复处理所有行,请将它们存储在列表或词典中(当然,使用好键)

2)尝试这样的通用扩展方法

 var list = new List() { "a", "b", "c" }; string oneA = list.FirstOrDefault(entry => !string.IsNullOrEmpty(entry) && entry.ToLowerInvariant().StartsWidth("a")); IEnumerable allAs = list.Where(entry => !string.IsNullOrEmpty(entry) && entry.ToLowerInvariant().StartsWidth("a")); 

这是我的VB.net代码。 它适用于引用合格的CSV,因此对于常规CSV,更改Let n = P.Split(New Char() {""","""})Let n = P.Split(New Char() {","})

 Dim path as String = "C:\linqpad\Patient.txt" Dim pat = System.IO.File.ReadAllLines(path) Dim Patz = From P in pat _ Let n = P.Split(New Char() {""","""}) _ Order by n(5) _ Select New With { .Doc =n(1), _ .Loc = n(3), _ .Chart = n(5), _ .PatientID= n(31), _ .Title = n(13), _ .FirstName = n(9), _ .MiddleName = n(11), _ .LastName = n(7), .StatusID = n(41) _ } Patz.dump 

通常我会建议找一个专用的CSV解析器(像这样或者这样 )。 但是,我在你的问题中注意到这一行:

我需要确定给定的输入,csv中的任何行是否以该输入开头。

这告诉我在确定之前计算机时间花费解析CSV数据是浪费时间。 您只需要代码来简单地匹配文本的文本,您可以通过字符串比较来做到这一点,就像其他任何东西一样容易。

此外,您提到数据已排序。 这应该可以让你大大加快速度……但你需要注意,要利用这一点,你需要编写自己的代码来对低级文件流进行搜索调用。 这将是迄今为止您表现最佳的结果,但它到目前为止还需要最初的工作和维护。

我建议使用基于工程的方法,在其中设置性能目标,构建相对简单的方法,并根据该目标测量结果。 特别是,从上面发布的第二个链接开始。 CSV读取器一次只能将一条记录加载到内存中,因此它应该运行得相当好,并且很容易上手。 构建使用该阅读器的东西,并测量结果。 如果他们达到你的目标,那就停在那里。

如果它们不符合您的目标,请调整链接中的代码,以便在读取每一行时首先进行字符串比较(在打扰解析csv数据之前),并且只进行解析csv的工作。比赛。 这应该会更好,但只有在第一个选项不符合您的目标时才能完成工作。 准备就绪后,再次测量性能。

最后,如果您仍然无法达到性能目标,我们就会编写低级代码,使用搜索调用对您的文件流进行二进制搜索。 这可能是你能做到的最好的,性能方面的,但是它会非常混乱并且容易出错,所以如果你绝对不能通过前面的步骤实现你的目标,那么你只想去这里。

请记住,性能是一项function,就像您需要评估相对于实际设计目标的该function构建方式所需的任何其他function一样。 “尽快”不是一个合理的设计目标。 像“在0.25秒内响应用户搜索”之类的东西是一个真正的设计目标,如果更简单但更慢的代码仍然符合这个目标,你需要停在那里。