请原谅我的新手问题,我需要一些指导,但是我找不到其他回答这个问题的方法。我有一个相当大的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"];
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;
}
fs.ReadLine()
周围加上 if(mid==0)
可能会有所帮助,但我需要测试一下……) - user645280while (min + 1 < max)
更改为 while (min + 1 <= max)
,它就会失败。 - knaki02while( min < max )
吗? 随意更改,如果你感觉完成了但不确定,我会进行测试。 - user645280string line = fs.ReadLine(); if (line.StartsWith(search)) return line;
循环后稍微快一点的方法可能是 if(min == 0) // check first line
. - user645280也许可以尝试类似以下的代码(未经测试!):
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);
}
}
file.seek(mid); file.readLine(); line = file.readLine();
Sarah真是个聪明的女孩。 - user645280我为工作快速写下了这篇文章,可能还有改进的空间...
定义列数:
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表示只需要按行搜索。
问题是是否要将这些行保存在内存中。
如果一行大小为1k,则需要300mb的内存。
如果一行大小为1兆,则需要300gb的内存。
Stream.Readline将具有较低的内存占用率。
由于已经排序,因此一旦大于目标,可以停止搜索。
如果将其保存在内存中,则可以使用简单的
List<String>
使用LINQ可以工作。
虽然LINQ不够智能化,无法充分利用排序,但对于30万条数据仍然非常快速。
而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代码。它用于引号限定的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秒内响应用户搜索”这样的目标才是真正的设计目标,如果更简单但速度较慢的代码仍然可以满足该目标,那么您需要停止。
TextFieldParser
可以正确处理 CSV,包括多行单元格、转义等。 - Steven Doggart