读取大型 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 帮助操作系统更有效地缓存它。
- 更大的缓冲区减少系统调用。
它在内存方面也更有效,因为它不会将全部信息保存在内存中。是需要把所有的信息都保存在内存中,还是一行一行的处理?
我有一个相当大的 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 帮助操作系统更有效地缓存它。
- 更大的缓冲区减少系统调用。
它在内存方面也更有效,因为它不会将全部信息保存在内存中。是需要把所有的信息都保存在内存中,还是一行一行的处理?