2012-03-09 20 views
10

Estoy viendo mis opciones de análisis de archivos delimitados (por ejemplo, CSV, separadores de pestañas, etc.) basados ​​en la pila MS en general, y .net específicamente. La única tecnología que excluyo es SSIS, porque ya sé que no satisfará mis necesidades.Opciones de análisis CSV con .NET

Así que mis opciones parecen ser:

  1. Regex.Split
  2. TextFieldParser
  3. OLEDB CSV Parser

Tengo dos criterios que deben cumplir. En primer lugar, dado el siguiente archivo que contiene dos filas lógicas de datos (y cinco filas físicos en total):

101, Bob, "Keeps his house ""clean"".
Needs to work on laundry."
102, Amy, "Brilliant.
Driven.
Diligent."

Los resultados analizados deben ceder dos lógicas "filas", que consta de tres cadenas (o columnas) cada uno . ¡La tercera cadena de fila/columna debe preservar las nuevas líneas! 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 el calificador de texto deben ser configurables, por archivo. Aquí hay dos secuencias, tomadas de diferentes archivos, que debo ser capaz de 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 cadena "primera" sería:

  • Este
  • es decir, una, registro
  • que "no se puede", dicen,
  • _
  • _
  • sea
  • razón
  • Analizada
  • en absoluto

El '_' significa simplemente que un espacio en blanco fue capturado - No quiero que aparezca un guión bajo 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 profundizar en las opciones técnicas.

expresiones regulares

En primer lugar, muchos comentan que los respondedores de expresiones regulares "no es el mejor camino" para lograr el objetivo.Yo, sin embargo, encontrar un commenter who offered an excellent CSV regex:

var regex = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))"; 
var Regex.Split(first, regex).Dump(); 

Los resultados, aplicado a la cadena "primero", son absolutamente maravilloso:

  • "Este"
  • "Es decir, una, Record"
  • "Eso "" no puede" "dicen,"
  • ""
  • _
  • "b e"
  • razón
  • 'analizada'
  • en absoluto

Sería bueno si las cotizaciones se limpiaron, pero se puede tratar fácilmente con eso como un paso posterior del proceso. De lo contrario, este enfoque se puede utilizar para analizar ambas cadenas de muestras "primero" y "segundo", siempre que la expresión regular se modifique para símbolos de tilde y de tubería en consecuencia. ¡Excelente!

Pero el problema real se relaciona con 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. Lamentablemente, no sé cuántas filas físicas leer para completar la fila lógica, a menos que tenga una máquina de expresiones regulares/estado.

Esto se convierte en un problema de "pollo y huevo". 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 revisé si la expresión regular anterior podía manejar eso). Si tengo un archivo de 10 gigas, esto podría ser un poco precario.

Activa la siguiente opción.

TextFieldParser

Tres líneas de código hará que el problema con esta opción aparente:

var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser(stream); 
reader.Delimiters = new string[] { @"|" }; 
reader.HasFieldsEnclosedInQuotes = true; 

La configuración delimitadores se ve bien. Sin embargo, el "HasFieldsEnclosedInQuotes" es "se acabó el juego". Me sorprende que los delimitadores sean arbitrariamente configurables, pero en cambio no tengo otra opción de calificador que no sean las citas. Recuerde, necesito configurabilidad sobre el calificador de texto. Entonces, de nuevo, a menos que alguien conozca un truco de configuración de TextFieldParser, se acabó el juego.

OLEDB

Un colega me dice que esta opción tiene dos defectos principales. En primer lugar, tiene un rendimiento terrible para archivos grandes (por ejemplo, 10 gigas). En segundo lugar, me dicen, adivina los tipos de datos de datos de entrada en lugar de permitirte especificarlos. No está bien.

AYUDA

así que me gustaría saber los hechos me dieron mal (si lo hay), y las otras opciones que me perdí. Tal vez alguien sepa una forma de jurado de TextFieldParser para usar un delimitador arbitrario. Y tal vez OLEDB ha resuelto los problemas establecidos (¿o quizás nunca los tuvo?).

¿Qué dices?

+0

¿Has probado las opciones enumeradas en http://stackoverflow.com/questions/316649/csv-parsing? – TrueWill

+0

Estoy de acuerdo con @ Appleman1234, Filehelpers debería ser todo lo que necesita – Kane

+0

¿[Filehelpers] (http://www.filehelpers.com/) cumple con sus requisitos? – Appleman1234

Respuesta

4

¿Has intentado buscar un .NET CSV parser ya existente? This one afirma que maneja registros de varias líneas significativamente más rápido que OLEDB.

+0

FastCSV es una biblioteca bastante aceptada. –

+0

Ya, eché un vistazo, y es por eso que mencioné tres opciones. El problema es que hay muchas más opciones, pero si cumplen o no mis criterios es un proceso muy lento. Espero que alguien ya sepa la opción correcta. –

1

Tome un vistazo al código que he publicado a esta pregunta:

https://stackoverflow.com/a/1544743/3043

Cubre más de sus necesidades, y no llevaría mucho para actualizarlo para apoyar delimitadores alternativos o calificadores de texto.

4

Escribí esto hace un tiempo como un analizador CSV liviano e independiente. Creo que cumple con todos sus requisitos. Pruébelo sabiendo que probablemente no sea a prueba de balas.

Si funciona para usted, siéntase libre de cambiar el espacio de nombres y utilizar 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]; 
      } 
     } 
    } 
} 
Cuestiones relacionadas