用C#解析带头的CSV文件


265

有没有默认/官方/推荐的方法来解析C#中的CSV文件?我不想滚动自己的解析器。

另外,我已经看到有人使用ODBC / OLE DB通过文本驱动程序读取CSV的实例,由于它的“缺点”,很多人不赞成这样做。这些缺点是什么?

理想情况下,我正在寻找一种方法,可以使用第一条记录作为标题/字段名称来按列名读取CSV。给出的一些答案是正确的,但是基本上可以将文件反序列化为类。

Answers:


138

让图书馆为您处理所有细节!:-)

检出FileHelpers并保持干燥-不要重复自己-无需花费万亿次重新发明轮子...。

基本上,您只需要通过公共类(以及经过深思熟虑的属性(例如默认值,NULL值的替换等))定义数据的形状-CSV中的单独行中的字段-在FileHelpers引擎上找到一个文件,然后进行宾果游戏-您从该文件中获取所有条目。一个简单的操作-出色的性能!


1
直到您真正需要定制为止(无论如何,大多数都可以作为扩展实现)FileHelpers是迄今为止最好的选择,真正方便,经过测试且性能良好的解决方案
mikus 2013年

3
从2015年6月1日开始,我下载FileHelpers的唯一方法是在sourceforge.net上搜索它。这是使用的链接:sourceforge.net/projects/filehelpers/?
source=directory

2
@dotnetguy我们正在发布3.1(当前为3.1-rc2)。我们还重新设计了网站:www.filehelpers.net,您可以从那里下载最新版本
Marcos Meli

1
@MarcosMeli非常感谢!我已经在我的一个项目中使用了FileHelpers,使用起来很轻松-对团队来说是个荣誉。我打算很快在上面计划一个博客,顺便说一句-爱这个新网站-做得好!
Sudhanshu Mishra

FileHelpers无法正确处理CSV中带引号的逗号,或无法实际映射字段标题,而是期望这些列与您在类型中声明的字段的顺序相同。我个人不会使用它。
Alastair花胶

357

CSV解析器现在是.NET Framework的一部分。

添加对Microsoft.VisualBasic.dll的引用(在C#中可以正常运行,不要管名称)

using (TextFieldParser parser = new TextFieldParser(@"c:\temp\test.csv"))
{
    parser.TextFieldType = FieldType.Delimited;
    parser.SetDelimiters(",");
    while (!parser.EndOfData)
    {
        //Process row
        string[] fields = parser.ReadFields();
        foreach (string field in fields)
        {
            //TODO: Process field
        }
    }
}

文档在这里-TextFieldParser类

PS:如果您需要CSV 导出程序,请尝试CsvExport(Disc:我是其中之一)


2
根据我的经验,TextFieldParser在大型文件(例如> 250Mb)中的性能不佳。:(
MBoros 2014年

6
TextFieldParser实现IDisposable,因此最好在using子句中使用它。否则,好的答案。
克里斯·布什

3
在构造函数中,您可能希望使用与默认编码不同的编码,例如:new TextFieldParser(“ c:\ temp \ test.csv”,System.Text.Encoding.UTF8)
Neuro5torm

1
请注意,如果CSV中的任何字段包含空白行,它们将被跳过TextFieldParser.ReadLine()。请参阅TextFieldParser文档
mcNux

3
是否可以在.NET Core中获得此功能?
雨果·辛克

183

CsvHelper(我维护的库)会将CSV文件读入自定义对象。

var csv = new CsvReader( File.OpenText( "file.csv" ) );
var myCustomObjects = csv.GetRecords<MyCustomObject>();

有时,您不拥有要读取的对象。在这种情况下,您可以使用流畅的映射,因为您不能在类上放置属性。

public sealed class MyCustomObjectMap : CsvClassMap<MyCustomObject>
{
    public MyCustomObjectMap()
    {
        Map( m => m.Property1 ).Name( "Column Name" );
        Map( m => m.Property2 ).Index( 4 );
        Map( m => m.Property3 ).Ignore();
        Map( m => m.Property4 ).TypeConverter<MySpecialTypeConverter>();
    }
}

编辑:

CsvReader现在要求将CultureInfo传递到构造器中(https://github.com/JoshClose/CsvHelper/issues/1441)。

例:

var csv = new CsvReader(File.OpenText("file.csv"), System.Globalization.CultureInfo.CurrentCulture);

18
我同意@ kubal5003。卖给我的是您可以通过NuGet软件包获得它。谢谢男人,它速度很快,并且可以完成我需要的所有csv阅读。
Gromer 2012年

7
该死的快。在10秒内读取并反序列化了130万条记录。
2013年

2
很棒的图书馆很容易实现。我只是建议Josh在这里更新他的答案,因为自编写此答案以来库已发生了一些变化,并且您无法再实例化CsvHelper(现在仅是一个名称空间),但是您必须使用CsvReader类。
Marko

1
在最新版本的CsvHelper中似乎不存在CsvClassMap?
knocte

1
知道,现在称为ClassMap。还有其他更改,例如在要求头记录之前必须进行读取(顺便说一下,它被设置为第一次调用Read()所读取的内容)。就像其他人之前提到的那样,它非常快速且易于使用。
诺吉

31

在业务应用程序中,我使用codeproject.com上的OpenSource项目CSVReader

它运作良好,并且具有良好的性能。我提供的链接上有一些基准测试。

从项目页面复制的一个简单示例:

using (CsvReader csv = new CsvReader(new StreamReader("data.csv"), true))
{
    int fieldCount = csv.FieldCount;
    string[] headers = csv.GetFieldHeaders();

    while (csv.ReadNextRecord())
    {
        for (int i = 0; i < fieldCount; i++)
            Console.Write(string.Format("{0} = {1};", headers[i], csv[i]));

        Console.WriteLine();
    }
}

如您所见,它非常容易使用。



12

如果您只需要读取csv文件,那么我建议您使用此库:快速CSV阅读器
如果您还需要生成csv文件,请使用以下库:FileHelpers

两者都是免费和开源的。


FileHelpers的摘要很吸引人:filehelpers.com FileHelpers是一个免费且易于使用的.NET库,用于从固定长度或文件,字符串或流中定界记录中导入/导出数据。
AnneTheAgile 2012年

尽管此链接可以回答问题,但在Stack Overflow上不鼓励仅链接的答案,但是您可以通过将链接的重要部分放入答案中来改善此答案,这可以确保如果更改链接,您的答案仍然是答案或删除:)
WhatsThePoint

11

这是我经常使用的帮助程序类,以防任何人回到该线程中(我想分享它)。

为了简化将其移植到准备使用的项目中,我使用了它:

public class CSVHelper : List<string[]>
{
  protected string csv = string.Empty;
  protected string separator = ",";

  public CSVHelper(string csv, string separator = "\",\"")
  {
    this.csv = csv;
    this.separator = separator;

    foreach (string line in Regex.Split(csv, System.Environment.NewLine).ToList().Where(s => !string.IsNullOrEmpty(s)))
    {
      string[] values = Regex.Split(line, separator);

      for (int i = 0; i < values.Length; i++)
      {
        //Trim values
        values[i] = values[i].Trim('\"');
      }

      this.Add(values);
    }
  }
}

并像这样使用它:

public List<Person> GetPeople(string csvContent)
{
  List<Person> people = new List<Person>();
  CSVHelper csv = new CSVHelper(csvContent);
  foreach(string[] line in csv)
  {
    Person person = new Person();
    person.Name = line[0];
    person.TelephoneNo = line[1];
    people.Add(person);
  }
  return people;
}

[更新的csv帮助程序:已修复在最后一个新行字符创建新行的地方的错误]


17
如果任何csv条目包含逗号(,),则此代码将不起作用。
hakan 2012年

为了使内容轻巧,我使用了竖线字符作为分隔符。'|'
2012年

优秀的解决方案。只是关于第二个片段的问题。Person是什么类型的对象
Cocoa Dev

@CocoaDev这是一个包含两个字符串属性的类-Name和TelephoneNo。纯粹出于示例。如果任何属性是整数,则应该只是直接转换(带有check?)。
2013年

10

此解决方案使用官方的Microsoft.VisualBasic程序集解析CSV。

优点:

  • 分隔符转义
  • 忽略标题
  • 修剪空间
  • 忽略评论

码:

    using Microsoft.VisualBasic.FileIO;

    public static List<List<string>> ParseCSV (string csv)
    {
        List<List<string>> result = new List<List<string>>();


        // To use the TextFieldParser a reference to the Microsoft.VisualBasic assembly has to be added to the project. 
        using (TextFieldParser parser = new TextFieldParser(new StringReader(csv))) 
        {
            parser.CommentTokens = new string[] { "#" };
            parser.SetDelimiters(new string[] { ";" });
            parser.HasFieldsEnclosedInQuotes = true;

            // Skip over header line.
            //parser.ReadLine();

            while (!parser.EndOfData)
            {
                var values = new List<string>();

                var readFields = parser.ReadFields();
                if (readFields != null)
                    values.AddRange(readFields);
                result.Add(values);
            }
        }

        return result;
    }

7

我已经编写了.NET的TinyCsvParser,它是最快的.NET解析器之一,可高度配置为解析几乎所有CSV格式。

它根据MIT许可证发布:

您可以使用NuGet进行安装。在程序包管理器控制台中运行以下命令。

PM> Install-Package TinyCsvParser

用法

想象一下,我们在CSV文件中persons.csv有一个“人物”列表,其中包含他们的名字,姓氏和生日。

FirstName;LastName;BirthDate
Philipp;Wagner;1986/05/12
Max;Musterman;2014/01/02

我们系统中相应的域模型可能如下所示。

private class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
}

使用TinyCsvParser时,必须定义CSV数据中的列与域模型中的属性之间的映射。

private class CsvPersonMapping : CsvMapping<Person>
{

    public CsvPersonMapping()
        : base()
    {
        MapProperty(0, x => x.FirstName);
        MapProperty(1, x => x.LastName);
        MapProperty(2, x => x.BirthDate);
    }
}

然后,我们可以使用映射使用来解析CSV数据CsvParser

namespace TinyCsvParser.Test
{
    [TestFixture]
    public class TinyCsvParserTest
    {
        [Test]
        public void TinyCsvTest()
        {
            CsvParserOptions csvParserOptions = new CsvParserOptions(true, new[] { ';' });
            CsvPersonMapping csvMapper = new CsvPersonMapping();
            CsvParser<Person> csvParser = new CsvParser<Person>(csvParserOptions, csvMapper);

            var result = csvParser
                .ReadFromFile(@"persons.csv", Encoding.ASCII)
                .ToList();

            Assert.AreEqual(2, result.Count);

            Assert.IsTrue(result.All(x => x.IsValid));

            Assert.AreEqual("Philipp", result[0].Result.FirstName);
            Assert.AreEqual("Wagner", result[0].Result.LastName);

            Assert.AreEqual(1986, result[0].Result.BirthDate.Year);
            Assert.AreEqual(5, result[0].Result.BirthDate.Month);
            Assert.AreEqual(12, result[0].Result.BirthDate.Day);

            Assert.AreEqual("Max", result[1].Result.FirstName);
            Assert.AreEqual("Mustermann", result[1].Result.LastName);

            Assert.AreEqual(2014, result[1].Result.BirthDate.Year);
            Assert.AreEqual(1, result[1].Result.BirthDate.Month);
            Assert.AreEqual(1, result[1].Result.BirthDate.Day);
        }
    }
}

用户指南

完整的用户指南位于:


1

这是我的KISS实施...

using System;
using System.Collections.Generic;
using System.Text;

class CsvParser
{
    public static List<string> Parse(string line)
    {
        const char escapeChar = '"';
        const char splitChar = ',';
        bool inEscape = false;
        bool priorEscape = false;

        List<string> result = new List<string>();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < line.Length; i++)
        {
            char c = line[i];
            switch (c)
            {
                case escapeChar:
                    if (!inEscape)
                        inEscape = true;
                    else
                    {
                        if (!priorEscape)
                        {
                            if (i + 1 < line.Length && line[i + 1] == escapeChar)
                                priorEscape = true;
                            else
                                inEscape = false;
                        }
                        else
                        {
                            sb.Append(c);
                            priorEscape = false;
                        }
                    }
                    break;
                case splitChar:
                    if (inEscape) //if in escape
                        sb.Append(c);
                    else
                    {
                        result.Add(sb.ToString());
                        sb.Length = 0;
                    }
                    break;
                default:
                    sb.Append(c);
                    break;
            }
        }

        if (sb.Length > 0)
            result.Add(sb.ToString());

        return result;
    }

}

1
这不会处理在CSV文件中有效的带引号的字符串中的换行符。
约翰·莱德格伦

亚历克斯(Alex),约翰(John)想要说的是RFC 4180(ietf.org/rfc/rfc4180.txt-参见第2节和第6项)允许一列在其中间具有CR LF,从而有效地将其分散一个文件中有2行。您的解决方案可能在大多数情况下都可以很好地工作(特别是如果CSV文件是通过Excel以外的方式创建的),但是它不能解决这种情况。上面提到的CsvHelper应该考虑这种情况。
大卫·耶茨

是的,的确如此,但是如果您的CSV中包含CR LF,则您可能不应该使用CSV,而应该使用更合适的格式,例如json或xml或固定长度格式。
亚历克斯·比根(Alex Begun)

1

前一段时间,我基于Microsoft.VisualBasic库编写了用于CSV读/写的简单类。使用这个简单的类,您将可以像处理2维数组一样使用CSV。您可以通过以下链接找到我的课程:https : //github.com/ukushu/DataExporter

用法的简单示例:

Csv csv = new Csv("\t");//delimiter symbol

csv.FileOpen("c:\\file1.csv");

var row1Cell6Value = csv.Rows[0][5];

csv.AddRow("asdf","asdffffff","5")

csv.FileSave("c:\\file2.csv");

对于只读取标题,您需要读取csv.Rows[0]单元格:)


1

满足直接解析需求的单一源文件解决方案很有用。处理所有讨厌的情况。例如换行规范化和在带引号的字符串文字中处理换行。别客气!

如果CSV文件具有标题,则只需从第一行中读出列名(并计算列索引)。就那么简单。

请注意,这Dump是一种LINQPad方法,如果您不使用LINQPad,则可能要删除该方法。

void Main()
{
    var file1 = "a,b,c\r\nx,y,z";
    CSV.ParseText(file1).Dump();

    var file2 = "a,\"b\",c\r\nx,\"y,z\"";
    CSV.ParseText(file2).Dump();

    var file3 = "a,\"b\",c\r\nx,\"y\r\nz\"";
    CSV.ParseText(file3).Dump();

    var file4 = "\"\"\"\"";
    CSV.ParseText(file4).Dump();
}

static class CSV
{
    public struct Record
    {
        public readonly string[] Row;

        public string this[int index] => Row[index];

        public Record(string[] row)
        {
            Row = row;
        }
    }

    public static List<Record> ParseText(string text)
    {
        return Parse(new StringReader(text));
    }

    public static List<Record> ParseFile(string fn)
    {
        using (var reader = File.OpenText(fn))
        {
            return Parse(reader);
        }
    }

    public static List<Record> Parse(TextReader reader)
    {
        var data = new List<Record>();

        var col = new StringBuilder();
        var row = new List<string>();
        for (; ; )
        {
            var ln = reader.ReadLine();
            if (ln == null) break;
            if (Tokenize(ln, col, row))
            {
                data.Add(new Record(row.ToArray()));
                row.Clear();
            }
        }

        return data;
    }

    public static bool Tokenize(string s, StringBuilder col, List<string> row)
    {
        int i = 0;

        if (col.Length > 0)
        {
            col.AppendLine(); // continuation

            if (!TokenizeQuote(s, ref i, col, row))
            {
                return false;
            }
        }

        while (i < s.Length)
        {
            var ch = s[i];
            if (ch == ',')
            {
                row.Add(col.ToString().Trim());
                col.Length = 0;
                i++;
            }
            else if (ch == '"')
            {
                i++;
                if (!TokenizeQuote(s, ref i, col, row))
                {
                    return false;
                }
            }
            else
            {
                col.Append(ch);
                i++;
            }
        }

        if (col.Length > 0)
        {
            row.Add(col.ToString().Trim());
            col.Length = 0;
        }

        return true;
    }

    public static bool TokenizeQuote(string s, ref int i, StringBuilder col, List<string> row)
    {
        while (i < s.Length)
        {
            var ch = s[i];
            if (ch == '"')
            {
                // escape sequence
                if (i + 1 < s.Length && s[i + 1] == '"')
                {
                    col.Append('"');
                    i++;
                    i++;
                    continue;
                }
                i++;
                return true;
            }
            else
            {
                col.Append(ch);
                i++;
            }
        }
        return false;
    }
}

1

清单中的另一个Cinchoo ETL是一个开放源代码库,用于读取和写入多种文件格式(CSV,平面文件,Xml,JSON等)

下面的示例显示了如何快速读取CSV文件(不需要POCO对象)

string csv = @"Id, Name
1, Carl
2, Tom
3, Mark";

using (var p = ChoCSVReader.LoadText(csv)
    .WithFirstLineHeader()
    )
{
    foreach (var rec in p)
    {
        Console.WriteLine($"Id: {rec.Id}");
        Console.WriteLine($"Name: {rec.Name}");
    }
}

下面的示例演示如何使用POCO对象读取CSV文件

public partial class EmployeeRec
{
    public int Id { get; set; }
    public string Name { get; set; }
}

static void CSVTest()
{
    string csv = @"Id, Name
1, Carl
2, Tom
3, Mark";

    using (var p = ChoCSVReader<EmployeeRec>.LoadText(csv)
        .WithFirstLineHeader()
        )
    {
        foreach (var rec in p)
        {
            Console.WriteLine($"Id: {rec.Id}");
            Console.WriteLine($"Name: {rec.Name}");
        }
    }
}

请查看CodeProject上有关如何使用它的文章。


0

基于unlimit的文章“ 如何使用C#split()函数正确分割CSV”?

string[] tokens = System.Text.RegularExpressions.Regex.Split(paramString, ",");

注意:这不处理转义/嵌套的逗号等,因此仅适用于某些简单的CSV列表。


2
这非常糟糕,可能会很慢:)
EKS

1
可能吧,但是对于一小部分参数它完美且简单地工作,因此是一种有效且有用的解决方案。为什么要投票呢?“非常糟糕”有点极端,您不觉得吗?
radsdau

1
它不能处理转义/嵌套的逗号等。在某些情况下可以使用,但绝对不能用于所有的csv文件
NStuke

你是对的;我将编辑回复以反映这一点。谢谢。但是它仍然有它的位置。
radsdau '16

这对于我正在构建sql server clr dll并且不能使用任何其他这些外部包的用例来说非常有效。我只需要解析一个简单的带有文件名和行数的csv文件。
dubvfan87

0

此代码将csv读取到DataTable:

public static DataTable ReadCsv(string path)
{
    DataTable result = new DataTable("SomeData");
    using (TextFieldParser parser = new TextFieldParser(path))
    {
        parser.TextFieldType = FieldType.Delimited;
        parser.SetDelimiters(",");
        bool isFirstRow = true;
        //IList<string> headers = new List<string>();

        while (!parser.EndOfData)
        {
            string[] fields = parser.ReadFields();
            if (isFirstRow)
            {
                foreach (string field in fields)
                {
                    result.Columns.Add(new DataColumn(field, typeof(string)));
                }
                isFirstRow = false;
            }
            else
            {
                int i = 0;
                DataRow row = result.NewRow();
                foreach (string field in fields)
                {
                    row[i++] = field;
                }
                result.Rows.Add(row);
            }
        }
    }
    return result;
}

1
TextFieldParser在Microsoft.VisualBasic.dll中。
user3285954'2

0

如果有人想要摘录,则无需绑定库或下载程序包,便可以放入其代码中。这是我写的一个版本:

    public static string FormatCSV(List<string> parts)
    {
        string result = "";

        foreach (string s in parts)
        {
            if (result.Length > 0)
            {
                result += ",";

                if (s.Length == 0)
                    continue;
            }

            if (s.Length > 0)
            {
                result += "\"" + s.Replace("\"", "\"\"") + "\"";
            }
            else
            {
                // cannot output double quotes since its considered an escape for a quote
                result += ",";
            }
        }

        return result;
    }

    enum CSVMode
    {
        CLOSED = 0,
        OPENED_RAW = 1,
        OPENED_QUOTE = 2
    }

    public static List<string> ParseCSV(string input)
    {
        List<string> results;

        CSVMode mode;

        char[] letters;

        string content;


        mode = CSVMode.CLOSED;

        content = "";
        results = new List<string>();
        letters = input.ToCharArray();

        for (int i = 0; i < letters.Length; i++)
        {
            char letter = letters[i];
            char nextLetter = '\0';

            if (i < letters.Length - 1)
                nextLetter = letters[i + 1];

            // If its a quote character
            if (letter == '"')
            {
                // If that next letter is a quote
                if (nextLetter == '"' && mode == CSVMode.OPENED_QUOTE)
                {
                    // Then this quote is escaped and should be added to the content

                    content += letter;

                    // Skip the escape character
                    i++;
                    continue;
                }
                else
                {
                    // otherwise its not an escaped quote and is an opening or closing one
                    // Character is skipped

                    // If it was open, then close it
                    if (mode == CSVMode.OPENED_QUOTE)
                    {
                        results.Add(content);

                        // reset the content
                        content = "";

                        mode = CSVMode.CLOSED;

                        // If there is a next letter available
                        if (nextLetter != '\0')
                        {
                            // If it is a comma
                            if (nextLetter == ',')
                            {
                                i++;
                                continue;
                            }
                            else
                            {
                                throw new Exception("Expected comma. Found: " + nextLetter);
                            }
                        }
                    }
                    else if (mode == CSVMode.OPENED_RAW)
                    {
                        // If it was opened raw, then just add the quote 
                        content += letter;
                    }
                    else if (mode == CSVMode.CLOSED)
                    {
                        // Otherwise open it as a quote 

                        mode = CSVMode.OPENED_QUOTE;
                    }
                }
            }
            // If its a comma seperator
            else if (letter == ',')
            {
                // If in quote mode
                if (mode == CSVMode.OPENED_QUOTE)
                {
                    // Just read it
                    content += letter;
                }
                // If raw, then close the content
                else if (mode == CSVMode.OPENED_RAW)
                {
                    results.Add(content);

                    content = "";

                    mode = CSVMode.CLOSED;
                }
                // If it was closed, then open it raw
                else if (mode == CSVMode.CLOSED)
                {
                    mode = CSVMode.OPENED_RAW;

                    results.Add(content);

                    content = "";
                }
            }
            else
            {
                // If opened quote, just read it
                if (mode == CSVMode.OPENED_QUOTE)
                {
                    content += letter;
                }
                // If opened raw, then read it
                else if (mode == CSVMode.OPENED_RAW)
                {
                    content += letter;
                }
                // It closed, then open raw
                else if (mode == CSVMode.CLOSED)
                {
                    mode = CSVMode.OPENED_RAW;

                    content += letter;
                }
            }
        }

        // If it was still reading when the buffer finished
        if (mode != CSVMode.CLOSED)
        {
            results.Add(content);
        }

        return results;
    }

0

这是一个简短的解决方案。

                using (TextFieldParser parser = new TextFieldParser(outputLocation))
                 {
                        parser.TextFieldType = FieldType.Delimited;
                        parser.SetDelimiters(",");
                        string[] headers = parser.ReadLine().Split(',');
                        foreach (string header in headers)
                        {
                            dataTable.Columns.Add(header);
                        }
                        while (!parser.EndOfData)
                        {
                            string[] fields = parser.ReadFields();
                            dataTable.Rows.Add(fields);
                        }
                    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.