10

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

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

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

4

10 回答 10

9

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


如果 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];
}

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

于 2013-01-15T17:13:48.357 回答
5

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

如果您在每个程序运行时只执行一次,这似乎相当快。(根据下面的评论更新为使用 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;
    }

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

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

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

于 2013-01-15T19:49:37.283 回答
3

鉴于 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);
    }
}
于 2013-01-15T16:53:18.327 回答
2

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

定义列号:

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

返回一个强类型对象列表

于 2015-06-25T19:34:38.087 回答
1

如果您的文件在内存中(例如因为您进行了排序)并且您将其保存为字符串(行)数组,那么您可以使用简单的二分搜索方法。您可以从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 算法

于 2013-01-15T16:56:42.320 回答
1

OP声明真的只需要基于线搜索。

然后问题是是否将这些行保留在内存中。

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

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

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

List<String> 

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

BinarySearch 将利用排序。

于 2013-01-15T17:23:17.863 回答
0

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

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

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"));
于 2013-01-15T16:55:12.387 回答
0

这是我的 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
于 2013-01-15T17:04:46.533 回答
0

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

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

这告诉我,在确定之前花费在解析 CSV 数据上的计算机时间是浪费时间。您只需要代码来简单地匹配文本,您可以通过字符串比较轻松地做到这一点。

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

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

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

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

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

于 2013-01-16T17:32:46.347 回答