目录
介绍
特征
CSV标准
性能基准
未完成的
使用代码
助手函数以提高性能
使用场景
代码内
样例用例
拆分数据集以进行测试和培训
- 下载fastCSV_v1.0.zip
介绍
随着机器学习的兴起和为此目的而采用CSV格式提取大型数据集的兴起,我决定编写一个CSV解析器,该解析器可以满足我对小型、快速和易于使用的要求。我看过的大多数库都不符合我的要求,因此fastCSV诞生了。
CSV还允许您将表格(二维)数据快速加载到内存中,这与其他序列化程序(例如fastJSON)不同。
特征
- 完全符合CSV标准
- 多行
- 引用栏
- 在分隔符之间保持空格
- 真正快速读取和写入CSV文件(请参阅性能)
- 微小的8kb DLL编译为
net40或netstandard20 - 能够从CSV文件中获取对象的类型列表
- 加载时能够过滤CSV文件
- 能够指定自定义分隔符
CSV标准
您可以在此处阅读CSV RFC:https : //tools.ietf.org/html/rfc4180作为CSV文件的摘要可以:
- 如果一列中的值包含新行,则为多行
- 如果列包含换行符,分隔符或引号字符,则必须用引号引起来
- 引号必须加引号
- 分隔符之间的空格被视为列的一部分
以下是来自Wiki页面https://en.wikipedia.org/wiki/Comma-separated_values的复杂的、符合标准的CSV文件的示例:
Year,Make,Model,Description,Price
1997,Ford,"E350
F150","ac, abs,
moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
1996,Jeep,Grand Cherokee,"MUST SELL!
air,
"",moon,""
roof, loaded",4799.00
1999,BMW,Z3,"used",14900.00
1999, Toyota,Corolla,,7000.00
如您所见,有些行是多行的,包含引号和逗号,这将给出下表:
| Year | Make | Model | Description | Price |
| 1997年 | 福特汽车 | E350 F150 | ac, abc, moon | 3000.00 |
| 1999年 | 雪佛兰 | Venture “扩展版” |
| 4900.00 |
| 1999年 | 雪佛兰 | Venture “扩展版,非常大” |
| 5000.00 |
| 1996年 | 吉普车 | 大切诺基 | MUST SELL! air, ",moon," roof, loaded | 4799.00 |
| 1999年 | 宝马 | Z3 | 用过的 | 14900.00 |
| 1999年 | 丰田汽车 | 花冠 |
| 7000.00 |
如您所见,有些列是多行,最后一行的“Toyota”列以空格开头。
性能基准
加载https://www.ncdc.noaa.gov/orders/qclcd/QCLCD201503.zip(585Mb)文件,该文件在我的计算机上具有4,496,263行,作为与其他库的相对比较:
- fastCSV:使用11.20s 639Mb
- NReco.CSV:使用19.05s 800Mb
- fastCSV string.Split():使用11.50s 638Mb
- TinyCSVparser:使用34s 992Mb
作为对同一数据集上可能存在的基准的比较:
File.ReadAllBytes():使用1.5s 573MbFile.ReadAllLines()未经处理:使用3.7s 1633MbFile.ReadLines()无处理:1.9sFile.ReadLines()+string.Split()没有返回列表:7.5s
1到2的区别是将字节转换为Unicode字符串的开销:2.2s
2和3之间的区别是创建string[]的内存开销:1.8秒
从4到fastCSV的区别是创建T对象并添加到列表的开销:4s
未完成的
- 将块加载到缓冲区中:
- 最初我尝试此路线时,事实证明它太复杂了,无法正常工作。从其他这样做的库来看,与当前的实现相比,它仍然很慢。
StringBuilder逐个字符:- 事实证明,使用此选项对于从行中解析列太慢。
使用代码
以下是一些使用fastCSV的方法示例:
public class car
{
// you can use fields or properties
public string Year;
public string Make;
public string Model;
public string Description;
public string Price;
}
// listcars = List
var listcars = fastCSV.ReadFile(
"csvstandard.csv", // filename
true, // has header
',', // delimiter
(o, c) => // to object function o : car object, c : columns array read
{
o.Year = c[0];
o.Make = c[1];
o.Model = c[2];
o.Description = c[3];
o.Price = c[4];
// add to list
return true;
});
fastCSV.WriteFile(
"filename2.csv", // filename
new string[] { "WBAN", "Date", "SkyCondition" }, // headers defined or null
'|', // delimiter
list, // list of LocalWeatherData to save
(o, c) => // from object function
{
c.Add(o.WBAN);
c.Add(o.Date.ToString("yyyyMMdd"));
c.Add(o.SkyCondition);
});
助手函数以提高性能
fastCSV 具有以下助手函数:
int ToInt(string s)从字符串创建一个intint ToInt(string s, int index, int count)从子串创建一个intDateTime ToDateTimeISO(string value, bool UseUTCDateTime)创建一个ISO标准,DateTime即yyyy-MM-ddTHH:mm:ss(可选部分.nnnZ)
public class LocalWeatherData
{
public string WBAN;
public DateTime Date;
public string SkyCondition;
}
var list = fastCSV.ReadFile("201503hourly.txt", true, ',', (o, c) =>
{
bool add = true;
o.WBAN = c[0];
// c[1] data is in "20150301" format
o.Date = new DateTime(fastCSV.ToInt(c[1], 0, 4),
fastCSV.ToInt(c[1], 4, 2),
fastCSV.ToInt(c[1], 6, 2));
o.SkyCondition = c[4];
//if (o.Date.Day % 2 == 0)
// add = false;
return add;
});
使用场景
- 加载时过滤CSV
- 在加载时的map函数中,您可以在加载的行数据上写条件,并通过使用
return false;过滤掉不需要的行
- 在加载时的map函数中,您可以在加载的行数据上写条件,并通过使用
- 读取CSV导入到其他系统
- 在map函数中,您可以将行数据发送到另一个系统,然后
return false; - 或处理整个文件并使用
List作为返回的
- 在map函数中,您可以将行数据发送到另一个系统,然后
- 加载时处理/汇总数据
- 您可以有一个
List它与CSV文件的列和sum/min/max/avg/etc没有关系
- 您可以有一个
代码内
本质上,读取是通过以下方式进行的循环:解析行,为列表创建通用元素,将创建的对象以及从该行提取的列移交给用户定义的map函数,并将其添加到列表中以供返回(如果map函数这么说):
var c = ParseLine(line, delimiter, cols);
T o = new T();
var b = mapper(o, c);
if (b)
list.Add(o);
现在,CSV标准复杂性来自正确处理多行,这是通过计算一行中引号是否为奇数来完成的,因此,它是多行并读取行直到引号是偶数,这是在ReadFile()函数中完成的。
这种方法的优点在于它简单,无反射并且非常快,并且控制在用户手中。
所有的阅读代码如下:
public static List ReadFile(string filename, bool hasheader, char delimiter, ToOBJ mapper) where T : new()
{
string[] cols = null;
List list = new List();
int linenum = -1;
StringBuilder sb = new StringBuilder();
bool insb = false;
foreach (var line in File.ReadLines(filename))
{
try
{
linenum++;
if (linenum == 0)
{
if (hasheader)
{
// actual col count
int cc = CountOccurence(line, delimiter);
if (cc == 0)
throw new Exception("File does not have '" + delimiter + "' as a delimiter");
cols = new string[cc + 1];
continue;
}
else
cols = new string[_COLCOUNT];
}
var qc = CountOccurence(line, '\"');
bool multiline = qc % 2 == 1 || insb;
string cline = line;
// if multiline add line to sb and continue
if (multiline)
{
insb = true;
sb.Append(line);
var s = sb.ToString();
qc = CountOccurence(s, '\"');
if (qc % 2 == 1)
{
sb.AppendLine();
continue;
}
cline = s;
sb.Clear();
insb = false;
}
var c = ParseLine(cline, delimiter, cols);
T o = new T();
var b = mapper(o, c);
if (b)
list.Add(o);
}
catch (Exception ex)
{
throw new Exception("error on line " + linenum, ex);
}
}
return list;
}
private unsafe static int CountOccurence(string text, char c)
{
int count = 0;
int len = text.Length;
int index = -1;
fixed (char* s = text)
{
while (index++ < len)
{
char ch = *(s + index);
if (ch == c)
count++;
}
}
return count;
}
private unsafe static string[] ParseLine(string line, char delimiter, string[] columns)
{
//return line.Split(delimiter);
int col = 0;
int linelen = line.Length;
int index = 0;
fixed (char* l = line)
{
while (index < linelen)
{
if (*(l + index) != '\"')
{
// non quoted
var next = line.IndexOf(delimiter, index);
if (next < 0)
{
columns[col++] = new string(l, index, linelen - index);
break;
}
columns[col++] = new string(l, index, next - index);
index = next + 1;
}
else
{
// quoted string change "" -> "
int qc = 1;
int start = index;
char c = *(l + ++index);
// find matching quote until delim or EOL
while (index++ < linelen)
{
if (c == '\"')
qc++;
if (c == delimiter && qc % 2 == 0)
break;
c = *(l + index);
}
columns[col++] = new string(l, start + 1, index - start - 3).Replace("\"\"", "\"");
}
}
}
return columns;
}
ParseLine()负责以一种优化的unsafe方式从一行中提取列。
编写代码就是:
public static void WriteFile(string filename, string[] headers, char delimiter, List list, FromObj mapper)
{
using (FileStream f = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
using (StreamWriter s = new StreamWriter(f))
{
if (headers != null)
s.WriteLine(string.Join(delimiter.ToString(), headers));
foreach (var o in list)
{
List cols = new List();
mapper(o, cols);
for (int i = 0; i < cols.Count; i++)
{
// qoute string if needed -> \" \r \n delim
var str = cols[i].ToString();
bool quote = false;
if (str.IndexOf('\"') >= 0)
{
quote = true;
str = str.Replace("\"", "\"\"");
}
if (quote == false && str.IndexOf('\n') >= 0)
quote = true;
if (quote == false && str.IndexOf('\r') >= 0)
quote = true;
if (quote == false && str.IndexOf(delimiter) >= 0)
quote = true;
if (quote)
s.Write("\"");
s.Write(str);
if (quote)
s.Write("\"");
if (i < cols.Count - 1)
s.Write(delimiter);
}
s.WriteLine();
}
s.Flush();
}
f.Close();
}
}
样例用例
拆分数据集以进行测试和培训
在数据科学中,通常将数据划分为训练集和测试集,在下面的示例中,每第3行用于测试(您可以使划分更加复杂):
var testing = new List();
int line = 0;
var training = fastCSV.ReadFile("201503hourly.txt", true, ',', (o, c) =>
{
bool add = true;
line++;
o.Date = new DateTime(fastCSV.ToInt(c[1], 0, 4),
fastCSV.ToInt(c[1], 4, 2),
fastCSV.ToInt(c[1], 6, 2));
o.SkyCondition = c[4];
if (line % 3 == 0)
{
add = false;
test.Add(o);
}
return add;
});
