技术频道导航
HTML/CSS
.NET技术
IIS技术
PHP技术
Js/JQuery
Photoshop
Fireworks
服务器技术
操作系统
网站运营

赞助商

分类目录

赞助商

最新文章

搜索

C# .NET fastCSV : 小巧快速、符合标准的CSV读写器

作者:admin    时间:2023-5-19 20:43:36    浏览:

本文介绍如何使用 fastCSV 读写CSV文件。fastCSV 是一个 CSV 解析器,可以满足我们对小、快速和易于使用的要求。

C# .NET fastCSV : 小巧快速、符合标准的CSV读写器

fastCSV 特征

  • 完全符合 CSV 标准
  • 多线
  • 引用列
  • 在分隔符之间保留空格
  • 真正快速地读取和写入 CSV 文件(参见性能)
  • 微型 8kb DLL 编译为net40或netstandard20
  • 能够从 CSV 文件中获取类型化的对象列表
  • 能够在加载时过滤 CSV 文件
  • 能够指定自定义分隔符

CSV 标准

请参阅文章:

下面是一点摘要

  • 如果列中的值包含新行,则为多行
  • 如果列包含换行符、分隔符或引号字符,则必须用引号引起来
  • 报价必须被引用
  • 分隔符之间的空格被视为列的一部分

使用代码

下面是一些如何使用 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<car>
var listcars = fastCSV.ReadFile<cars>(
    "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<LocalWeatherData>(
    "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<LocalWeatherData>("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

  • 在加载时的地图函数中,你可以在加载的行数据上编写条件,并通过使用return false;过滤掉不需要的行。

读取 CSV 以导入到其他系统

  • 在你的地图功能中,你可以将线路数据发送到另一个系统,并且return false;
  • 或处理整个文件并使用返回的List<T>

加载时处理/聚合数据

  • 你可以有一个List<T>,它与CSV文件的列和 sum/min/max/avg 等读取的行无关。

代码里面

本质上,读取是一个循环,通过解析一行,为列表创建一个通用元素,将创建的对象和从行中提取的列传递给用户定义的映射函数并将其添加到列表以返回(如果映射函数这么说):

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<T> ReadFile<T>(string filename, bool hasheader, char delimiter, ToOBJ<T> mapper) where T : new()
{
    string[] cols = null;
    List<T> list = new List<T>();
    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<T>(string filename, string[] headers, char delimiter, List<T> list, FromObj<T> 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<object> cols = new List<object>();
                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<LocalWeatherData>();
int line = 0;
var training = fastCSV.ReadFile<LocalWeatherData>("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;
            testing.Add(o);
        }
        return add;
    });

性能表现

新的性能数字如下所示,与 v1 代码相比,在相同的 4,496,263 行数据集上以稍微多一点的内存使用为代价快了近2 倍。

  • fastCSV .net4:使用 6.27s 753Mb
  • fastCSV core:使用 6.51s 669Mb

有趣的是,在 .net core上,该库使用的内存更少。

下载 fastCSV

本站提供 fastCSV 源码直接下载。

下载 fastCSV_v2.0.8.rar

总结

本文介绍了如何使用 fastCSV 读写CSV文件。fastCSV 是一个 CSV 解析器,可以满足我们对小、快速和易于使用的要求。.NET 4.0编译 fastCSV 后,它的dll只有8KB。

相关文章

标签: fastCSV  asp.net  CSharp  CSV  
x
  • 站长推荐
/* 左侧显示文章内容目录 */