Наиболее эффективный способ обработки большого csv in.NET

Простите мою тупость, но мне просто нужно руководство, и я не могу найти другой вопрос, который отвечает на это. У меня есть довольно большой csv-файл (~300k строк), и мне нужно определить для данного ввода, начинается ли какая-либо строка в csv с этого ввода. Я отсортировал csv в алфавитном порядке, но я не знаю:

1) Как обрабатывать строки в csv - должен ли я читать его как список/коллекцию, или использовать OLEDB, или встроенную базу данных или что-то еще?

2) как найдите что-то эффективно из алфавитного списка (используя тот факт, что он отсортирован, чтобы ускорить процесс, а не искать весь список)

10 ответов


вы не даете достаточно конкретики, чтобы дать вам конкретный ответ, но...


Если 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;
    }

это выполняется за 0.007 секунд для тестового файла записи 600,000, который составляет 50 мегабайт. Для сравнения, сканирование файлов занимает в среднем более половины секунды в зависимости от того, где находится запись. (разница в 100 раз)

очевидно, что если вы сделаете это более одного раза, кэширование ускорит процесс. Один простой способ сделать частичное кэширование должно было бы держать StreamReader открытым и повторно использовать его, просто сбрасывать min и max каждый раз. Это сэкономит вам хранение 50 мегабайт в памяти все время.

EDIT: добавлено предлагаемое исправление knaki02.


учитывая, что CSV отсортирован - если вы можете загрузить все это в память (Если единственная обработка, которую вам нужно сделать, это a .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()) каждый раз, но это далеко от оптимального (вы прочитаете все, даже если вам понадобится изучить всего несколько строк), а сам файл может быть слишком большим.

если вы действительно нужно что-то еще, и у вас нет файла в памяти из-за сортировки (но вы должны профиль ваша фактическая производительность по сравнению с вашими требованиями) вы должны реализовать лучший алгоритм поиска, например алгоритм Бойера-Мура.


OP заявил, что действительно просто нужно искать на основе линии.

вопросы тогда держать строки в памяти или нет.

если строка 1 k, то 300 Мб памяти.
Если строка 1 Мег, то 300 ГБ памяти.

поток.Readline будет иметь низкий профиль памяти
Поскольку он отсортирован, вы можете перестать смотреть, как только он больше.

Если вы держите его в памяти, то просто

List<String> 

С LINQ будет работа.
LINQ недостаточно умен, чтобы воспользоваться этим, но против 300K все равно будет довольно быстро.

ищет воспользуется рода.


Я написал это быстро для работы,можно было бы улучшить...

определить номера столбцов:

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 Reader. Нет необходимости изобретать колесо снова и снова ;)

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() {""","""}) to 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-данных, прежде чем это будет определено, это время впустую. Вам просто нужен код, чтобы просто сопоставить текст для текста, и вы можете сделать это через строку сравнение так же легко, как и все остальное.

кроме того, вы упоминаете, что данные отсортированы. Это должно позволить вам ускорить процесс чрезвычайно... но вы должны знать, что, чтобы воспользоваться этим, вам нужно будет написать свой собственный код для поиска вызовов на низкоуровневых файловых потоках. Это будет однозначно ваш лучший результат, но он также будет однозначно требуйте самых начальных работы и обслуживания.

I порекомендуйте инженерный подход, где вы устанавливаете цель производительности, строите что-то относительно простое и измеряете результаты с этой целью. В частности, начните со 2-й ссылки, которую я разместил выше. Читатель CSV будет загружать только одну запись в память за раз, поэтому он должен работать достаточно хорошо, и с ним легко начать. Создайте что-то, что использует этот читатель, и измерьте результаты. если они достигнут вашей цели, то остановитесь там.

Если они не достигайте своей цели, адаптируйте код из ссылки так, чтобы при чтении каждой строки вы сначала делали сравнение строк (прежде чем утруждать себя разбором данных csv) и выполняли только работу по разбору csv для соответствующих строк. Это должно работать лучше, но только если первый вариант не соответствует вашим целям. Когда это будет готово, измерьте производительность еще раз.

наконец, если вы все еще не достигли цели производительности, мы находимся на территории написания низкоуровневого кода, чтобы сделать двоичный поиск в потоке файлов с помощью вызовов seek. Это, вероятно, лучшее, что вы сможете сделать, с точки зрения производительности, но это будет очень грязный и подверженный ошибкам код для записи, и поэтому вы только хотите пойти сюда, если вы абсолютно не достигаете своих целей с более ранних шагов.

помните, что производительность-это функция, и, как и любая другая функция, вам нужно оценить, как вы строите для этой функции относительно реальных целей дизайна. "Как можно быстрее" не является разумной целью. Что-то как " ответить на поиск пользователя внутри .25 секунд " - настоящая цель Дизайна, и если более простой, но медленный код все еще соответствует этой цели, вам нужно остановиться на этом.