Como posso analisar uma string CSV com Javascript, que contém vírgula nos dados?

Eu tenho o seguinte tipo de string

var string = "'string, duppi, du', 23, lala" 

Eu quero dividir a string em uma matriz em cada vírgula, mas apenas as vírgulas fora as aspas simples.

Eu não consigo descobrir o regex certo para a divisão …

 string.split(/,/) 

vai me dar

 ["'string", " duppi", " du'", " 23", " lala"] 

mas o resultado deve ser:

 ["string, duppi, du", "23", "lala"] 

Existe alguma solução cross-browser?

aviso Legal

Atualização de 2014-12-01: A resposta abaixo funciona apenas para um formato muito específico de CSV. Como corretamente apontado pela DG nos comentários, esta solução NÃO se encheckbox na definição RFC 4180 de CSV e também NÃO se encheckbox no formato MS Excel. Essa solução demonstra simplesmente como é possível analisar uma linha de input CSV (não padrão) que contém uma mistura de tipos de sequência, em que as cadeias podem conter aspas e vírgulas com escape.

Uma solução CSV não padrão

Como austincheney corretamente aponta, você realmente precisa analisar a cadeia do início ao fim se você deseja manipular corretamente as strings citadas que podem conter caracteres de escape. Além disso, o OP não define claramente o que realmente é uma “string CSV”. Primeiro, devemos definir o que constitui uma string CSV válida e seus valores individuais.

Dado: Definição “CSV String”

Para o propósito desta discussão, uma “string CSV” consiste em zero ou mais valores, onde vários valores são separados por uma vírgula. Cada valor pode consistir em:

  1. Uma cadeia com aspas duplas. (pode conter aspas simples sem escape.)
  2. Uma única string entre aspas. (pode conter aspas duplas sem escape.)
  3. Uma string não citada. (não pode conter aspas, vírgulas ou barras invertidas.)
  4. Um valor vazio. (Um valor de todo o espaço em branco é considerado vazio.)

Regras / Notas:

  • Valores indicados podem conter vírgulas.
  • Valores citados podem conter qualquer coisa com escape, por exemplo, 'that\'s cool' .
  • Valores contendo aspas, vírgulas ou barras invertidas devem ser citados.
  • Valores contendo espaços em branco à esquerda ou à direita devem ser citados.
  • A barra invertida é removida de todos: \' in single quoted values.
  • A barra invertida é removida de todos: \" em valores entre aspas duplas.
  • As strings não citadas são aparadas de qualquer espaço inicial e final.
  • O separador de vírgula pode ter um espaço em branco adjacente (que é ignorado).

Encontrar:

Uma function JavaScript que converte uma string CSV válida (conforme definido acima) em uma matriz de valores de string.

Solução:

As expressões regulares usadas por essa solução são complexas. E (IMHO) todas as expressões regulares não-triviais devem ser apresentadas no modo de espaçamento livre com muitos comentários e recuos. Infelizmente, o JavaScript não permite o modo de espaçamento livre. Assim, as expressões regulares implementadas por esta solução são apresentadas pela primeira vez na syntax de regex nativo (expressa usando a syntax handy: r'''...''' raw-multi-line-string) do Python.

Primeiro, aqui está uma expressão regular que valida que uma string do CVS atende aos requisitos acima:

Regex para validar uma “string 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. """ 

Se uma cadeia de caracteres corresponder à regex acima, essa cadeia será uma cadeia de caracteres CSV válida (de acordo com as regras estabelecidas anteriormente) e poderá ser analisada usando a seguinte regex. O regex a seguir é usado para corresponder a um valor da string CSV. Ele é aplicado repetidamente até que nenhuma outra correspondência seja encontrada (e todos os valores foram analisados).

Regex para analisar um valor da cadeia de caracteres 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. """ 

Observe que há um valor de caso especial que esta regex não corresponde – o último valor quando esse valor está vazio. Este caso especial “último valor vazio” é testado e tratado pela function js que se segue.

Função JavaScript para analisar a string CSV:

 // 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; }; 

Exemplo de input e saída:

Nos exemplos a seguir, as chaves são usadas para delimitar as {result strings} . (Isso é para ajudar a visualizar espaços iniciais / finais e sequências de comprimento zero.)

 // Test 1: Test string from original question. var test = "'string, duppi, du', 23, lala"; var a = CSVtoArray(test); /* Array hes 3 elements: a[0] = {string, duppi, du} a[1] = {23} a[2] = {lala} */ 
 // Test 2: Empty CSV string. var test = ""; var a = CSVtoArray(test); /* Array hes 0 elements: */ 
 // Test 3: CSV string with two empty values. var test = ","; var a = CSVtoArray(test); /* Array hes 2 elements: a[0] = {} a[1] = {} */ 
 // Test 4: Double quoted CSV string having single quoted values. var test = "'one','two with escaped \' single quote', 'three, with, commas'"; var a = CSVtoArray(test); /* Array hes 3 elements: a[0] = {one} a[1] = {two with escaped ' single quote} a[2] = {three, with, commas} */ 
 // Test 5: Single quoted CSV string having double quoted values. var test = '"one","two with escaped \" double quote", "three, with, commas"'; var a = CSVtoArray(test); /* Array hes 3 elements: a[0] = {one} a[1] = {two with escaped " double quote} a[2] = {three, with, commas} */ 
 // Test 6: CSV string with whitespace in and around empty and non-empty values. var test = " one , 'two' , , ' four' ,, 'six ', ' seven ' , "; var a = CSVtoArray(test); /* Array hes 8 elements: a[0] = {one} a[1] = {two} a[2] = {} a[3] = { four} a[4] = {} a[5] = {six } a[6] = { seven } a[7] = {} */ 

Notas Adicionais:

Esta solução requer que a string CSV seja “válida”. Por exemplo, valores não citados não podem conter barras invertidas ou citações, por exemplo, a seguinte string CSV NÃO é válida:

 var invalid1 = "one, that's me!, escaped \, comma" 

Isso não é realmente uma limitação porque qualquer subcadeia pode ser representada como um valor entre aspas simples ou duplo. Note também que esta solução representa apenas uma definição possível para: “Valores separados por vírgula”.

Edit: 2014-05-19: Adicionado disclaimer. Edit: 2014-12-01: disclaimer movido para cima.

Solução RFC 4180

Isso não resolve a cadeia na questão, pois seu formato não está em conformidade com o RFC 4180; a codificação aceitável está escapando de aspas duplas com aspas duplas. A solução abaixo funciona corretamente com arquivos CSV d / l das planilhas do Google.

ATUALIZAÇÃO (3/2017)

Parsing linha única seria errado. De acordo com a RFC 4180, os campos podem conter CRLF, o que fará com que qualquer leitor de linha quebre o arquivo CSV. Aqui está uma versão atualizada que analisa a string 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)); 

Gramática PEG (.js) que manipula os exemplos da RFC 4180 em http://en.wikipedia.org/wiki/Comma-separated_values :

 start = [\n\r]* first:line rest:([\n\r]+ data:line { return data; })* [\n\r]* { rest.unshift(first); return rest; } line = first:field rest:("," text:field { return text; })* & { return !!first || rest.length; } // ignore blank lines { rest.unshift(first); return rest; } field = '"' text:char* '"' { return text.join(''); } / text:[^\n\r,]* { return text.join(''); } char = '"' '"' { return '"'; } / [^"] 

Teste em http://jsfiddle.net/knvzk/10 ou https://pegjs.org/online .

Faça o download do analisador gerado em https://gist.github.com/3362830 .

Se você pode ter o seu delimitador de cotação entre aspas, então esta é uma duplicata do código JavaScript para analisar dados CSV .

Você pode traduzir todas as aspas simples para aspas duplas primeiro:

 string = string.replace( /'/g, '"' ); 

… ou você pode editar o regex nessa pergunta para reconhecer aspas simples em vez de aspas duplas:

 // Quoted fields. "(?:'([^']*(?:''[^']*)*)'|" + 

No entanto, isso pressupõe uma marcação que não está clara na sua pergunta. Por favor, esclareça o que todas as várias possibilidades de marcação podem ser, por meu comentário sobre a sua pergunta.

Eu tinha um caso de uso muito específico em que queria copiar células do Planilhas Google para meu aplicativo da web. As células podem include aspas duplas e caracteres de nova linha. Usando copiar e colar, as células são delimitadas por um caractere de tabulação e as células com dados ímpares são duplamente citadas. Eu tentei esta solução principal, o artigo vinculado usando regexp e Jquery-CSV e CSVToArray. http://papaparse.com/ É o único que funcionou fora da checkbox. Copiar e colar é perfeito com o Planilhas Google, com opções padrão de detecção automática.

Eu gostei da resposta do FakeRainBrigand, no entanto, ele contém alguns problemas: Ele não pode lidar com espaços em branco entre uma citação e uma vírgula, e não suporta duas vírgulas consecutivas. Eu tentei editar sua resposta, mas minha edição foi rejeitada por revisores que aparentemente não entendiam meu código. Aqui está a minha versão do código do FakeRainBrigand. Há também um violino: 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('|')); 

Minha resposta presume que sua input é um reflexo do código / conteúdo de fonts da web, em que caracteres de aspas simples e duplas são totalmente intercambiáveis, desde que ocorram como um conjunto de correspondência não escapado.

Você não pode usar o regex para isso. Você realmente tem que escrever um micro analisador para analisar a string que você deseja dividir. Eu vou, por causa dessa resposta, chamar as partes citadas de suas strings como sub-strings. Você precisa percorrer especificamente a cadeia. Considere o seguinte caso:

 var a = "some sample string with \"double quotes\" and 'single quotes' and some craziness like this: \\\" or \\'", b = "sample of code from JavaScript with a regex containing a comma /\,/ that should probably be ignored."; 

Neste caso, você não tem absolutamente nenhuma idéia de onde uma subcadeia inicia ou termina simplesmente analisando a input para um padrão de caractere. Em vez disso, você tem que escrever lógica para tomar decisões sobre se um caractere de aspas é usado um caractere de aspas, ele próprio não é citada e que o caractere de aspas não está seguindo uma fuga.

Eu não vou escrever esse nível de complexidade de código para você, mas você pode olhar para algo que eu escrevi recentemente que tem o padrão que você precisa. Este código não tem nada a ver com vírgulas, mas é de outra forma um micro parser válido o suficiente para você seguir escrevendo seu próprio código. Olhe para a function asifix do seguinte aplicativo:

https://github.com/austincheney/Pretty-Diff/blob/master/fulljsmin.js

As pessoas pareciam estar contra a RegEx por isso. Por quê?

 (\s*'[^']+'|\s*[^,]+)(?=,|$) 

Aqui está o código. Eu também fiz um violino .

 String.prototype.splitCSV = function(sep) { var regex = /(\s*'[^']+'|\s*[^,]+)(?=,|$)/g; return matches = this.match(regex); } var string = "'string, duppi, du', 23, 'string, duppi, du', lala"; var parsed = string.splitCSV(); alert(parsed.join('|')); 

Ao ler csv para string, ele contém valor nulo entre string, então tente \ 0 Linha por linha, isso me funciona.

 stringLine = stringLine.replace( /\0/g, "" ); 

Para complementar esta resposta

Se você precisar analisar aspas que escapingam com outra cotação, por exemplo:

 "some ""value"" that is on xlsx file",123 

Você pode usar

 function parse(text) { const csvExp = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|"([^""]*(?:"[\S\s][^""]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g; const values = []; text.replace(csvExp, (m0, m1, m2, m3, m4) => { if (m1 !== undefined) { values.push(m1.replace(/\\'/g, "'")); } else if (m2 !== undefined) { values.push(m2.replace(/\\"/g, '"')); } else if (m3 !== undefined) { values.push(m3.replace(/""/g, '"')); } else if (m4 !== undefined) { values.push(m4); } return ''; }); if (/,\s*$/.test(text)) { values.push(''); } return values; } 

Eu também tenho enfrentado o mesmo tipo de problema quando eu tenho que analisar um arquivo CSV. O arquivo contém uma coluna Endereço que contém o ‘,’.
Depois de analisar esse CSV para JSON, obtive um mapeamento sem correspondência das chaves ao convertê-lo em Arquivo JSON.
Eu usei o nó para analisar o arquivo e biblioteca como baby parse e csvtojson
Exemplo de arquivo –

 address,pincode foo,baar , 123456 

Enquanto eu estava analisando diretamente sem usar baby parse em JSON eu estava recebendo

 [{ address: 'foo', pincode: 'baar', 'field3': '123456' }] 

Então eu escrevi um código que remove a vírgula (,) com qualquer outro deliminador em cada campo

 /* csvString(input) = "address, pincode\\nfoo, bar, 123456\\n" output = "address, pincode\\nfoo {YOUR DELIMITER} bar, 123455\\n" */ const removeComma = function(csvString){ let delimiter = '|' let Baby = require('babyparse') let arrRow = Baby.parse(csvString).data; /* arrRow = [ [ 'address', 'pincode' ], [ 'foo, bar', '123456'] ] */ return arrRow.map((singleRow, index) => { //the data will include /* singleRow = [ 'address', 'pincode' ] */ return singleRow.map(singleField => { //for removing the comma in the feild return singleField.split(',').join(delimiter) }) }).reduce((acc, value, key) => { acc = acc +(Array.isArray(value) ? value.reduce((acc1, val)=> { acc1 = acc1+ val + ',' return acc1 }, '') : '') + '\n'; return acc; },'') } 

De acordo com esta postagem do blog , essa function deve fazer isso:

 String.prototype.splitCSV = function(sep) { for (var foo = this.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--) { if (foo[x].replace(/'\s+$/, "'").charAt(foo[x].length - 1) == "'") { if ((tl = foo[x].replace(/^\s+'/, "'")).length > 1 && tl.charAt(0) == "'") { foo[x] = foo[x].replace(/^\s*'|'\s*$/g, '').replace(/''/g, "'"); } else if (x) { foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep)); } else foo = foo.shift().split(sep).concat(foo); } else foo[x].replace(/''/g, "'"); } return foo; }; 

Você chamaria assim:

 var string = "'string, duppi, du', 23, lala"; var parsed = string.splitCSV(); alert(parsed.join("|")); 

Este tipo jsfiddle funciona, mas parece que alguns dos elementos têm espaços antes deles.

Além da resposta excelente e completa do ridgerunner, pensei em uma solução muito simples para quando o seu backend roda o php.

Adicione este arquivo php ao backend de seu domínio (digamos: csv.php )

  

Agora adicione esta function ao seu kit de ferramentas de javascript (deve ser revisado um pouco para tornar crossbrowser eu acredito.)

 function csvToArray(csv) { var oXhr = new XMLHttpRequest; oXhr.addEventListener("readystatechange", function () { if (this.readyState == 4 && this.status == 200) { console.log(this.responseText); console.log(JSON.parse(this.responseText)); } } ); oXhr.open("POST","path/to/csv.php",true); oXhr.setRequestHeader("Content-type","application/x-www-form-urlencoded; charset=utf-8"); oXhr.send("csv=" + encodeURIComponent(csv)); } 

Vai custar-lhe 1 chamada ajax, mas pelo menos você não irá duplicar o código nem include qualquer biblioteca externa.

Ref: http://php.net/manual/pt/function.str-getcsv.php