【问题标题】:Most efficient way to process a large csv in .NET在 .NET 中处理大型 csv 的最有效方法
【发布时间】:2012-12-29 20:04:24
【问题描述】:

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

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

2) 如何有效地从按字母顺序排列的列表中查找内容(利用排序的事实来加快速度,而不是搜索整个列表)

【问题讨论】:

  • 是否需要每次都重新加载文件,还是可以缓存在内存中,比如字典或哈希表中?
  • 如果您不想编写 CSV 解析器,可以尝试 FileHelpers。请告诉我们这是否是一个特定的点解决方案,或者您是否需要一个通用的阅读器。实际上,您的问题有点……未指定。
  • @Steven Doggart 只要在某个时间点加载——数据就不会改变。
  • 是的,我会查看 FileHelpers 或 Sebastian Lorien 的 Fast CSV Reader.。这是其他人已经很好解决的问题之一。
  • FileHelpers?为什么不直接使用属于 .NET 框架的 TextFieldParser 类?

标签: c# .net vb.net search csv


【解决方案1】:

为了工作,我写得很快,可以改进...

定义列号:

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();

返回一个强类型对象列表

【讨论】:

    【解决方案2】:

    如果您在每个程序运行时只执行一次,这似乎相当快。 (更新为使用 StreamReader 而不是基于下面 cmets 的 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;
        }
    

    对于 50 兆的 600,000 条记录测试文件,这将在 0.007 秒内运行。相比之下,文件扫描平均超过半秒,具体取决于记录所在的位置。 (相差 100 倍)

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

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

    【讨论】:

    • 那么为什么不使用 System.IO.StreamReader?!
    • @Adriano 我很确定 StreamReader 由于缓冲而无法正确搜索。根据一些谷歌搜索,它似乎不值得尝试。
    • 您将 FileStream “包装”到 StreamReader 中,并且您只能通过 StreamReader 访问底层流。当然它可以工作(它也通过 BOM 管理编码)。
    • @Adriano 看起来你是对的。但只有当我在每个循环中调用StreamReader.DiscardBufferedData() 时。注意:如果您将文件名传递给 StreamReader,则默认情况下会包装 FileStream。搜索需要 BaseStream.Position 和 Length。
    • 我不明白为什么你必须丢弃缓冲区,你用 ReadLine() 循环行,然后停止你正在寻找的那个(为什么要寻找?)
    【解决方案3】:

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

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

    Dictionary<string, string[]> data = new Dictionary<string, string[]>();
    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"];
    

    【讨论】:

    • 如果你有你正在寻找的确切密钥,这将起作用。
    • @RobertHarvey 正确。我只是根据原始问题中给出的要求来回答:“我需要确定给定输入,csv 中的任何行是否以该输入开头”。如果这就是所有需要做的,在我看来字典是一个很好的解决方案。
    • 谢谢,我对字典对象没有任何经验,但我会调查一下。
    • 我对这个要求的理解是不同的。可能是输入跨列,也可能是输入只是列的一部分。
    • @ebyrob 是的。我假设“开始于”是指特定数量的列,很可能只是第一列。
    【解决方案4】:

    通常我会建议找到一个专用的 CSV 解析器(如 thisthis)。但是,我在您的问题中注意到了这一行:

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

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

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

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

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

    最后,如果您仍然没有达到性能目标,我们将进入编写低级代码的领域,以便使用 seek 调用对您的文件流进行二进制搜索。就性能而言,这可能是您能做的最好的事情,但编写代码会非常混乱且容易出错,因此如果您绝对没有达到前面步骤中的目标,您只想去这里.

    请记住,性能是一项功能,就像任何其他功能一样,您需要根据实际设计目标来评估您为该功能构建的方式。 “尽可能快”不是一个合理的设计目标。像“在 0.25 秒内响应用户搜索”这样的设计目标是真正的设计目标,如果更简单但速度较慢的代码仍然满足该目标,则需要停止。

    【讨论】:

      【解决方案5】:

      OP 说真的只需要根据行搜索。

      接下来的问题就是要不要记住这些台词。

      如果线路为 1 k,则内存为 300 mb。
      如果一行是 1 兆,那么 300 GB 的内存。

      Stream.Readline 将具有低内存配置文件
      由于它已排序,因此一旦大于,您就可以停止查找。

      如果你把它放在内存中,那么一个简单的

      List<String> 
      

      使用 LINQ 即可。
      LINQ 不够聪明,无法利用这种排序,但针对 300K 仍然会很快。

      BinarySearch 将利用排序。

      【讨论】:

        【解决方案6】:

        这是我的 VB.net 代码。它适用于 Quote Qualified 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
        

        【讨论】:

          【解决方案7】:

          你没有给出足够的细节来给你一个具体的答案,但是......


          如果 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<long, string> Rows = new Dictionar<long, string>();
          ...
          if(Rows.ContainsKey(search)) ...
          

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

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

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

          【讨论】:

          • 感谢代码 sn-ps,这真的很有帮助!我会尝试不同的方法,看看哪种方法最快。
          【解决方案8】:

          鉴于 CSV 已排序 - 如果您可以将整个内容加载到内存中(如果您需要做的唯一处理是每行上的 .StartsWith() ) - 您可以使用Binary search 进行异常快速的搜索.

          可能是这样的(未经测试!):

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

          ...

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

          【讨论】:

          • 实际上,您不必将其加载到内存中即可对文件进行二分搜索:blog.sarah-happy.ca/2010/04/… 但是,对于文件,btree 搜索会更快,但这可能是比它的价值更多的工作。 (需要磁盘几何知识)
          • 这将涉及将整个内容加载到列表中,对吗?我不确定除了 List.BinarySearch Method 之外如何在 .NET 中进行二进制搜索
          • 如果您没有将打开的文件读入内存,您将不得不流过(最多)50% 的行 - 这听起来会慢得多(但 obv . 使用更少的内存)
          • @ebyrob 谢谢,我会想办法把它转换成 C#,我不知道 java
          • @DaveBish System.IO.FileStream.Seek() 是读取整个文件内容还是可以跳到文件的缓冲部分?
          【解决方案9】:

          如果您的文件在内存中(例如因为您进行了排序)并且您将其保存为字符串(行)数组,那么您可以使用简单的二分搜索 方法。您可以从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&lt;T&gt;.BinarySearch()),但这远非最佳(您将阅读所有内容即使您可能只需要检查几行)并且文件本身甚至可能太大。

          如果您真的需要更多的东西,并且由于排序而没有将文件保存在内存中(但您应该分析与您的要求相比的实际性能),您有实现更好的搜索算法,例如Boyer-Moore algorithm

          【讨论】:

          • 谢谢,我已经对文件进行了排序,因此将其加载到内存中是一个额外的步骤。我会看一下 CodeReview 链接。
          【解决方案10】:

          免费试用CSV Reader。无需一遍又一遍地发明轮子;)

          1) 如果您不需要存储结果,只需遍历 CSV - 处理每一行并忘记它。如果您需要一次又一次地处理所有行,请将它们存储在 List 或 Dictionary 中(当然要使用好键)

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2012-02-23
            • 1970-01-01
            • 1970-01-01
            • 2011-12-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多