¿Cómo puedo analizar una cadena CSV con JavaScript, que contiene comas en los datos?
Tengo el siguiente tipo de cadena
var string = "'string, duppi, du', 23, lala"
Quiero dividir la cadena en una matriz en cada coma, pero solo las comas fuera de las comillas simples.
No puedo encontrar la expresión regular correcta para la división...
string.split(/,/)
me dará
["'string", " duppi", " du'", " 23", " lala"]
pero el resultado debería ser:
["string, duppi, du", "23", "lala"]
¿Existe una solución para varios navegadores?
Descargo de responsabilidad
Actualización del 1 de diciembre de 2014: La respuesta a continuación funciona solo para un formato muy específico de CSV. Como señaló correctamente DG en los comentarios, esta solución NO se ajusta a la definición RFC 4180 de CSV y tampoco se ajusta al formato MS Excel. Esta solución simplemente demuestra cómo se puede analizar una línea de entrada CSV (no estándar) que contiene una combinación de tipos de cadenas, donde las cadenas pueden contener comillas y comas escapadas.
Una solución CSV no estándar
Como señala correctamente Austincheney, realmente necesita analizar la cadena de principio a fin si desea manejar adecuadamente cadenas entre comillas que pueden contener caracteres de escape. Además, el OP no define claramente qué es realmente una "cadena CSV". Primero debemos definir qué constituye una cadena CSV válida y sus valores individuales.
Dado: Definición de "cadena CSV"
A los efectos de esta discusión, una "cadena CSV" consta de cero o más valores, donde varios valores están separados por una coma. Cada valor puede consistir en:
- Una cadena entre comillas dobles. (puede contener comillas simples sin escape).
- Una cadena entre comillas simples. (puede contener comillas dobles sin escape).
- Una cadena sin comillas. (NO puede contener comillas, comas ni barras invertidas).
- Un valor vacío. (Un valor de todos los espacios en blanco se considera vacío).
Reglas/Notas:
- Los valores citados pueden contener comas.
- Los valores citados pueden contener cualquier elemento escapado, por ejemplo
'that\'s cool'
. - Los valores que contengan comillas, comas o barras invertidas deben incluirse entre comillas.
- Los valores que contienen espacios en blanco al principio o al final deben citarse.
- La barra invertida se elimina de todos:
\'
en valores entre comillas simples. - La barra invertida se elimina de todos:
\"
en valores entre comillas dobles. - Las cadenas no entrecomilladas se eliminan de los espacios iniciales y finales.
- El separador de coma puede tener espacios en blanco adyacentes (que se ignoran).
Encontrar:
Una función de JavaScript que convierte una cadena CSV válida (como se define anteriormente) en una matriz de valores de cadena.
Solución:
Las expresiones regulares utilizadas por esta solución son complejas. Y (en mi humilde opinión) todas las expresiones regulares no triviales deben presentarse en modo de espacio libre con muchos comentarios y sangrías. Desafortunadamente, JavaScript no permite el modo de espacio libre. Por lo tanto, las expresiones regulares implementadas por esta solución se presentan primero en sintaxis de expresiones regulares nativas (expresadas utilizando la práctica r'''...'''
sintaxis de Python: cadena multilínea sin formato).
Primero, aquí hay una expresión regular que valida que una cadena CVS cumple con los requisitos anteriores:
Regex para validar una "cadena CSV":
re_valid = r"""
# Validate a CSV string having single, double or un-quoted values.
^ # Anchor to start of string.
\s* # Allow whitespace before value.
(?: # Group for value alternatives.
'[^'\\]*(?:\\[\S\s][^'\\]*)*' # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*" # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)* # or Non-comma, non-quote stuff.
) # End group of value alternatives.
\s* # Allow whitespace after value.
(?: # Zero or more additional values
, # Values separated by a comma.
\s* # Allow whitespace before value.
(?: # Group for value alternatives.
'[^'\\]*(?:\\[\S\s][^'\\]*)*' # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*" # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)* # or Non-comma, non-quote stuff.
) # End group of value alternatives.
\s* # Allow whitespace after value.
)* # Zero or more additional values
$ # Anchor to end of string.
"""
Si una cadena coincide con la expresión regular anterior, entonces esa cadena es una cadena CSV válida (de acuerdo con las reglas establecidas anteriormente) y se puede analizar utilizando la siguiente expresión regular. Luego se utiliza la siguiente expresión regular para hacer coincidir un valor de la cadena CSV. Se aplica repetidamente hasta que no se encuentren más coincidencias (y se hayan analizado todos los valores).
Regex para analizar un valor de una cadena CSV válida:
re_value = r"""
# Match one value in valid CSV string.
(?!\s*$) # Don't match empty last value.
\s* # Strip whitespace before value.
(?: # Group for value alternatives.
'([^'\\]*(?:\\[\S\s][^'\\]*)*)' # Either $1: Single quoted string,
| "([^"\\]*(?:\\[\S\s][^"\\]*)*)" # or $2: Double quoted string,
| ([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*) # or $3: Non-comma, non-quote stuff.
) # End group of value alternatives.
\s* # Strip whitespace after value.
(?:,|$) # Field ends on comma or EOS.
"""
Tenga en cuenta que hay un valor de caso especial con el que esta expresión regular no coincide: el último valor cuando ese valor está vacío. Este caso especial de "último valor vacío" se prueba y maneja mediante la función js que aparece a continuación.
Ejemplo de entrada y salida:
En los siguientes ejemplos, se utilizan llaves para delimitar el archivo {result strings}
. (Esto es para ayudar a visualizar espacios iniciales/finales y cadenas de longitud cero).
// Return array of string values, or NULL if CSV string not well formed.
function CSVtoArray(text) {
var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/;
var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
// Return NULL if input string is not well formed CSV string.
if (!re_valid.test(text)) return null;
var a = []; // Initialize array to receive values.
text.replace(re_value, // "Walk" the string using replace with callback.
function(m0, m1, m2, m3) {
// Remove backslash from \' in single quoted values.
if (m1 !== undefined) a.push(m1.replace(/\\'/g, "'"));
// Remove backslash from \" in double quoted values.
else if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"'));
else if (m3 !== undefined) a.push(m3);
return ''; // Return empty string.
});
// Handle special case of empty last value.
if (/,\s*$/.test(text)) a.push('');
return a;
};
console.log('Test 1: Test string from original question.');
console.log(CSVtoArray("'string, duppi, du', 23, lala"));
console.log('Test 2: Empty CSV string.');
console.log(CSVtoArray(""));
console.log('Test 3: CSV string with two empty values.');
console.log(CSVtoArray(","));
console.log('Test 4: Double quoted CSV string having single quoted values.');
console.log(CSVtoArray("'one','two with escaped \' single quote', 'three, with, commas'"));
console.log('Test 5: Single quoted CSV string having double quoted values.');
console.log(CSVtoArray('"one","two with escaped \" double quote", "three, with, commas"'));
console.log('Test 6: CSV string with whitespace in and around empty and non-empty values.');
console.log(CSVtoArray(" one , 'two' , , ' four' ,, 'six ', ' seven ' , "));
console.log('Test 7: Not valid');
console.log(CSVtoArray("one, that's me!, escaped \, comma"));
Notas adicionales:
Esta solución requiere que la cadena CSV sea "válida". Por ejemplo, los valores sin comillas no pueden contener barras invertidas ni comillas; por ejemplo, la siguiente cadena CSV NO es válida:
var invalid1 = "one, that's me!, escaped \, comma"
Esto no es realmente una limitación porque cualquier subcadena puede representarse como un valor entre comillas simples o dobles. Tenga en cuenta también que esta solución representa sólo una definición posible para: "Valores separados por comas".
Editar: 2014-05-19: Descargo de responsabilidad agregado. Editar: 01/12/2014: Descargo de responsabilidad movido a la parte superior.
solución RFC 4180
Esto no resuelve la cadena en la pregunta ya que su formato no cumple con RFC 4180; la codificación aceptable es escapar de comillas dobles con comillas dobles. La siguiente solución funciona correctamente con archivos CSV d/l de hojas de cálculo de Google.
ACTUALIZACIÓN (3/2017)
Analizar una sola línea sería incorrecto. Según RFC 4180, los campos pueden contener CRLF, lo que hará que cualquier lector de línea rompa el archivo CSV. Aquí hay una versión actualizada que analiza la cadena CSV:
'use strict';
function csvToArray(text) {
let p = '', row = [''], ret = [row], i = 0, r = 0, s = !0, l;
for (l of text) {
if ('"' === l) {
if (s && l === p) row[i] += l;
s = !s;
} else if (',' === l && s) l = row[++i] = '';
else if ('\n' === l && s) {
if ('\r' === p) row[i] = row[i].slice(0, -1);
row = ret[++r] = [l = '']; i = 0;
} else row[i] += l;
p = l;
}
return ret;
};
let test = '"one","two with escaped """" double quotes""","three, with, commas",four with no quotes,"five with CRLF\r\n"\r\n"2nd line one","two with escaped """" double quotes""","three, with, commas",four with no quotes,"five with CRLF\r\n"';
console.log(csvToArray(test));
ANTIGUA RESPUESTA
(Solución de una sola línea)
function CSVtoArray(text) {
let ret = [''], i = 0, p = '', s = true;
for (let l in text) {
l = text[l];
if ('"' === l) {
s = !s;
if ('"' === p) {
ret[i] += '"';
l = '-';
} else if ('' === p)
l = '-';
} else if (s && ',' === l)
l = ret[++i] = '';
else
ret[i] += l;
p = l;
}
return ret;
}
let test = '"one","two with escaped """" double quotes""","three, with, commas",four with no quotes,five for fun';
console.log(CSVtoArray(test));
Y para divertirse, así es como se crea CSV a partir de la matriz:
function arrayToCSV(row) {
for (let i in row) {
row[i] = row[i].replace(/"/g, '""');
}
return '"' + row.join('","') + '"';
}
let row = [
"one",
"two with escaped \" double quote",
"three, with, commas",
"four with no quotes (now has)",
"five for fun"
];
let text = arrayToCSV(row);
console.log(text);
Me gustó la respuesta de FakeRainBrigand, sin embargo, contiene algunos problemas: no puede manejar espacios en blanco entre una comilla y una coma, y no admite 2 comas consecutivas. Intenté editar su respuesta, pero los revisores rechazaron mi edición y aparentemente no entendieron mi código. Aquí está mi versión del código de FakeRainBrigand. También hay un violín: http://jsfiddle.net/xTezm/46/
String.prototype.splitCSV = function() {
var matches = this.match(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g);
for (var n = 0; n < matches.length; ++n) {
matches[n] = matches[n].trim();
if (matches[n] == ',') matches[n] = '';
}
if (this[0] == ',') matches.unshift("");
return matches;
}
var string = ',"string, duppi, du" , 23 ,,, "string, duppi, du",dup,"", , lala';
var parsed = string.splitCSV();
alert(parsed.join('|'));