簡體   English   中英

在.NET中處理大型csv的最有效方法

[英]Most efficient way to process a large csv in .NET

原諒我的愚蠢,但我只需要一些指導,我找不到另一個問題來回答這個問題。 我有一個相當大的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這樣的部分匹配,那么就有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];
}

注意代碼是在瀏覽器窗口中編寫的,可能包含語法錯誤,因為它未經過測試。

如果你每次運行程序只執行一次,這看起來非常快。 (根據以下評論更新為使用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記錄測試文件(50 megs),這在0.007秒內運行。 相比之下,文件掃描平均超過半秒,具體取決於記錄的位置。 (100倍的差異)

顯然,如果你不止一次這樣做,緩存會加快速度。 一種簡單的部分緩存方法是保持StreamReader打開並重新使用它,每次只重置min和max。 這樣可以節省您在內存中存儲50兆的時間。

編輯:添加了knaki02的建議修復。

如果您可以將數據緩存在內存中,並且只需要在一個主鍵列上搜索列表,我建議將數據作為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"];

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

如果您的文件在內存中 (例如,因為您進行了排序)並將其保存為字符串(行)數組,那么您可以使用簡單的二分搜索方法。 您可以從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將利用這種排序。

我快速寫了這篇文章,可以改進......

定義列號:

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

返回強類型對象的列表

試試免費的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代碼。 這是一個引用合格的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數據是浪費時間。 您只需要代碼來簡單地匹配文本的文本,您可以通過字符串比較來做到這一點,就像其他任何東西一樣容易。

此外,您提到數據已排序。 這應該可以讓你大大加快速度......但是你需要注意,要利用這一點,你需要編寫自己的代碼來對低級文件流進行搜索調用。 這將是迄今為止表現最佳的結果,但到目前為止還需要最初的工作和維護。

我建議使用基於工程的方法,在其中設置性能目標,構建相對簡單的方法,並根據該目標測量結果。 特別是,從上面發布的第二個鏈接開始。 CSV讀取器一次只能將一條記錄加載到內存中,因此它應該運行得相當好,並且很容易上手。 構建使用該閱讀器的東西,並測量結果。 如果他們達到你的目標,那就停在那里。

如果它們不符合您的目標,請調整鏈接中的代碼,以便在讀取每一行時首先進行字符串比較(在打擾解析csv數據之前),並且只進行解析csv的工作。比賽。 這應該會更好,但只有在第一個選項不符合您的目標時才能完成工作。 准備就緒后,再次測量性能。

最后,如果您仍然沒有達到性能目標,我們就會編寫低級代碼,使用搜索調用對您的文件流進行二進制搜索。 這可能是你能做到的最好的,性能方面的,但它會非常混亂且容易出錯的代碼,所以如果你絕對不能通過前面的步驟實現目標,你只想去這里。

請記住,性能是一項功能,就像您需要評估相對於實際設計目標構建該功能所需的任何其他功能一樣。 “盡快”不是一個合理的設計目標。 像“在0.25秒內響應用戶搜索”之類的東西是一個真正的設計目標,如果更簡單但更慢的代碼仍然符合這個目標,你需要停在那里。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM