Qual é a melhor maneira de importar um arquivo CSV para uma estrutura de dados fortemente tipada?
Qual é a melhor maneira de importar um arquivo CSV para uma estrutura de dados fortemente tipada?
Respostas:
O TextFieldParser da Microsoft é estável e segue a RFC 4180 para arquivos CSV. Não desanime pelo Microsoft.VisualBasicnamespace; é um componente padrão no .NET Framework, basta adicionar uma referência ao Microsoft.VisualBasicconjunto global .
Se você está compilando para Windows (em oposição ao Mono) e não prevê ter que analisar arquivos CSV "quebrados" (não compatíveis com RFC), então esta seria a escolha óbvia, pois é gratuito, irrestrito, estável, e ativamente apoiado, a maioria dos quais não pode ser dito para FileHelpers.
Consulte também: How to: Read From Comma-Delimited Text Files in Visual Basic for a VB code example.
TextFieldParserfuncionará para delimitado por tabulação e outros dados estranhos gerados em Excel também. Eu percebo que a sua resposta anterior não estava afirmando que a biblioteca foi específico-VB, ele só veio até mim como implicando que foi realmente significou para VB, e não destina a ser usado em C #, que eu não acho que é o caso - existem algumas classes realmente úteis no MSVB.
Use uma conexão 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();
Se você está esperando cenários bastante complexos para a análise CSV, nem pense em lançar nosso próprio analisador . Existem muitas ferramentas excelentes por aí, como FileHelpers , ou mesmo algumas do CodeProject .
A questão é que este é um problema bastante comum e você pode apostar que muitos desenvolvedores de software já pensaram e resolveram esse problema.
Brian oferece uma boa solução para convertê-lo em uma coleção fortemente tipada.
A maioria dos métodos de análise CSV fornecidos não leva em conta os campos de escape ou algumas das outras sutilezas dos arquivos CSV (como campos de corte). Aqui está o código que eu uso pessoalmente. É um pouco áspero nas bordas e praticamente não tem relatórios de erros.
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;
}
Observe que isso não lida com o caso extremo de campos não serem delimitados por aspas duplas, mas apenas com uma string entre aspas. Veja este post para uma melhor expansão, bem como alguns links para algumas bibliotecas adequadas.
Eu concordo com @NotMyself . FileHelpers é bem testado e lida com todos os tipos de casos extremos com os quais você eventualmente terá que lidar se fizer isso sozinho. Dê uma olhada no que o FileHelpers faz e apenas escreva o seu próprio se você tiver certeza absoluta de que (1) você nunca precisará lidar com os casos extremos que o FileHelpers faz, ou (2) você adora escrever esse tipo de coisa e vai fique muito feliz quando tiver que analisar coisas como esta:
1, "Bill", "Smith", "Supervisor", "Sem comentários"
2, 'Drake,', 'O'Malley', "Zelador,
Ops, não fui citado e estou em uma nova linha!
Eu estava entediado, então modifiquei algumas coisas que escrevi. Ele tenta encapsular a análise de uma maneira OO enquanto corta a quantidade de iterações no arquivo, itera apenas uma vez no primeiro 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]),
};
}
}
}
}
Existem dois artigos sobre CodeProject que fornecem código para uma solução, um que usa StreamReader e outro que importa dados CSV usando o driver de texto da Microsoft .
Uma boa maneira simples de fazer isso é abrir o arquivo e ler cada linha em um array, lista encadeada, estrutura de dados de sua escolha. No entanto, tenha cuidado ao manusear a primeira linha.
Isso pode passar por cima da sua cabeça, mas parece haver uma maneira direta de acessá-los usando uma string de conexão .
Por que não tentar usar Python em vez de C # ou VB? Tem um bom módulo CSV para importar que faz todo o trabalho pesado para você.
Tive que usar um analisador CSV em .NET para um projeto neste verão e me conformei com o driver de texto Microsoft Jet. Você especifica uma pasta usando uma string de conexão e consulta um arquivo usando uma instrução SQL Select. Você pode especificar tipos fortes usando um arquivo schema.ini. Não fiz isso no início, mas depois estava obtendo resultados ruins em que o tipo de dados não era imediatamente aparente, como números de IP ou uma entrada como "XYQ 3.9 SP1".
Uma limitação que encontrei é que ele não pode lidar com nomes de colunas com mais de 64 caracteres; ele trunca. Isso não deve ser um problema, exceto que eu estava lidando com dados de entrada muito mal projetados. Ele retorna um ADO.NET DataSet.
Essa foi a melhor solução que encontrei. Eu desconfiaria de lançar meu próprio analisador CSV, já que provavelmente perderia alguns dos casos finais e não encontrei nenhum outro pacote de análise CSV gratuito para .NET por aí.
EDIT: Além disso, só pode haver um arquivo schema.ini por diretório, então eu anexei dinamicamente a ele para digitar fortemente as colunas necessárias. Ele só digitará fortemente as colunas especificadas e fará a inferência para qualquer campo não especificado. Eu realmente gostei disso, pois estava lidando com a importação de um CSV de coluna 70+ de fluido e não queria especificar cada coluna, apenas as que se comportavam mal.
Eu digitei algum código. O resultado no datagridviewer parecia bom. Ele analisa uma única linha de texto para uma lista de arraylist de objetos.
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;
}
Se você pode garantir que não há vírgulas nos dados, a maneira mais simples provavelmente seria usar String.split .
Por exemplo:
String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);
Pode haver bibliotecas que você possa usar para ajudar, mas provavelmente é o mais simples possível. Apenas certifique-se de que não haja vírgulas nos dados, caso contrário, você precisará analisá-los melhor.