原谅我的菜鸟,但我只需要一些指导,我找不到另一个可以回答这个问题的问题。我有一个相当大的 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<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];
}
注意代码是在浏览器窗口中编写的,并且可能包含语法错误,因为它没有经过测试。
如果您可以将数据缓存在内存中,并且只需要在一个主键列上搜索列表,我建议将数据作为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;
}
对于 50 兆的 600,000 条记录测试文件,这将在 0.007 秒内运行。相比之下,文件扫描平均超过半秒,具体取决于记录所在的位置。(相差100倍)
显然,如果你不止一次这样做,缓存会加快速度。进行部分缓存的一种简单方法是保持 StreamReader 处于打开状态并重新使用它,每次只需重置最小值和最大值。这将节省您在内存中存储 50 兆的时间。
编辑:添加了 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,则内存为 300 mb。
如果一行是 1 兆,那么 300 GB 的内存。
Stream.Readline 将具有低内存配置
由于它已排序,一旦它大于,您就可以停止查找。
如果你把它放在内存中,那么一个简单的
List<String>
使用 LINQ 即可。
LINQ 不够聪明,无法利用这种排序,但针对 300K 仍然会非常快。
BinarySearch 将利用排序。
试用免费的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"));
这是我的 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 解析器(如this或this)。但是,我在您的问题中注意到了这一行:
我需要确定给定输入,csv 中的任何行是否以该输入开头。
这告诉我,在确定之前花费在解析 CSV 数据上的计算机时间是浪费时间。您只需要代码来简单地匹配文本,您可以通过字符串比较轻松地做到这一点。
此外,您提到数据已排序。这应该可以让您大大加快速度……但是您需要注意,要利用这一点,您需要编写自己的代码来对低级文件流进行搜索调用。这将是迄今为止您表现最好的结果,但它也需要最初始的工作和维护。
我推荐一种基于工程的方法,您可以在其中设定性能目标,构建相对简单的东西,然后根据该目标衡量结果。特别是,从我上面发布的第二个链接开始。那里的 CSV 阅读器一次只能将一条记录加载到内存中,因此它应该表现得相当好,而且很容易上手。构建使用该阅读器的东西,并测量结果。如果他们达到了你的目标,那就停在那里。
如果它们不符合您的目标,请调整链接中的代码,以便在阅读每一行时首先进行字符串比较(在解析 csv 数据之前),然后只做为那些行解析 csv 的工作匹配。这应该会表现得更好,但只有在第一个选项不符合您的目标时才能完成工作。准备好后,再次测量性能。
最后,如果您仍然没有达到性能目标,我们将进入编写低级代码的领域,以便使用 seek 调用对您的文件流进行二进制搜索。就性能而言,这可能是您能做的最好的事情,但编写代码会非常混乱且容易出错,因此如果您绝对没有达到前面步骤中的目标,您只想去这里.
请记住,性能是一个特性,就像任何其他特性一样,您需要评估相对于实际设计目标如何构建该特性。“尽可能快”不是一个合理的设计目标。“在 0.25 秒内响应用户搜索”之类的东西是真正的设计目标,如果更简单但速度较慢的代码仍然满足该目标,则需要停止。