您当前的位置: 首页 > 

寒冰屋

暂无认证

  • 0浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

fastCSV

寒冰屋 发布时间:2020-01-19 19:51:30 ,浏览量:0

目录

介绍

特征

CSV标准

性能基准

未完成的

使用代码

助手函数以提高性能

使用场景

代码内

样例用例

拆分数据集以进行测试和培训

  • 下载fastCSV_v1.0.zip
介绍

随着机器学习的兴起和为此目的而采用CSV格式提取大型数据集的兴起,我决定编写一个CSV解析器,该解析器可以满足我对小型、快速和易于使用的要求。我看过的大多数库都不符合我的要求,因此fastCSV诞生了。

CSV还允许您将表格(二维)数据快速加载到内存中,这与其他序列化程序(例如fastJSON)不同。

特征
  • 完全符合CSV标准
    • 多行
    • 引用栏
    • 在分隔符之间保持空格
  • 真正快速读取和写入CSV文件(请参阅性能)
  • 微小的8kb DLL编译为net40netstandard20
  • 能够从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

作为对同一数据集上可能存在的基准的比较:

  1. File.ReadAllBytes() :使用1.5s 573Mb
  2. File.ReadAllLines() 未经处理:使用3.7s 1633Mb
  3. File.ReadLines() 无处理:1.9s
  4. File.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标准,DateTimeyyyy-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;过滤掉不需要的行 
  • 读取CSV导入到其他系统
    • 在map函数中,您可以将行数据发送到另一个系统,然后 return false;
    • 或处理整个文件并使用List作为返回的
  • 加载时处理/汇总数据
    • 您可以有一个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;
    });

 

关注
打赏
1665926880
查看更多评论
立即登录/注册

微信扫码登录

0.0503s