在.net中处理大型csv文件的最有效方法

本文关键字:有效 方法 文件 csv net 处理 大型 | 更新日期: 2023-09-27 17:53:33

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

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

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

在.net中处理大型csv文件的最有效方法

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


如果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然后有1个数组包含你的可搜索数据(即:第一列)和另一个列表或数组包含你的行数据。然后使用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];
}

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

如果您可以在内存中缓存数据,并且您只需要在一个主键列上搜索列表,我建议将数据作为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"];

如果每次程序运行只执行一次,这看起来相当快。(根据下面的注释更新为使用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;
    }

对于一个600,000条记录的测试文件,它在0.007秒内运行。相比之下,根据记录的位置,文件扫描的平均时间超过半秒。(相差100倍)

显然,如果你做了不止一次,缓存将会加快速度。实现部分缓存的一个简单方法是保持StreamReader打开并重用它,每次都重置min和max。这将节省您在内存中存储50mb的时间。

EDIT:添加knaki02的建议修复。

给定CSV排序-如果您可以将整个内容加载到内存中(如果您需要做的唯一处理是每行上的. startswith()) -您可以使用二进制搜索来进行异常快速的搜索。

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

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

我写这个快速工作,可以改进…

定义列号:

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

返回一个强类型对象列表

如果你的文件在内存(例如,因为你做了排序),你保持它作为字符串(行)的数组,那么你可以使用一个简单的对分搜索方法。你可以从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<T>.BinarySearch()),但这是远远不是最佳的(你将读取所有内容,即使你可能只需要检查几行),文件本身甚至可能太大。

如果你真的需要更多的东西,你没有你的文件在内存中,因为排序(但你应该配置文件你的实际性能与你的需求相比),你必须实现一个更好的搜索算法,例如Boyer-Moore算法。

OP声明真的只需要按行搜索。

然后问题是记住或不记住这些行。

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

流。Readline将有一个低内存配置文件
因为它是排序的,当它大于。

时,你可以停止查找。

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

List<String> 

使用LINQ可以工作。
LINQ不够聪明,无法利用这种排序,但相对于300K来说仍然相当快。

BinarySearch将利用排序。

尝试免费的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"));

这是我的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

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

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

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

另外,您提到数据是排序的。这应该允许你极大地加快…但是您需要意识到,要利用这一点,您需要编写自己的代码来对低级文件流进行seek调用。这将是到目前为止您的最佳执行结果,但它也将到目前为止需要最多的初始工作和维护。

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

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

最后,如果您仍然没有达到性能目标,我们将进入编写低级代码的领域,使用seek调用在文件流上进行二进制搜索。在性能方面,这可能是您所能做到的最好的情况,但它将是非常混乱和易于编写的代码,因此,只有当您绝对没有达到前面步骤中的目标时,您才想要转到这里。

请记住,性能是一个特性,就像任何其他特性一样,您需要根据实际设计目标来评估如何构建该特性。"尽可能快"并不是一个合理的设计目标。像"在0.25秒内响应用户搜索"这样的东西是一个真正的设计目标,如果更简单但更慢的代码仍然满足这个目标,那么您需要停止。