dividir una cadena separada por comas con cadenas entre comillas y sin comillas

Tengo la siguiente cadena separada por comas que necesito dividir. El problema es que parte del contenido está entre comillas y contiene comas que no deberían usarse en la división …

Cuerda:

111,222,"33,44,55",666,"77,88","99" 

Quiero la salida:

 111 222 33,44,55 666 77,88 99 

He intentado esto:

 (?:,?)((?<=")[^"]+(?=")|[^",]+) 

Pero lee la coma entre “77,88”, “99” como un golpe y obtengo el siguiente resultado:

 111 222 33,44,55 666 77,88 , 99 

Alguien puede ayudarme? Me estoy quedando sin horas … 🙂 / Peter

Dependiendo de sus necesidades, es posible que no pueda usar un analizador csv, ¡y de hecho puede querer reinventar la rueda!

Puedes hacerlo con algunas expresiones regulares simples

 (?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*) 

Esto hará lo siguiente:

(?:^|,) = Coincide con la expresión “Principio de línea o cadena”

(\"(?:[^\"]+|\"\")*\"|[^,]*) = Un grupo de captura numerado, esto seleccionará entre 2 alternativas:

  1. cosas entre comillas
  2. cosas entre comas

Esto debería darle el resultado que está buscando.

Código de ejemplo en C #

 public static string[] SplitCSV(string input) { Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)", RegexOptions.Compiled); List list = new List(); string curr = null; foreach (Match match in csvSplit.Matches(input)) { curr = match.Value; if (0 == curr.Length) { list.Add(""); } list.Add(curr.TrimStart(',')); } return list.ToArray(); } private void button1_Click(object sender, RoutedEventArgs e) { Console.WriteLine(SplitCSV("111,222,\"33,44,55\",666,\"77,88\",\"99\"")); } 

Advertencia Según el comentario de @ MrE: si aparece un nuevo personaje de línea deshonesto en un archivo csv mal formado y termina con una cadena irregular obtendrá un retroceso catastrófico ( https://www.regular-expressions.info/ catastrophic.html ) en su expresión regular y su sistema probablemente fallará (como lo hizo nuestro sistema de producción). Se puede replicar fácilmente en Visual Studio y, como he descubierto, se bloqueará. Un simple try / catch tampoco atrapará este problema.

Deberías usar:

 (?:^|,)(\"(?:[^\"])*\"|[^,]*) 

en lugar

Realmente me gusta la respuesta de jimplode, pero creo que una versión con retorno de rendimiento es un poco más útil, así que aquí está:

 public IEnumerable SplitCSV(string input) { Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)", RegexOptions.Compiled); foreach (Match match in csvSplit.Matches(input)) { yield return match.Value.TrimStart(','); } } 

Tal vez sea aún más útil tenerlo como un método de extensión:

 public static class StringHelper { public static IEnumerable SplitCSV(this string input) { Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)", RegexOptions.Compiled); foreach (Match match in csvSplit.Matches(input)) { yield return match.Value.TrimStart(','); } } } 

Esta expresión regular funciona sin la necesidad de recorrer valores y TrimStart(',') , como en la respuesta aceptada:

 ((?< =\")[^\"]*(?=\"(,|$)+)|(?<=,|^)[^,\"]*(?=,|$)) 

Aquí está la implementación en C #:

 string values = "111,222,\"33,44,55\",666,\"77,88\",\"99\""; MatchCollection matches = new Regex("((?< =\")[^\"]*(?=\"(,|$)+)|(?<=,|^)[^,\"]*(?=,|$))").Matches(values); foreach (var match in matches) { Console.WriteLine(match); } 

Salidas

 111 222 33,44,55 666 77,88 99 

Prueba esto:

  string s = @"111,222,""33,44,55"",666,""77,88"",""99"""; List result = new List(); var splitted = s.Split('"').ToList(); splitted.RemoveAll(x => x == ","); foreach (var it in splitted) { if (it.StartsWith(",") || it.EndsWith(",")) { var tmp = it.TrimEnd(',').TrimStart(','); result.AddRange(tmp.Split(',')); } else { if(!string.IsNullOrEmpty(it)) result.Add(it); } } //Results: foreach (var it in result) { Console.WriteLine(it); } 

No reinvente un analizador CSV, pruebe FileHelpers .

Para la respuesta de Jay, si usa un segundo booleano, entonces puede tener comillas dobles anidadas dentro de comillas simples y viceversa.

  private string[] splitString(string stringToSplit) { char[] characters = stringToSplit.ToCharArray(); List returnValueList = new List(); string tempString = ""; bool blockUntilEndQuote = false; bool blockUntilEndQuote2 = false; int characterCount = 0; foreach (char character in characters) { characterCount = characterCount + 1; if (character == '"' && !blockUntilEndQuote2) { if (blockUntilEndQuote == false) { blockUntilEndQuote = true; } else if (blockUntilEndQuote == true) { blockUntilEndQuote = false; } } if (character == '\'' && !blockUntilEndQuote) { if (blockUntilEndQuote2 == false) { blockUntilEndQuote2 = true; } else if (blockUntilEndQuote2 == true) { blockUntilEndQuote2 = false; } } if (character != ',') { tempString = tempString + character; } else if (character == ',' && (blockUntilEndQuote == true || blockUntilEndQuote2 == true)) { tempString = tempString + character; } else { returnValueList.Add(tempString); tempString = ""; } if (characterCount == characters.Length) { returnValueList.Add(tempString); tempString = ""; } } string[] returnValue = returnValueList.ToArray(); return returnValue; } 

Ninguna de estas respuestas funciona cuando la cadena tiene una coma dentro de comillas, como en "value, 1" , o comillas dobles escapadas, como en "value ""1""" , que son CSV válidos que deben analizarse como value, 1 y value "1" , respectivamente.

Esto también funcionará con el formato delimitado por tabuladores si pasa una pestaña en lugar de una coma como su delimitador.

 public static IEnumerable SplitRow(string row, char delimiter = ',') { var currentString = new StringBuilder(); var inQuotes = false; var quoteIsEscaped = false; //Store when a quote has been escaped. row = string.Format("{0}{1}", row, delimiter); //We add new cells at the delimiter, so append one for the parser. foreach (var character in row.Select((val, index) => new {val, index})) { if (character.val == delimiter) //We hit a delimiter character... { if (!inQuotes) //Are we inside quotes? If not, we've hit the end of a cell value. { Console.WriteLine(currentString); yield return currentString.ToString(); currentString.Clear(); } else { currentString.Append(character.val); } } else { if (character.val != ' ') { if(character.val == '"') //If we've hit a quote character... { if(character.val == '\"' && inQuotes) //Does it appear to be a closing quote? { if (row[character.index + 1] == character.val) //If the character afterwards is also a quote, this is to escape that (not a closing quote). { quoteIsEscaped = true; //Flag that we are escaped for the next character. Don't add the escaping quote. } else if (quoteIsEscaped) { quoteIsEscaped = false; //This is an escaped quote. Add it and revert quoteIsEscaped to false. currentString.Append(character.val); } else { inQuotes = false; } } else { if (!inQuotes) { inQuotes = true; } else { currentString.Append(character.val); //...It's a quote inside a quote. } } } else { currentString.Append(character.val); } } else { if (!string.IsNullOrWhiteSpace(currentString.ToString())) //Append only if not new cell { currentString.Append(character.val); } } } } } 

Sé que llegué un poco tarde a esto, pero para las búsquedas, así es como hice lo que me preguntas en C sharp

 private string[] splitString(string stringToSplit) { char[] characters = stringToSplit.ToCharArray(); List returnValueList = new List(); string tempString = ""; bool blockUntilEndQuote = false; int characterCount = 0; foreach (char character in characters) { characterCount = characterCount + 1; if (character == '"') { if (blockUntilEndQuote == false) { blockUntilEndQuote = true; } else if (blockUntilEndQuote == true) { blockUntilEndQuote = false; } } if (character != ',') { tempString = tempString + character; } else if (character == ',' && blockUntilEndQuote == true) { tempString = tempString + character; } else { returnValueList.Add(tempString); tempString = ""; } if (characterCount == characters.Length) { returnValueList.Add(tempString); tempString = ""; } } string[] returnValue = returnValueList.ToArray(); return returnValue; } 

Con actualizaciones menores a la función proporcionada por “Chad Hedgcock”.

Las actualizaciones están en:

Línea 26: character.val == ‘\ “‘ – Esto nunca puede ser cierto debido a la verificación realizada en la línea 24. es decir, character.val == ‘”‘

Línea 28: if (fila [character.index + 1] == character.val) added! QuoteIsEscaped para escapar de 3 citas consecutivas.

 public static IEnumerable SplitRow(string row, char delimiter = ',') { var currentString = new StringBuilder(); var inQuotes = false; var quoteIsEscaped = false; //Store when a quote has been escaped. row = string.Format("{0}{1}", row, delimiter); //We add new cells at the delimiter, so append one for the parser. foreach (var character in row.Select((val, index) => new {val, index})) { if (character.val == delimiter) //We hit a delimiter character... { if (!inQuotes) //Are we inside quotes? If not, we've hit the end of a cell value. { //Console.WriteLine(currentString); yield return currentString.ToString(); currentString.Clear(); } else { currentString.Append(character.val); } } else { if (character.val != ' ') { if(character.val == '"') //If we've hit a quote character... { if(character.val == '"' && inQuotes) //Does it appear to be a closing quote? { if (row[character.index + 1] == character.val && !quoteIsEscaped) //If the character afterwards is also a quote, this is to escape that (not a closing quote). { quoteIsEscaped = true; //Flag that we are escaped for the next character. Don't add the escaping quote. } else if (quoteIsEscaped) { quoteIsEscaped = false; //This is an escaped quote. Add it and revert quoteIsEscaped to false. currentString.Append(character.val); } else { inQuotes = false; } } else { if (!inQuotes) { inQuotes = true; } else { currentString.Append(character.val); //...It's a quote inside a quote. } } } else { currentString.Append(character.val); } } else { if (!string.IsNullOrWhiteSpace(currentString.ToString())) //Append only if not new cell { currentString.Append(character.val); } } } } 

}

Rapido y facil:

  public static string[] SplitCsv(string line) { List result = new List(); StringBuilder currentStr = new StringBuilder(""); bool inQuotes = false; for (int i = 0; i < line.Length; i++) // For each character { if (line[i] == '\"') // Quotes are closing or opening inQuotes = !inQuotes; else if (line[i] == ',') // Comma { if (!inQuotes) // If not in quotes, end of current string, add it to result { result.Add(currentStr.ToString()); currentStr.Clear(); } else currentStr.Append(line[i]); // If in quotes, just add it } else // Add any other character to current string currentStr.Append(line[i]); } result.Add(currentStr.ToString()); return result.ToArray(); // Return array of all strings } 

Con esta cadena como entrada:

  111,222,"33,44,55",666,"77,88","99" 

Volverá:

 111 222 33,44,55 666 77,88 99 

Una vez tuve que hacer algo similar y al final me atasqué con Regular Expressions. La incapacidad de Regex para tener estado hace que sea bastante complicado. Acabo de escribir un pequeño analizador simple .

Si está realizando el análisis CSV, debe seguir utilizando un analizador CSV ; no reinvente la rueda.

Aquí está mi implementación más rápida basada en la manipulación del puntero sin formato de cadena:

 string[] FastSplit(string sText, char? cSeparator = null, char? cQuotes = null) { string[] oTokens; if (null == cSeparator) { cSeparator = DEFAULT_PARSEFIELDS_SEPARATOR; } if (null == cQuotes) { cQuotes = DEFAULT_PARSEFIELDS_QUOTE; } unsafe { fixed (char* lpText = sText) { #region Fast array estimatation char* lpCurrent = lpText; int nEstimatedSize = 0; while (0 != *lpCurrent) { if (cSeparator == *lpCurrent) { nEstimatedSize++; } lpCurrent++; } nEstimatedSize++; // Add EOL char(s) string[] oEstimatedTokens = new string[nEstimatedSize]; #endregion #region Parsing char[] oBuffer = new char[sText.Length]; int nIndex = 0; int nTokens = 0; lpCurrent = lpText; while (0 != *lpCurrent) { if (cQuotes == *lpCurrent) { // Quotes parsing lpCurrent++; // Skip quote nIndex = 0; // Reset buffer while ( (0 != *lpCurrent) && (cQuotes != *lpCurrent) ) { oBuffer[nIndex] = *lpCurrent; // Store char lpCurrent++; // Move source cursor nIndex++; // Move target cursor } } else if (cSeparator == *lpCurrent) { // Separator char parsing oEstimatedTokens[nTokens++] = new string(oBuffer, 0, nIndex); // Store token nIndex = 0; // Skip separator and Reset buffer } else { // Content parsing oBuffer[nIndex] = *lpCurrent; // Store char nIndex++; // Move target cursor } lpCurrent++; // Move source cursor } // Recover pending buffer if (nIndex > 0) { // Store token oEstimatedTokens[nTokens++] = new string(oBuffer, 0, nIndex); } // Build final tokens list if (nTokens == nEstimatedSize) { oTokens = oEstimatedTokens; } else { oTokens = new string[nTokens]; Array.Copy(oEstimatedTokens, 0, oTokens, 0, nTokens); } #endregion } } // Epilogue return oTokens; } 

Necesitaba algo un poco más robusto, así que tomé de aquí y creé esto … Esta solución es un poco menos elegante y un poco más prolija, pero en mis pruebas (con una muestra de 1,000,000 de filas), encontré que era 2 a 3 veces más rápido. Además, maneja citas incrustadas no escapadas. Utilicé el delimitador de cadenas y calificadores en lugar de caracteres debido a los requisitos de mi solución. Me resultó más difícil de lo que esperaba encontrar un buen analizador de CSV genérico, así que espero que este algoritmo de análisis pueda ayudar a alguien.

  public static string[] SplitRow(string record, string delimiter, string qualifier, bool trimData) { // In-Line for example, but I implemented as string extender in production code Func  IndexOfNextNonWhiteSpaceChar = delegate (string source, int startIndex) { if (startIndex >= 0) { if (source != null) { for (int i = startIndex; i < source.Length; i++) { if (!char.IsWhiteSpace(source[i])) { return i; } } } } return -1; }; var results = new List(); var result = new StringBuilder(); var inQualifier = false; var inField = false; // We add new columns at the delimiter, so append one for the parser. var row = $"{record}{delimiter}"; for (var idx = 0; idx < row.Length; idx++) { // A delimiter character... if (row[idx]== delimiter[0]) { // Are we inside qualifier? If not, we've hit the end of a column value. if (!inQualifier) { results.Add(trimData ? result.ToString().Trim() : result.ToString()); result.Clear(); inField = false; } else { result.Append(row[idx]); } } // NOT a delimiter character... else { // ...Not a space character if (row[idx] != ' ') { // A qualifier character... if (row[idx] == qualifier[0]) { // Qualifier is closing qualifier... if (inQualifier && row[IndexOfNextNonWhiteSpaceChar(row, idx + 1)] == delimiter[0]) { inQualifier = false; continue; } else { // ...Qualifier is opening qualifier if (!inQualifier) { inQualifier = true; } // ...It's a qualifier inside a qualifier. else { inField = true; result.Append(row[idx]); } } } // Not a qualifier character... else { result.Append(row[idx]); inField = true; } } // ...A space character else { if (inQualifier || inField) { result.Append(row[idx]); } } } } return results.ToArray(); } 

Algunos códigos de prueba:

  //var input = "111,222,\"33,44,55\",666,\"77,88\",\"99\""; var input = "111, 222, \"99\",\"33,44,55\" , \"666 \"mark of a man\"\", \" spaces \"77,88\" \""; Console.WriteLine("Split with trim"); Console.WriteLine("---------------"); var result = SplitRow(input, ",", "\"", true); foreach (var r in result) { Console.WriteLine(r); } Console.WriteLine(""); // Split 2 Console.WriteLine("Split with no trim"); Console.WriteLine("------------------"); var result2 = SplitRow(input, ",", "\"", false); foreach (var r in result2) { Console.WriteLine(r); } Console.WriteLine(""); // Time Trial 1 Console.WriteLine("Experimental Process (1,000,000) iterations"); Console.WriteLine("-------------------------------------------"); watch = Stopwatch.StartNew(); for (var i = 0; i < 1000000; i++) { var x1 = SplitRow(input, ",", "\"", false); } watch.Stop(); elapsedMs = watch.ElapsedMilliseconds; Console.WriteLine($"Total Process Time: {string.Format("{0:0.###}", elapsedMs / 1000.0)} Seconds"); Console.WriteLine(""); 

Resultados

 Split with trim --------------- 111 222 99 33,44,55 666 "mark of a man" spaces "77,88" Split with no trim ------------------ 111 222 99 33,44,55 666 "mark of a man" spaces "77,88" Original Process (1,000,000) iterations ------------------------------- Total Process Time: 7.538 Seconds Experimental Process (1,000,000) iterations -------------------------------------------- Total Process Time: 3.363 Seconds 

Actualmente uso la siguiente expresión regular:

  public static Regex regexCSVSplit = new Regex(@"(?x:( (? (^|[,;\t\r\n])\s* ( (? (?[""'])(?([^,;\t\r\n]|(?< !\k\s*)[,;\t\r\n])*)\k) | (? (? [^""',;\s\r\n]* )) ) (?=\s*([,;\t\r\n]|$)) ) | (? (^|[\s\t\r\n]) ( (? (?[""'])(? [^""',;\s\t\r\n]* )\k) | (? (? [^""',;\s\t\r\n]* )) ) (?=[,;\s\t\r\n]|$)) ))", RegexOptions.Compiled); 

Esta solución también puede manejar casos bastante caóticos como a continuación: enter image description here

Esta es la forma de alimentar el resultado en una matriz:

  var data = regexCSVSplit.Matches(line_to_process).Cast().Select(x => x.Groups["DAT"].Value).ToArray(); 

Vea este ejemplo en acción AQUÍ

Prueba esto

 private string[] GetCommaSeperatedWords(string sep, string line) { List list = new List(); StringBuilder word = new StringBuilder(); int doubleQuoteCount = 0; for (int i = 0; i < line.Length; i++) { string chr = line[i].ToString(); if (chr == "\"") { if (doubleQuoteCount == 0) doubleQuoteCount++; else doubleQuoteCount--; continue; } if (chr == sep && doubleQuoteCount == 0) { list.Add(word.ToString()); word = new StringBuilder(); continue; } word.Append(chr); } list.Add(word.ToString()); return list.ToArray(); } 

Esta es la respuesta de Chad reescrita con lógica basada en estado. Su respuesta falló para mí cuando se encontró con """BRAD""" como un campo. Eso debería devolver "BRAD" pero acabó con todos los campos restantes. Cuando intenté depurarlo, terminé reescribiéndolo como lógica basada en estado:

 enum SplitState { s_begin, s_infield, s_inquotefield, s_foundquoteinfield }; public static IEnumerable SplitRow(string row, char delimiter = ',') { var currentString = new StringBuilder(); SplitState state = SplitState.s_begin; row = string.Format("{0}{1}", row, delimiter); //We add new cells at the delimiter, so append one for the parser. foreach (var character in row.Select((val, index) => new { val, index })) { //Console.WriteLine("character = " + character.val + " state = " + state); switch (state) { case SplitState.s_begin: if (character.val == delimiter) { /* empty field */ yield return currentString.ToString(); currentString.Clear(); } else if (character.val == '"') { state = SplitState.s_inquotefield; } else { currentString.Append(character.val); state = SplitState.s_infield; } break; case SplitState.s_infield: if (character.val == delimiter) { /* field with data */ yield return currentString.ToString(); state = SplitState.s_begin; currentString.Clear(); } else { currentString.Append(character.val); } break; case SplitState.s_inquotefield: if (character.val == '"') { // could be end of field, or escaped quote. state = SplitState.s_foundquoteinfield; } else { currentString.Append(character.val); } break; case SplitState.s_foundquoteinfield: if (character.val == '"') { // found escaped quote. currentString.Append(character.val); state = SplitState.s_inquotefield; } else if (character.val == delimiter) { // must have been last quote so we must find delimiter yield return currentString.ToString(); state = SplitState.s_begin; currentString.Clear(); } else { throw new Exception("Quoted field not terminated."); } break; default: throw new Exception("unknown state:" + state); } } //Console.WriteLine("currentstring = " + currentString.ToString()); } 

Esta es una línea de código mucho más que las otras soluciones, pero es fácil de modificar para agregar casos extremos.