Более быстрый способ чтения большого файла CSV - PullRequest
0 голосов
/ 27 мая 2020

У меня довольно большой набор данных CSV, около 13,5 МБ, примерно с 120000 строк и 13 столбцов. Код ниже - это текущее решение, которое у меня есть.

private IEnumerator readDataset()
{
    starsRead = 0;
    var totalLines = File.ReadLines(path).Count();
    totalStars = totalLines - 1;

    string firstLine = File.ReadLines(path).First();
    int columnCount = firstLine.Count(f => f == ',');

    string[,] datasetTable = new string[totalStars, columnCount];

    int lineLength;
    char bufferChar;
    var bufferString = new StringBuilder();
    int column;
    int row;

    using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    using (BufferedStream bs = new BufferedStream(fs))
    using (StreamReader sr = new StreamReader(bs))
    {
        string line = sr.ReadLine();
        while ((line = sr.ReadLine()) != null)
        {
            row = 0;
            column = 0;
            lineLength = line.Length;
            for (int i = 0; i < lineLength; i++)
            {
                bufferChar = line[i];
                if (bufferChar == ',')
                {
                    datasetTable[row, column] = bufferString.ToString();
                    column++;
                }
                else
                {
                    bufferString.Append(bufferChar);
                }
            }
            row++;
            starsRead++;
            yield return null;
        }
    }
}

К счастью, поскольку я запускаю это через сопрограмму Unity, программа не зависает, но текущему решению требуется 31 минута и 44 секунды, чтобы полностью прочитать CSV-файл.

Есть ли другой способ сделать это? Я пытаюсь установить время синтаксического анализа менее 1 минуты.

Ответы [ 4 ]

2 голосов
/ 27 мая 2020

Что насчет этого?

private IEnumerable<string[]> ReadCsv(string path)
{
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 64 * 1024, FileOptions.SequentialScan)) 
    using (var reader = new StreamReader(fs))
    {
        string line = reader.ReadLine();
        while ((line = reader.ReadLine()) != null)
        {
            yield return line.Split(',');
        }
    }
}

Это должно быть быстрее, потому что:

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

Он также более эффективен с точки зрения памяти, поскольку не сохраняет всю информацию в памяти. Вам нужно хранить всю информацию в памяти, или вы можете обрабатывать ее построчно?

2 голосов
/ 27 мая 2020

Основная c ошибка, которую вы делаете, это только 1 одна строка на кадр , поэтому вы можете в основном рассчитать, сколько времени у вас займет около 60 кадров в секунду:

120,000 rows / 60fps = 2000 seconds = 33.3333 minutes

из-за на yield return null;, который в основном говорит: «Приостановите процедуру, визуализируйте этот кадр и продолжите в следующем кадре».


Конечно, было бы намного быстрее говорить об абсолютном времени, не используя yield return null или Coroutine вообще, но пусть все это будет проанализировано в одном go. Но затем, конечно, он на мгновение замораживает основной поток пользовательского интерфейса.

Чтобы избежать этого, лучший способ, на мой взгляд, на самом деле будет заключаться в перемещении всего объекта в Thread / Task и вернуть только результат!

FileIO и синтаксический анализ строк всегда довольно медленный.


Однако, я думаю, вы уже могли бы значительно ускорить его, просто используя StopWatch как

...

var stopWatch = new Stopwatch();
stopWatch.Start();

// Use the last frame duration as a guide for how long one frame should take
var targetMilliseconds = Time.deltaTime * 1000f;

while ((line = sr.ReadLine()) != null)
{
    ....

    // If you are too long in this frame render one and continue in the next frame
    // otherwise keep going with the next line
    if(stopWatch.ElapsedMilliseconds > targetMilliseconds)
    {
        yield return null;
        stopWatch.Restart();
    }
}

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


В общем, я бы не стал читать «вручную» через каждый байт / символ. Вместо этого используйте для этого встроенные методы, например, String.Split.

На самом деле я использую немного более продвинутый Regex.Matches, поскольку при экспорте CSV из Excel возможны особые случаи, такие как одна ячейка, содержащая , или другие специальные символы, например разрывы строк (!).

В этом случае Excel делает это, заключая ячейку в ". Это добавляет второй особый случай, а именно ячейку, содержащую ".

Regex.Marches, конечно, довольно сложный и медленный, но охватывает эти особые случаи. (См. Также Basi c правила CSV для более подробного объяснения особых случаев)

Если вы хорошо знаете формат вашего CSV и не нуждаетесь в нем, вы могли бы / должны, вероятно, скорее просто придерживайтесь

var columns = row.Split(new []{ ','});

, чтобы всегда разделять его только на ,, что будет работать быстрее.

private const char Quote = '\"';
private const string LineBreak = "\r\n";
private const string DoubleQuote = "\"\"";

private IEnumerator readDataset(string path)
{
    starsRead = 0;
    // Use the last frame duration as a guide how long one frame should take
    // you can also try and experiment with hardcodd target framerates like e.g. "1000f / 30" for 30fps
    var targetMilliseconds = Time.deltaTime * 1000f;
    var stopWatch = new Stopwatch();

    // NOTE: YOU ARE ALREADY READING THE ENTIRE FILE HERE ONCE!!
    // => Instead of later again read it line by line rather re-use this file content
    var lines = File.ReadLines(path).ToArray();
    var totalLines = lines.Length;
    totalStars = totalLines - 1;

    // HERE YOU DID READ THE FILE AGAIN JUST TO GET THE FIRST LINE ;)
    string firstLine = lines[0];

    var firstLineColumns = GetColumns(firstLine);

    columnCount = firstLineColumns.Length;

    var datasetTable = new string[totalStars, columnCount];

    stopWatch.Start();
    for(var i = 0; i < totalStars; i++)
    {
        string row = lines[i + 1];

        string[] columns = GetColumns(row);

        var colIndex = 0;
        foreach(var column in columns)
        {
            if(colIndex >= columnCount - 1) break;
            datasetTable[i, colIndex] = colum;
            colIndex++;
        }

        starsRead = i + 1;

        // If you are too long in this frame render one and continue in the next frame
        // otherwise keep going with the next line
        if (stopWatch.ElapsedMilliseconds > targetMilliseconds)
        {
            yield return null;
            stopWatch.Restart();
        }
    }
}

private string[] GetColumns(string row)
{
    var columns = new List<string>();

    // Look for the following expressions:
    // (?<x>(?=[,\r\n]+))           --> Creates a Match Group (?<x>...) of every expression it finds before a , a \r or a \n (?=[...])
    // OR |
    // ""(?<x>([^""]|"""")+)""      --> An Expression wrapped in single-quotes (escaped by "") is matched into a Match Group that is neither NOT a single-quote [^""] or is a double-quote
    // OR |
    // (?<x>[^,\r\n]+)),?)          --> Creates a Match Group (?<x>...) that does not contain , \r, or \n
    var matches = Regex.Matches(row, @"(((?<x>(?=[,\r\n]+))|""(?<x>([^""]|"""")+)""|(?<x>[^,\r\n]+)),?)", RegexOptions.ExplicitCapture);

    foreach (Match match in matches)
    {
        var cleanedMatch = match.Groups[1].Value == "\"\"" ? "" : match.Groups[1].Value.Replace("\"\"", Quote.ToString());
        columns.Add(cleanedMatch);
    }

    // If last thing is a `,` then there is an empty item missing at the end
    if (row.Length > 0 && row[row.Length - 1].Equals(','))
    {
        columns.Add("");
    }

    return columns.ToArray();
}
1 голос
/ 27 мая 2020

30 минут - безумно медленно!

Кажется, есть несколько проблем:

  • bufferString никогда не очищается. См. Обновленную версию ниже. Очистка позволяет коду запускаться в течение <1 с на моем компьютере с входным файлом строк размером 23 МБ 130000. </li>
  • row сбрасывается в конце каждой итерации l oop, что означает, что только datasetTable[0, col] получает заселены. Если это сделано намеренно, вы, вероятно, можете упростить часть кода запуска.
  • Как уже упоминалось, код для правильно синтаксический анализ CSV безумно сложен, но если вы можете быть уверены в формате ваши входные файлы должны быть в порядке.
                        if (bufferChar == ',')
                        {
                            datasetTable[row, column] = bufferString.ToString();
                            column++;
                            bufferString.Clear(); // <-- Add this line
                        }
                        else
                        {
                            bufferString.Append(bufferChar);
                        }
0 голосов
/ 27 мая 2020

Возможно, у вас проблема с памятью. Откройте диспетчер задач во время выполнения кода, чтобы убедиться, что вы достигли максимального объема памяти.

Попробуйте следующее:

        private void readDataset()
        {

            List<List<string>> datasetTable = new List<List<string>>(); ;


            using (StreamReader sr = new StreamReader(path))
            {
                string line = sr.ReadLine();  //skip header row
                while ((line = sr.ReadLine()) != null)
                {
                    datasetTable.Add(line.Split(new char[] { ',' }).ToList());
                }
            }
        }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...