将 CSV 文件导入强类型数据结构的最佳方法是什么?
11 回答
Microsoft 的TextFieldParser是稳定的,并遵循CSV 文件的RFC 4180。不要被Microsoft.VisualBasic
命名空间推迟;它是 .NET Framework 中的标准组件,只需添加对全局Microsoft.VisualBasic
程序集的引用。
如果您正在为 Windows(而不是 Mono)进行编译,并且预计不必解析“损坏”(不符合 RFC)的 CSV 文件,那么这将是显而易见的选择,因为它是免费的、不受限制的、稳定的,并积极支持,其中大部分对于 FileHelpers 来说是不能说的。
另请参阅:如何:在 Visual Basic 中读取逗号分隔的文本文件以获取 VB 代码示例。
使用 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();
如果您期望 CSV 解析的场景相当复杂,甚至不要考虑推出我们自己的 parser。那里有很多优秀的工具,比如FileHelpers,甚至是来自CodeProject的工具。
关键是这是一个相当普遍的问题,你可以打赌很多软件开发人员已经考虑并解决了这个问题。
我同意@NotMyself。FileHelpers经过了良好的测试,可以处理各种边缘情况,如果您自己动手,您最终将不得不处理这些情况。看看 FileHelpers 做了什么,只有在你完全确定以下两种情况之一时才编写自己的:(1)你永远不需要处理 FileHelpers 所做的边缘情况,或者(2)你喜欢写这种东西并且会去当你必须解析这样的东西时会大喜过望:
1、“比尔”、“史密斯”、“主管”、“无评论”
2,“德雷克”,“奥马利”,“看门人”,
糟糕,我没有被引用,而且我在换行!
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 在其中包含带引号的字符串。请参阅这篇文章以获得更好的扩展以及一些适当库的链接。
我很无聊,所以我修改了一些我写的东西。它尝试以 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]),
};
}
}
}
}
CodeProject 上有两篇文章为解决方案提供代码,一篇使用StreamReader,另一篇使用Microsoft Text Driver导入 CSV 数据。
一个很好的简单方法是打开文件,并将每一行读入数组、链表、数据结构。不过要小心处理第一行。
这可能超出您的想象,但似乎有一种直接的方法可以使用连接字符串来访问它们。
为什么不尝试使用 Python 而不是 C# 或 VB?它有一个不错的 CSV 模块可以导入,可以为您完成所有繁重的工作。
今年夏天,我不得不在 .NET 中为一个项目使用 CSV 解析器,并选择了 Microsoft Jet Text Driver。您使用连接字符串指定文件夹,然后使用 SQL Select 语句查询文件。您可以使用 schema.ini 文件指定强类型。起初我没有这样做,但后来我得到了不好的结果,其中数据的类型没有立即显现出来,例如 IP 号码或像“XYQ 3.9 SP1”这样的条目。
我遇到的一个限制是它不能处理超过 64 个字符的列名。它截断。这应该不是问题,除非我处理的是设计得很糟糕的输入数据。它返回一个 ADO.NET 数据集。
这是我找到的最佳解决方案。我会谨慎使用自己的 CSV 解析器,因为我可能会错过一些最终情况,而且我没有找到任何其他适用于 .NET 的免费 CSV 解析包。
编辑:另外,每个目录只能有一个 schema.ini 文件,所以我动态地附加到它以强类型化所需的列。它只会强类型指定的列,并推断任何未指定的字段。我真的很感激这一点,因为我正在处理导入 70+ 列 CSV 并且不想指定每一列,只指定行为不端的列。
I typed in some code. The result in the datagridviewer looked good. It parses a single line of text to an arraylist of objects.
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;
}
如果您可以保证数据中没有逗号,那么最简单的方法可能是使用String.split。
例如:
String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);
可能有您可以使用的库来提供帮助,但这可能很简单。只要确保数据中不能有逗号,否则您需要更好地解析它。