读取大型 CSV 文件的更快方法

Faster method to read large CSV file

我有一个相当大的 CSV 数据集,大约 13.5MB,大约有 120,000 行和 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 分钟。

您可能有记忆问题。在代码 运行 时打开任务管理器,看看您是否达到了最大内存量。

尝试以下操作:

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

你犯的基本错误是每帧只做 1 行 所以你基本上可以计算出大约 60fps 需要多长时间:

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

由于 yield return null; 基本上表示 "Pause the routine, render this frame and continue in the next frame".


当然,完全不使用 yield return null 或协程来谈论绝对时间会更快,但让整个事情一次性解析。但是当然它会暂时冻结 UI 主线程。

为了避免 最好的 方式,在我看来实际上是将整个东西移动到 Thread/Task 并且只移动 return 结果!

文件 IO 和字符串解析总是很慢。


但是,我认为您只需使用 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();
    }
}

这允许在尝试保持 60fps 帧速率的同时在一帧内处理多行。您可能想对其进行一些试验,以找到帧速率和持续时间之间的良好折衷。例如。也许你可以让它 运行 只有 30fps 但导入速度更快,因为这样它可以在一帧中处理更多行。


一般来说,我不会把 "manually" 通读一遍 byte/char。而是使用内置方法,例如String.Split

我实际上使用了更高级的 Regex.Matches,因为如果您从 Excel 导出 CSV,它允许特殊情况,例如一个单元格本身包含 , 或其他特殊字符,例如例如换行符 (!).

在这种情况下,

Excel 通过将单元格包装在 " 中来实现。这增加了第二个特殊情况,即单元格本身包含 ".

Regex.Marches当然相当复杂而且本身很慢,但涵盖了这些特殊情况。 (另请参阅 Basic CSV rules 以获取有关特殊情况的更详细说明)

如果您非常了解 CSV 的格式并且不需要它,您 could/should 可能宁愿坚持使用

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

30 分钟太慢了!

似乎有几个问题:

  • bufferString 永远不会被清除。请参阅下面的更新版本。清除它允许代码在 <1 秒内在我的机器上使用 23MB 130,000 行输入文件。
  • row 在每次循环迭代结束时重置,这意味着只有 datasetTable[0, col] 被填充。如果这是有意的,您可以简化一些启动代码。
  • 正如人们提到的,正确 解析 CSV 的代码非常棘手,但如果您对输入文件的格式有信心,这应该没问题。
                        if (bufferChar == ',')
                        {
                            datasetTable[row, column] = bufferString.ToString();
                            column++;
                            bufferString.Clear(); // <-- Add this line
                        }
                        else
                        {
                            bufferString.Append(bufferChar);
                        }

这个呢?

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 帮助操作系统更有效地缓存它。
  • 更大的缓冲区减少系统调用。

它在内存方面也更有效,因为它不会将全部信息保存在内存中。是需要把所有的信息都保存在内存中,还是一行一行的处理?