目录
介绍
特征
CSV标准
性能基准
未完成的
使用代码
助手函数以提高性能
使用场景
代码内
样例用例
拆分数据集以进行测试和培训
- 下载fastCSV_v1.0.zip
随着机器学习的兴起和为此目的而采用CSV格式提取大型数据集的兴起,我决定编写一个CSV解析器,该解析器可以满足我对小型、快速和易于使用的要求。我看过的大多数库都不符合我的要求,因此fastCSV
诞生了。
CSV还允许您将表格(二维)数据快速加载到内存中,这与其他序列化程序(例如fastJSON
)不同。
- 完全符合CSV标准
- 多行
- 引用栏
- 在分隔符之间保持空格
- 真正快速读取和写入CSV文件(请参阅性能)
- 微小的8kb DLL编译为
net40
或netstandard20
- 能够从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)
从字符串创建一个int
int ToInt(string s, int index, int count)
从子串创建一个int
DateTime 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;
});