Cómo dividir csv cuyas columnas pueden contener,

105

Dado

2,1016,7 / 31/2008 14: 22, Geoff Dalgas , 6/5/2011 22:21, http://stackoverflow.com , "Corvallis, OR", 7679,351,81, b437f461b3fd27387c5d8ab47a293d35,34

Cómo usar C # para dividir la información anterior en cadenas de la siguiente manera:

2
1016
7/31/2008 14:22
Geoff Dalgas
6/5/2011 22:21
http://stackoverflow.com
Corvallis, OR
7679
351
81
b437f461b3fd27387c5d8ab47a293d35
34

Como puede ver, una de las columnas contiene <= (Corvallis, OR)

// actualización // Basado en C # Regex Split - comas fuera de las comillas

string[] result = Regex.Split(samplestring, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
q0987
fuente
1
Aunque en Java, Pregunta similar: stackoverflow.com/questions/1757065/…
sgokhales
1
Usar una expresión regular para hacer esto es un mal consejo. .NET Framework ya tiene soporte integrado para analizar CSV. Vea esta respuesta que es la que debe aceptar. De lo contrario, cerraré esto como un engaño de stackoverflow.com/questions/3147836/… que es igualmente incorrecto.
Kev
¿Puede explicar en detalle cuál es el soporte integrado de .NET para analizar archivos CSV con comas incrustadas? ¿Se refiere a la clase Microsoft.VisualBasic.FileIO.TextFieldParser?
AllSolutions

Respuestas:

182

Usa la Microsoft.VisualBasic.FileIO.TextFieldParserclase. Esto manejará el análisis de un archivo delimitado, TextReadero Streamdonde algunos campos están entre comillas y otros no.

Por ejemplo:

using Microsoft.VisualBasic.FileIO;

string csv = "2,1016,7/31/2008 14:22,Geoff Dalgas,6/5/2011 22:21,http://stackoverflow.com,\"Corvallis, OR\",7679,351,81,b437f461b3fd27387c5d8ab47a293d35,34";

TextFieldParser parser = new TextFieldParser(new StringReader(csv));

// You can also read from a file
// TextFieldParser parser = new TextFieldParser("mycsvfile.csv");

parser.HasFieldsEnclosedInQuotes = true;
parser.SetDelimiters(",");

string[] fields;

while (!parser.EndOfData)
{
    fields = parser.ReadFields();
    foreach (string field in fields)
    {
        Console.WriteLine(field);
    }
} 

parser.Close();

Esto debería dar como resultado el siguiente resultado:

2
1016
31/7/2008 14:22
Geoff Dalgas
5/6/2011 22:21
http://stackoverflow.com
Corvallis, Oregón
7679
351
81
b437f461b3fd27387c5d8ab47a293d35
34

Consulte Microsoft.VisualBasic.FileIO.TextFieldParser para obtener más información.

Debe agregar una referencia a Microsoft.VisualBasicen la pestaña Agregar referencias .NET.

Tim
fuente
9
Amigo, muchas gracias por esta solución, tengo alrededor de 500K + filas de datos CSV que necesito cargar en una tabla y se cargan con comas dentro de las comillas. Te debo una bebida para adultos de tu elección si nuestros caminos se cruzan.
Mark Kram
@tim usé esto, y nota que se salta todos los números de línea pares, solo procesa los números de línea impares en un archivo que tiene 1050 líneas. ¿algunas ideas?
Smith
@Smith: sin ver su código o entrada de muestra, no tengo idea. Sugiero publicar una nueva pregunta. ¿Quizás al archivo le falta un retorno de carro u otro marcador de fin de línea en las líneas pares?
Tim
Ni siquiera sabía acerca de esta biblioteca hasta que vi esto, ¡gracias! Si alguien más quiere un ejemplo que analiza un archivo CSV completo, vea esta respuesta SO: stackoverflow.com/a/3508572/3105807
Amy Barrett
2
¿Podemos linchar a Microsoft por no proporcionar un constructor que tome una cadena, por lo que primero tenemos que pasar por el aro de convertirlo en una secuencia? De lo contrario, buena respuesta.
Loren Pechtel
43

Es demasiado tarde, pero esto puede ser útil para alguien. Podemos usar RegEx como abajo.

Regex CSVParser = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");
String[] Fields = CSVParser.Split(Test);
Husen
fuente
4
Esto es perfecto. Preferiría usar esto que importar una biblioteca completamente diferente. Bravo.
TheGeekYouNeed
1
Coincide con asdf, "", "como ,\" df ",
Esta solución no funciona correctamente; no tiene en cuenta las marcas de habla, lo que significa que habrá muchas marcas de habla en ubicaciones incorrectas durante la lectura.
AidanH
¿Qué pasa si falta la cita final en alguna línea: asd, "", "as, \" df "," asd asd "," as
MarmiK
1
Esto funcionó para mí y tuvo en cuenta las marcas de habla entre comillas. 30 millones de filas de ellos. Muy bueno y con una mínima cantidad de código.
GBGOLC
4

Veo que si pegas texto delimitado por csv en Excel y haces un "Texto a columnas", te pide un "calificador de texto". Tiene un valor predeterminado de comillas dobles para tratar el texto entre comillas dobles como literal. Me imagino que Excel implementa esto yendo un carácter a la vez, si encuentra un "calificador de texto", sigue yendo al siguiente "calificador". Probablemente pueda implementar esto usted mismo con un bucle for y un booleano para indicar si está dentro de un texto literal.

public string[] CsvParser(string csvText)
{
    List<string> tokens = new List<string>();

    int last = -1;
    int current = 0;
    bool inText = false;

    while(current < csvText.Length)
    {
        switch(csvText[current])
        {
            case '"':
                inText = !inText; break;
            case ',':
                if (!inText) 
                {
                    tokens.Add(csvText.Substring(last + 1, (current - last)).Trim(' ', ',')); 
                    last = current;
                }
                break;
            default:
                break;
        }
        current++;
    }

    if (last != csvText.Length - 1) 
    {
        tokens.Add(csvText.Substring(last+1).Trim());
    }

    return tokens.ToArray();
}
Roly
fuente
3

Utilice una biblioteca como LumenWorks para realizar su lectura CSV. Manejará campos con comillas en ellos y, en general, será más sólido que su solución personalizada en virtud de haber existido durante mucho tiempo.

Adam Lear
fuente
2

Es un asunto complicado analizar archivos .csv cuando el archivo .csv puede ser cadenas separadas por comas, cadenas entre comillas separadas por comas o una combinación caótica de los dos. La solución que se me ocurrió permite cualquiera de las tres posibilidades.

Creé un método, ParseCsvRow () que devuelve una matriz de una cadena csv. Primero trato con las comillas dobles en la cadena dividiendo la cadena entre comillas dobles en una matriz llamada quotesArray. Los archivos .csv de cadenas entre comillas solo son válidos si hay un número par de comillas dobles. Las comillas dobles en un valor de columna deben reemplazarse con un par de comillas dobles (este es el enfoque de Excel). Siempre que el archivo .csv cumpla con estos requisitos, puede esperar que las comas delimitadores aparezcan solo fuera de los pares de comillas dobles. Las comas dentro de los pares de comillas dobles son parte del valor de la columna y deben ignorarse al dividir el .csv en una matriz.

Mi método probará las comas fuera de los pares de comillas dobles mirando solo los índices pares de quotesArray. También elimina las comillas dobles del inicio y el final de los valores de columna.

    public static string[] ParseCsvRow(string csvrow)
    {
        const string obscureCharacter = "ᖳ";
        if (csvrow.Contains(obscureCharacter)) throw new Exception("Error: csv row may not contain the " + obscureCharacter + " character");

        var unicodeSeparatedString = "";

        var quotesArray = csvrow.Split('"');  // Split string on double quote character
        if (quotesArray.Length > 1)
        {
            for (var i = 0; i < quotesArray.Length; i++)
            {
                // CSV must use double quotes to represent a quote inside a quoted cell
                // Quotes must be paired up
                // Test if a comma lays outside a pair of quotes.  If so, replace the comma with an obscure unicode character
                if (Math.Round(Math.Round((decimal) i/2)*2) == i)
                {
                    var s = quotesArray[i].Trim();
                    switch (s)
                    {
                        case ",":
                            quotesArray[i] = obscureCharacter;  // Change quoted comma seperated string to quoted "obscure character" seperated string
                            break;
                    }
                }
                // Build string and Replace quotes where quotes were expected.
                unicodeSeparatedString += (i > 0 ? "\"" : "") + quotesArray[i].Trim();
            }
        }
        else
        {
            // String does not have any pairs of double quotes.  It should be safe to just replace the commas with the obscure character
            unicodeSeparatedString = csvrow.Replace(",", obscureCharacter);
        }

        var csvRowArray = unicodeSeparatedString.Split(obscureCharacter[0]); 

        for (var i = 0; i < csvRowArray.Length; i++)
        {
            var s = csvRowArray[i].Trim();
            if (s.StartsWith("\"") && s.EndsWith("\""))
            {
                csvRowArray[i] = s.Length > 2 ? s.Substring(1, s.Length - 2) : "";  // Remove start and end quotes.
            }
        }

        return csvRowArray;
    }

Una desventaja de mi enfoque es la forma en que reemplazo temporalmente las comas delimitadoras con un carácter unicode oscuro. Este carácter debe ser tan oscuro que nunca aparecerá en su archivo .csv. Es posible que desee poner más manejo en torno a esto.

Jason Williams
fuente
1

Tuve un problema con un CSV que contiene campos con un carácter de comillas, así que al usar TextFieldParser, se me ocurrió lo siguiente:

private static string[] parseCSVLine(string csvLine)
{
  using (TextFieldParser TFP = new TextFieldParser(new MemoryStream(Encoding.UTF8.GetBytes(csvLine))))
  {
    TFP.HasFieldsEnclosedInQuotes = true;
    TFP.SetDelimiters(",");

    try 
    {           
      return TFP.ReadFields();
    }
    catch (MalformedLineException)
    {
      StringBuilder m_sbLine = new StringBuilder();

      for (int i = 0; i < TFP.ErrorLine.Length; i++)
      {
        if (i > 0 && TFP.ErrorLine[i]== '"' &&(TFP.ErrorLine[i + 1] != ',' && TFP.ErrorLine[i - 1] != ','))
          m_sbLine.Append("\"\"");
        else
          m_sbLine.Append(TFP.ErrorLine[i]);
      }

      return parseCSVLine(m_sbLine.ToString());
    }
  }
}

Un StreamReader todavía se usa para leer el CSV línea por línea, de la siguiente manera:

using(StreamReader SR = new StreamReader(FileName))
{
  while (SR.Peek() >-1)
    myStringArray = parseCSVLine(SR.ReadLine());
}
RooiWillie
fuente
1

Con Cinchoo ETL , una biblioteca de código abierto, puede manejar automáticamente los valores de las columnas que contienen separadores.

string csv = @"2,1016,7/31/2008 14:22,Geoff Dalgas,6/5/2011 22:21,http://stackoverflow.com,""Corvallis, OR"",7679,351,81,b437f461b3fd27387c5d8ab47a293d35,34";

using (var p = ChoCSVReader.LoadText(csv)
    )
{
    Console.WriteLine(p.Dump());
}

Salida:

Key: Column1 [Type: String]
Value: 2
Key: Column2 [Type: String]
Value: 1016
Key: Column3 [Type: String]
Value: 7/31/2008 14:22
Key: Column4 [Type: String]
Value: Geoff Dalgas
Key: Column5 [Type: String]
Value: 6/5/2011 22:21
Key: Column6 [Type: String]
Value: http://stackoverflow.com
Key: Column7 [Type: String]
Value: Corvallis, OR
Key: Column8 [Type: String]
Value: 7679
Key: Column9 [Type: String]
Value: 351
Key: Column10 [Type: String]
Value: 81
Key: Column11 [Type: String]
Value: b437f461b3fd27387c5d8ab47a293d35
Key: Column12 [Type: String]
Value: 34

Para obtener más información, visite el artículo de codeproject.

Espero eso ayude.

RajN
fuente