将CSV文件导入.Net中的强类型数据结构


106

将CSV文件导入到强类型数据结构中的最佳方法是什么?




7
考虑到这是早于1103495年创建的,因此我认为这个问题是重复的。
MattH

2
谢谢,马特。我只是试图将它们链接在一起,而不是指出先到者。您会看到我在指向该问题的另一个问题上有完全相同的文字,是否有更好的方法将两个问题联系在一起?
Mark Meuer 2011年

Answers:


74

微软的TextFieldParser是稳定的,并且遵循RFC 4180的CSV文件格式。不要被Microsoft.VisualBasic命名空间推迟;它是.NET Framework中的标准组件,只需添加对全局Microsoft.VisualBasic程序集的引用即可。

如果您要针对Windows(而不是Mono)进行编译,并且不希望解析“破碎的”(不符合RFC要求)CSV文件,那么这将是显而易见的选择,因为它是免费,不受限制,稳定,并得到积极的支持,其中FileHelpers绝不能说大部分。

另请参见:如何:从Visual Basic中的逗号分隔的文本文件中读取 VB代码示例。


2
除了不幸的是命名空间,这个类实际上没有VB特有的。如果我只需要“简单的” CSV解析器,我肯定会选择该库,因为通常无需下载,分发或担心。为此,我根据该答案编辑了以VB为重点的词组。
亚伦诺特2011年

@Aaronaught我认为您的编辑大部分都是改进。尽管该RFC不一定具有权威性,但由于许多CSV编写者并不遵守该RFC,例如Excel 并不总是在“ CSV”文件中使用逗号。我以前的回答还不是说可以在C#中使用该类?
MarkJ 2011年

TextFieldParser用于制表符分隔和其它怪异的Excel生成的克鲁夫特也能工作。我意识到您以前的回答并不是说该库是VB特定的,而是对我而言,这暗示它确实 VB的意思,而不是打算在C#中使用,我认为这不是情况-MSVB中有一些非常有用的类。
亚伦诺特,2011年

21

使用OleDB连接。

String sConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\InputDirectory\\;Extended Properties='text;HDR=Yes;FMT=Delimited'";
OleDbConnection objConn = new OleDbConnection(sConnectionString);
objConn.Open();
DataTable dt = new DataTable();
OleDbCommand objCmdSelect = new OleDbCommand("SELECT * FROM file.csv", objConn);
OleDbDataAdapter objAdapter1 = new OleDbDataAdapter();
objAdapter1.SelectCommand = objCmdSelect;
objAdapter1.Fill(dt);
objConn.Close();

这需要文件系统访问。据我所知,没有办法使OLEDB与内存流一起工作:(
UserControl 2012年

3
@UserControl,当然它需要文件系统访问权限。他问有关导入CSV文件的问题
凯文(Kevin)

1
我没有抱怨。实际上,我比其他人更喜欢OLEDB解决方案,但是当我需要在ASP.NET应用程序中解析CSV时,我感到非常沮丧,因此想记一下。
UserControl 2012年

12

如果您期望CSV解析的场景相当复杂,那么甚至不要想起我们自己的解析器。有很多出色的工具,例如FileHelpers甚至是CodeProject的工具

关键是这是一个相当普遍的问题,您可以打赌很多软件开发人员已经考虑并解决了这个问题。


尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。- 来自评论
techspider '16

感谢@techspider,我希望你能注意到这篇文章是从StackOverflow的beta时期开始的:D话虽如此,如今CSV工具最好来自Nuget软件包-所以我不确定甚至连答案都可以不受8年的影响的技术发展周期
乔恩·利姆贾普

9

Brian为将其转换为强类型集合提供了一个很好的解决方案。

给出的大多数CSV解析方法都没有考虑转义字段或CSV文件的其他一些细微差别(例如修剪字段)。这是我个人使用的代码。边缘有些粗糙,几乎没有错误报告。

public static IList<IList<string>> Parse(string content)
{
    IList<IList<string>> records = new List<IList<string>>();

    StringReader stringReader = new StringReader(content);

    bool inQoutedString = false;
    IList<string> record = new List<string>();
    StringBuilder fieldBuilder = new StringBuilder();
    while (stringReader.Peek() != -1)
    {
        char readChar = (char)stringReader.Read();

        if (readChar == '\n' || (readChar == '\r' && stringReader.Peek() == '\n'))
        {
            // If it's a \r\n combo consume the \n part and throw it away.
            if (readChar == '\r')
            {
                stringReader.Read();
            }

            if (inQoutedString)
            {
                if (readChar == '\r')
                {
                    fieldBuilder.Append('\r');
                }
                fieldBuilder.Append('\n');
            }
            else
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();

                records.Add(record);
                record = new List<string>();

                inQoutedString = false;
            }
        }
        else if (fieldBuilder.Length == 0 && !inQoutedString)
        {
            if (char.IsWhiteSpace(readChar))
            {
                // Ignore leading whitespace
            }
            else if (readChar == '"')
            {
                inQoutedString = true;
            }
            else if (readChar == ',')
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();
            }
            else
            {
                fieldBuilder.Append(readChar);
            }
        }
        else if (readChar == ',')
        {
            if (inQoutedString)
            {
                fieldBuilder.Append(',');
            }
            else
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();
            }
        }
        else if (readChar == '"')
        {
            if (inQoutedString)
            {
                if (stringReader.Peek() == '"')
                {
                    stringReader.Read();
                    fieldBuilder.Append('"');
                }
                else
                {
                    inQoutedString = false;
                }
            }
            else
            {
                fieldBuilder.Append(readChar);
            }
        }
        else
        {
            fieldBuilder.Append(readChar);
        }
    }
    record.Add(fieldBuilder.ToString().TrimEnd());
    records.Add(record);

    return records;
}

请注意,这不能处理没有用双引号引起来的字段的极端情况,而是在其中用引号引起来的字符串meerley。有关更好的扩展以及一些适当库的链接,请参见本文


9

我同意@NotMyselfFileHelpers经过了良好的测试,可以处理各种边缘情况,如果您自己这样做,最终将不得不处理这些情况。看一下FileHelpers的功能,只有在绝对确定(1)您将永远不需要处理FileHelpers的极端情况时,或者(2)您喜欢编写此类内容并且打算当您必须解析如下内容时,您会大喜过望:

1,“比尔”,“史密斯”,“主管”,“无评论”

2,“德雷克”,“奥马利”,“管理员”

糟糕,我没有被引用,我正在换行!


6

我很无聊,所以我修改了我写的一些东西。它尝试以OO方式封装解析,以减少遍历文件的迭代次数,它仅在顶部foreach迭代一次。

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.IO;

namespace ConsoleApplication1
{
    class Program
    {

        static void Main(string[] args)
        {

            // usage:

            // note this wont run as getting streams is not Implemented

            // but will get you started

            CSVFileParser fileParser = new CSVFileParser();

            // TO Do:  configure fileparser

            PersonParser personParser = new PersonParser(fileParser);

            List<Person> persons = new List<Person>();
            // if the file is large and there is a good way to limit
            // without having to reparse the whole file you can use a 
            // linq query if you desire
            foreach (Person person in personParser.GetPersons())
            {
                persons.Add(person);
            }

            // now we have a list of Person objects
        }
    }

    public abstract  class CSVParser 
    {

        protected String[] deliniators = { "," };

        protected internal IEnumerable<String[]> GetRecords()
        {

            Stream stream = GetStream();
            StreamReader reader = new StreamReader(stream);

            String[] aRecord;
            while (!reader.EndOfStream)
            {
                  aRecord = reader.ReadLine().Split(deliniators,
                   StringSplitOptions.None);

                yield return aRecord;
            }

        }

        protected abstract Stream GetStream(); 

    }

    public class CSVFileParser : CSVParser
    {
        // to do: add logic to get a stream from a file

        protected override Stream GetStream()
        {
            throw new NotImplementedException();
        } 
    }

    public class CSVWebParser : CSVParser
    {
        // to do: add logic to get a stream from a web request

        protected override Stream GetStream()
        {
            throw new NotImplementedException();
        }
    }

    public class Person
    {
        public String Name { get; set; }
        public String Address { get; set; }
        public DateTime DOB { get; set; }
    }

    public class PersonParser 
    {

        public PersonParser(CSVParser parser)
        {
            this.Parser = parser;
        }

        public CSVParser Parser { get; set; }

        public  IEnumerable<Person> GetPersons()
        {
            foreach (String[] record in this.Parser.GetRecords())
            {
                yield return new Person()
                {
                    Name = record[0],
                    Address = record[1],
                    DOB = DateTime.Parse(record[2]),
                };
            }
        }
    }
}


2

一种简单的好方法是打开文件,然后将每一行读入一个数组,链接列表以及您选择的数据结构。但是要小心处理第一行。

这可能很麻烦,但是似乎也可以使用连接字符串来直接访问它们。

为什么不尝试使用Python代替C#或VB?它有一个不错的CSV模块导入,可以为您完成所有繁重的工作。


1
不要为了CSV解析器而从VB跳到python。VB中有一个。尽管很奇怪,但在该问题的答案中似乎忽略了它。msdn.microsoft.com/en-us/library/…–
MarkJ

1

今年夏天,我不得不在.NET中为项目使用CSV解析器,并决定使用Microsoft Jet文本驱动程序。您可以使用连接字符串指定文件夹,然后使用SQL Select语句查询文件。您可以使用schema.ini文件指定强类型。起初我没有这样做,但是后来我得到了不好的结果,即数据类型不是立即显而易见的,例如IP号或诸如“ XYQ 3.9 SP1”的条目。

我遇到的一个限制是它不能处理超过64个字符的列名。它会截断。这应该不是问题,除非我正在处理设计很差的输入数据。它返回一个ADO.NET数据集。

这是我发现的最佳解决方案。我会警惕使用自己的CSV解析器,因为我可能会错过一些最终案例,而且我没有找到用于.NET的任何其他免费CSV解析包。

编辑:此外,每个目录只能有一个schema.ini文件,因此我将动态地附加到该文件以强烈键入所需的列。它只会强类型化指定的列,并推断任何未指定的字段。我真的很感激,因为我正在处理导入70+列的连续CSV且不想指定每个列,而只指定行为不佳的列。


为什么不内置CSV解析器的VB.NET?msdn.microsoft.com/en-us/library/…–
MarkJ

1

我输入了一些代码。datagridviewer中的结果看起来不错。它将一行文本解析为对象的数组列表。

    enum quotestatus
    {
        none,
        firstquote,
        secondquote
    }
    public static System.Collections.ArrayList Parse(string line,string delimiter)
    {        
        System.Collections.ArrayList ar = new System.Collections.ArrayList();
        StringBuilder field = new StringBuilder();
        quotestatus status = quotestatus.none;
        foreach (char ch in line.ToCharArray())
        {                                
            string chOmsch = "char";
            if (ch == Convert.ToChar(delimiter))
            {
                if (status== quotestatus.firstquote)
                {
                    chOmsch = "char";
                }                         
                else
                {
                    chOmsch = "delimiter";                    
                }                    
            }

            if (ch == Convert.ToChar(34))
            {
                chOmsch = "quotes";           
                if (status == quotestatus.firstquote)
                {
                    status = quotestatus.secondquote;
                }
                if (status == quotestatus.none )
                {
                    status = quotestatus.firstquote;
                }
            }

            switch (chOmsch)
            {
                case "char":
                    field.Append(ch);
                    break;
                case "delimiter":                        
                    ar.Add(field.ToString());
                    field.Clear();
                    break;
                case "quotes":
                    if (status==quotestatus.firstquote)
                    {
                        field.Clear();                            
                    }
                    if (status== quotestatus.secondquote)
                    {                                                                           
                            status =quotestatus.none;                                
                    }                    
                    break;
            }
        }
        if (field.Length != 0)            
        {
            ar.Add(field.ToString());                
        }           
        return ar;
    }

0

如果可以保证数据中没有逗号,则最简单的方法可能是使用String.split

例如:

String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);

可能有一些库可以用来帮助您,但是您可能会很简单。只要确保您的数据中不能包含逗号,否则就需要更好地解析它。


这不是最佳解决方案
轮回危机

在内存使用和很多开销上都非常糟糕。小应该少一些,几千字节。对于10mb的csv绝对不好!
Piotr Kula 2012年

这取决于您的内存和文件的大小。
tonymiao15年
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.