¿Cuál es la mejor manera de importar un archivo CSV en una estructura de datos fuertemente tipada?
¿Cuál es la mejor manera de importar un archivo CSV en una estructura de datos fuertemente tipada?
Respuestas:
TextFieldParser de Microsoft es estable y sigue RFC 4180 para archivos CSV. No se deje intimidar por el Microsoft.VisualBasic
espacio de nombres; es un componente estándar en .NET Framework, solo agregue una referencia al Microsoft.VisualBasic
ensamblado global .
Si está compilando para Windows (a diferencia de Mono) y no anticipa tener que analizar archivos CSV "rotos" (no compatibles con RFC), entonces esta sería la opción obvia, ya que es gratis, sin restricciones, estable, y con soporte activo, la mayoría de los cuales no se pueden decir de FileHelpers.
Consulte también: Cómo: Leer archivos de texto delimitados por comas en Visual Basic para ver un ejemplo de código VB.
TextFieldParser
también funcionará para archivos cruft delimitados por tabuladores y otros extraños generados por Excel. Me doy cuenta de que su respuesta anterior no afirmaba que la biblioteca fuera específica de VB, simplemente me pareció que implicaba que realmente estaba destinada a VB y no estaba destinada a ser utilizada desde C #, lo que no creo que sea el caso - hay algunas clases realmente útiles en MSVB.
Utilice una conexión 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();
Si espera escenarios bastante complejos para el análisis de CSV, ni siquiera piense en ejecutar nuestro propio analizador . Hay muchas herramientas excelentes, como FileHelpers o incluso las de CodeProject .
El punto es que este es un problema bastante común y podría apostar a que muchos desarrolladores de software ya han pensado y resuelto este problema.
Brian ofrece una buena solución para convertirlo en una colección fuertemente tipada.
La mayoría de los métodos de análisis de CSV proporcionados no tienen en cuenta los campos de escape o algunas de las otras sutilezas de los archivos CSV (como los campos de recorte). Aquí está el código que utilizo personalmente. Es un poco tosco en los bordes y prácticamente no tiene informes de errores.
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;
}
Tenga en cuenta que esto no maneja el caso límite de los campos que no están delimitados por comillas dobles, sino que simplemente tienen una cadena entre comillas dentro. Vea esta publicación para una mejor expansión, así como algunos enlaces a algunas bibliotecas adecuadas.
Estoy de acuerdo con @ NotMyself . FileHelpers está bien probado y maneja todo tipo de casos que eventualmente tendrá que lidiar si lo hace usted mismo. Eche un vistazo a lo que hace FileHelpers y solo escriba el suyo propio si está absolutamente seguro de que (1) nunca necesitará manejar los casos extremos que FileHelpers hace, o (2) le encanta escribir este tipo de cosas y va a regocíjese cuando tenga que analizar cosas como esta:
1, "Bill", "Smith", "Supervisor", "Sin comentarios"
2, 'Drake', 'O'Malley', "Conserje,
¡Vaya, no estoy cotizado y estoy en una nueva línea!
Estaba aburrido, así que modifiqué algunas cosas que escribí. Intenta encapsular el análisis sintáctico de manera orientada a objetos mientras reduce la cantidad de iteraciones a través del archivo, solo itera una vez en la parte superior de cada uno.
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]),
};
}
}
}
}
Hay dos artículos en CodeProject que proporcionan código para una solución, uno que usa StreamReader y otro que importa datos CSV usando Microsoft Text Driver .
Una buena forma sencilla de hacerlo es abrir el archivo y leer cada línea en una matriz, lista vinculada, estructura de datos de su elección. Sin embargo, tenga cuidado con el manejo de la primera línea.
Esto puede estar por encima de su cabeza, pero parece haber una forma directa de acceder a ellos también usando una cadena de conexión .
¿Por qué no intentar usar Python en lugar de C # o VB? Tiene un buen módulo CSV para importar que hace todo el trabajo pesado por usted.
Tuve que usar un analizador CSV en .NET para un proyecto este verano y me decidí por Microsoft Jet Text Driver. Usted especifica una carpeta usando una cadena de conexión, luego consulta un archivo usando una instrucción SQL Select. Puede especificar tipos seguros mediante un archivo schema.ini. No hice esto al principio, pero luego estaba obteniendo malos resultados donde el tipo de datos no era evidente de inmediato, como números de IP o una entrada como "XYQ 3.9 SP1".
Una limitación con la que me encontré es que no puede manejar nombres de columna de más de 64 caracteres; se trunca. Esto no debería ser un problema, excepto que estaba tratando con datos de entrada muy mal diseñados. Devuelve un conjunto de datos ADO.NET.
Esta fue la mejor solución que encontré. Sería cauteloso al usar mi propio analizador CSV, ya que probablemente perdería algunos de los casos finales, y no encontré ningún otro paquete de análisis CSV gratuito para .NET por ahí.
EDITAR: Además, solo puede haber un archivo schema.ini por directorio, por lo que lo agregué dinámicamente para escribir firmemente las columnas necesarias. Solo escribirá fuertemente las columnas especificadas e inferirá para cualquier campo no especificado. Realmente aprecié esto, ya que estaba tratando con la importación de un CSV fluido de 70+ columnas y no quería especificar cada columna, solo las que se comportaban mal.
Escribí un código. El resultado en el visor de cuadrícula de datos se veía bien. Analiza una sola línea de texto en una lista de matrices 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;
}
Si puede garantizar que no haya comas en los datos, probablemente la forma más sencilla sería utilizar String.split .
Por ejemplo:
String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);
Es posible que haya bibliotecas que pueda utilizar para ayudar, pero probablemente sea lo más simple que puede conseguir. Solo asegúrese de no tener comas en los datos, de lo contrario tendrá que analizarlos mejor.