Opciones de análisis de CSV con .NET [cerrado]
Frecuentes
Visto 37,338 veces
14
Estoy mirando mi archivo delimitado (por ejemplo, CSV, tabulado separado, etc.) opciones de análisis basadas en la pila de MS en general, y .net específicamente. La única tecnología que excluyo es SSIS, porque ya sé que no satisfará mis necesidades.
Así que mis opciones parecen ser:
- Regex.Split
- Analizador de campos de texto
- Analizador OLEDB CSV
Tengo dos criterios que debo cumplir. Primero, dado el siguiente archivo que contiene dos filas lógicas de datos (y cinco filas físicas en total):
101, Bob, "Keeps his house ""clean"".
Needs to work on laundry."
102, Amy, "Brilliant.
Driven.
Diligent."
Los resultados analizados deben generar dos "filas" lógicas, que constan de tres cadenas (o columnas) cada una. ¡La cadena de la tercera fila/columna debe conservar las líneas nuevas! Dicho de otra manera, el analizador debe reconocer cuándo las líneas "continúan" en la siguiente fila física, debido al calificador de texto "no cerrado".
El segundo criterio es que el delimitador y calificador de texto debe ser configurable, por archivo. Aquí hay dos cadenas, tomadas de diferentes archivos, que debo poder analizar:
var first = @"""This"",""Is,A,Record"",""That """"Cannot"""", they say,"","""",,""be"",rightly,""parsed"",at all";
var second = @"~This~|~Is|A|Record~|~ThatCannot~|~be~|~parsed~|at all";
Un análisis adecuado de la cadena "primero" sería:
- Este
- Es,A,Registro
- Ese "no se puede", dicen,
- _
- _
- be
- correctamente
- analizado
- en absoluto
El '_' simplemente significa que se capturó un espacio en blanco; no quiero que aparezca un subrayado literal.
Se puede hacer una suposición importante sobre los archivos planos que se analizarán: habrá un número fijo de columnas por archivo.
Ahora, para una inmersión en las opciones técnicas.
REGEX
Primero, muchos respondedores comentan que la expresión regular "no es la mejor manera" de lograr el objetivo. Sin embargo, encontré un comentarista que ofreció una excelente expresión regular CSV:
var regex = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
var Regex.Split(first, regex).Dump();
Los resultados, aplicados a la cadena "primero", son maravillosos:
- "Esta"
- "Es, un, registro"
- "Ese ""No Puede"", dicen, "
- ""
- _
- "ser"
- correctamente
- "analizado"
- en absoluto
Sería bueno si se limpiaran las comillas, pero puedo manejar eso fácilmente como un paso posterior al proceso. De lo contrario, este enfoque se puede usar para analizar las cadenas de muestra "primero" y "segundo", siempre que la expresión regular se modifique para los símbolos de tilde y tubería en consecuencia. ¡Excelente!
Pero el verdadero problema se refiere a los criterios de varias líneas. Antes de que se pueda aplicar una expresión regular a una cadena, debo leer la "fila" lógica completa del archivo. Desafortunadamente, no sé cuántas filas físicas leer para completar la fila lógica, a menos que tenga una máquina de estado/regex.
Así que esto se convierte en un problema de "el huevo y la gallina". Mi mejor opción sería leer todo el archivo en la memoria como una cadena gigante y dejar que la expresión regular clasifique las líneas múltiples (no verifiqué si la expresión regular anterior podía manejar eso). Si tengo un archivo de 10 gigas, esto podría ser un poco precario.
A la siguiente opción.
Analizador de campos de texto
Tres líneas de código harán evidente el problema con esta opción:
var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser(stream);
reader.Delimiters = new string[] { @"|" };
reader.HasFieldsEnclosedInQuotes = true;
La configuración de los delimitadores se ve bien. Sin embargo, "HasFieldsEnclosedInQuotes" es "game over". Me sorprende que los delimitadores se puedan configurar arbitrariamente, pero, por el contrario, no tengo otra opción de calificación aparte de las comillas. Recuerde, necesito configurabilidad sobre el calificador de texto. Entonces, de nuevo, a menos que alguien sepa un truco de configuración de TextFieldParser, se acabó el juego.
OLE DB
Un colega me dice que esta opción tiene dos fallas importantes. Primero, tiene un rendimiento terrible para archivos grandes (por ejemplo, 10 gigas). En segundo lugar, según me dijeron, adivina los tipos de datos de los datos de entrada en lugar de permitirle especificar. No es bueno.
AYUDA
Así que me gustaría saber los hechos en los que me equivoqué (si los hay) y las otras opciones que me perdí. Tal vez alguien conozca una forma de manipular TextFieldParser para usar un delimitador arbitrario. Y tal vez OLEDB haya resuelto los problemas mencionados (¿o quizás nunca los haya tenido?).
¿Que dices tu?
3 Respuestas
6
¿Intentó buscar un .NET ya existente? Analizador CSV? Este afirma manejar registros de varias líneas significativamente más rápido que OLEDB.
respondido 09 mar '12, 23:03
FastCSV es una biblioteca bastante bien aceptada. - Joel Coehoorn
Sí, miré un poco, por eso mencioné tres opciones. El problema es que hay muchas más opciones, pero si cumplen o no con mis criterios es un proceso muy lento. Espero que alguien ya sepa la opción correcta. - Brent Arias
solía nuget.org/packages/CsvHelper basado puramente en la cantidad de descargas en nuget (enlace de búsqueda nuget.org/packages?q=csv) - xhafán
4
Escribí esto hace un tiempo como un analizador CSV liviano e independiente. Creo que cumple con todos sus requisitos. Pruébalo sabiendo que probablemente no sea a prueba de balas.
Si funciona para usted, siéntase libre de cambiar el espacio de nombres y usarlo sin restricciones.
namespace NFC.Portability
{
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
/// <summary>
/// Loads and reads a file with comma-separated values into a tabular format.
/// </summary>
/// <remarks>
/// Parsing assumes that the first line will always contain headers and that values will be double-quoted to escape double quotes and commas.
/// </remarks>
public unsafe class CsvReader
{
private const char SEGMENT_DELIMITER = ',';
private const char DOUBLE_QUOTE = '"';
private const char CARRIAGE_RETURN = '\r';
private const char NEW_LINE = '\n';
private DataTable _table = new DataTable();
/// <summary>
/// Gets the data contained by the instance in a tabular format.
/// </summary>
public DataTable Table
{
get
{
// validation logic could be added here to ensure that the object isn't in an invalid state
return _table;
}
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="path">The fully-qualified path to the file from which the instance will be populated.</param>
public CsvReader( string path )
{
if( path == null )
{
throw new ArgumentNullException( "path" );
}
FileStream fs = new FileStream( path, FileMode.Open );
Read( fs );
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="stream">The stream from which the instance will be populated.</param>
public CsvReader( Stream stream )
{
if( stream == null )
{
throw new ArgumentNullException( "stream" );
}
Read( stream );
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="bytes">The array of bytes from which the instance will be populated.</param>
public CsvReader( byte[] bytes )
{
if( bytes == null )
{
throw new ArgumentNullException( "bytes" );
}
MemoryStream ms = new MemoryStream();
ms.Write( bytes, 0, bytes.Length );
ms.Position = 0;
Read( ms );
}
private void Read( Stream s )
{
string lines;
using( StreamReader sr = new StreamReader( s ) )
{
lines = sr.ReadToEnd();
}
if( string.IsNullOrWhiteSpace( lines ) )
{
throw new InvalidOperationException( "Data source cannot be empty." );
}
bool inQuotes = false;
int lineNumber = 0;
StringBuilder buffer = new StringBuilder( 128 );
List<string> values = new List<string>();
Action endSegment = () =>
{
values.Add( buffer.ToString() );
buffer.Clear();
};
Action endLine = () =>
{
if( lineNumber == 0 )
{
CreateColumns( values );
values.Clear();
}
else
{
CreateRow( values );
values.Clear();
}
values.Clear();
lineNumber++;
};
fixed( char* pStart = lines )
{
char* pChar = pStart;
char* pEnd = pStart + lines.Length;
while( pChar < pEnd ) // leave null terminator out
{
if( *pChar == DOUBLE_QUOTE )
{
if( inQuotes )
{
if( Peek( pChar, pEnd ) == SEGMENT_DELIMITER )
{
endSegment();
pChar++;
}
else if( !ApproachingNewLine( pChar, pEnd ) )
{
buffer.Append( DOUBLE_QUOTE );
}
}
inQuotes = !inQuotes;
}
else if( *pChar == SEGMENT_DELIMITER )
{
if( !inQuotes )
{
endSegment();
}
else
{
buffer.Append( SEGMENT_DELIMITER );
}
}
else if( AtNewLine( pChar, pEnd ) )
{
if( !inQuotes )
{
endSegment();
endLine();
//pChar++;
}
else
{
buffer.Append( *pChar );
}
}
else
{
buffer.Append( *pChar );
}
pChar++;
}
}
// append trailing values at the end of the file
if( values.Count > 0 )
{
endSegment();
endLine();
}
}
/// <summary>
/// Returns the next character in the sequence but does not advance the pointer. Checks bounds.
/// </summary>
/// <param name="pChar">Pointer to current character.</param>
/// <param name="pEnd">End of range to check.</param>
/// <returns>
/// Returns the next character in the sequence, or char.MinValue if range is exceeded.
/// </returns>
private char Peek( char* pChar, char* pEnd )
{
if( pChar < pEnd )
{
return *( pChar + 1 );
}
return char.MinValue;
}
/// <summary>
/// Determines if the current character represents a newline. This includes lookahead for two character newline delimiters.
/// </summary>
/// <param name="pChar"></param>
/// <param name="pEnd"></param>
/// <returns></returns>
private bool AtNewLine( char* pChar, char* pEnd )
{
if( *pChar == NEW_LINE )
{
return true;
}
if( *pChar == CARRIAGE_RETURN && Peek( pChar, pEnd ) == NEW_LINE )
{
return true;
}
return false;
}
/// <summary>
/// Determines if the next character represents a newline, or the start of a newline.
/// </summary>
/// <param name="pChar"></param>
/// <param name="pEnd"></param>
/// <returns></returns>
private bool ApproachingNewLine( char* pChar, char* pEnd )
{
if( Peek( pChar, pEnd ) == CARRIAGE_RETURN || Peek( pChar, pEnd ) == NEW_LINE )
{
// technically this cheats a little to avoid a two char peek by only checking for a carriage return or new line, not both in sequence
return true;
}
return false;
}
private void CreateColumns( List<string> columns )
{
foreach( string column in columns )
{
DataColumn dc = new DataColumn( column );
_table.Columns.Add( dc );
}
}
private void CreateRow( List<string> values )
{
if( values.Where( (o) => !string.IsNullOrWhiteSpace( o ) ).Count() == 0 )
{
return; // ignore rows which have no content
}
DataRow dr = _table.NewRow();
_table.Rows.Add( dr );
for( int i = 0; i < values.Count; i++ )
{
dr[i] = values[i];
}
}
}
}
Respondido 27 Jul 18, 11:07
1
Echa un vistazo al código que publiqué en esta pregunta:
Cubre MEJOR DE TU de sus requisitos, y no se necesitaría mucho para actualizarlo para admitir delimitadores alternativos o calificadores de texto.
contestado el 23 de mayo de 17 a las 11:05
No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas c# .net parsing or haz tu propia pregunta.
¿Ha probado las opciones enumeradas en stackoverflow.com/questions/316649/csv-parsing ? - TrueWill
Estoy de acuerdo con @ Appleman1234, Filehelpers debería ser todo lo que necesita: Kane
¿Afecta la regulación de la Ayudantes de archivos cumplir con sus requisitos? - Appleman1234
posible duplicado de Leer archivos CSV en C # - Dour High Arch