Java: dividindo uma string separada por vírgula mas ignorando aspas entre aspas

Eu tenho uma string vagamente como esta:

foo,bar,c;qual="baz,blurb",d;junk="quux,syzygy" 

que eu quero dividir por vírgulas – mas eu preciso ignorar vírgulas entre aspas. Como posso fazer isso? Parece que uma abordagem regexp falha; Suponho que posso digitalizar manualmente e entrar em um modo diferente quando vejo uma cotação, mas seria bom usar bibliotecas preexistentes. ( editar : Eu acho que eu quis dizer bibliotecas que já fazem parte do JDK ou já fazem parte de uma biblioteca comumente usada como o Apache Commons.)

a string acima deve ser dividida em:

 foo bar c;qual="baz,blurb" d;junk="quux,syzygy" 

Nota: este não é um arquivo CSV, é uma única string contida em um arquivo com uma estrutura geral maior

Experimentar:

 public class Main { public static void main(String[] args) { String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; String[] tokens = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); for(String t : tokens) { System.out.println("> "+t); } } } 

Saída:

 > foo > bar > c;qual="baz,blurb" > d;junk="quux,syzygy" 

Em outras palavras: dividir na vírgula apenas se essa vírgula tiver zero, ou um número par de citações à frente dela .

Ou, um pouco mais amigável para os olhos:

 public class Main { public static void main(String[] args) { String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; String otherThanQuote = " [^\"] "; String quotedString = String.format(" \" %s* \" ", otherThanQuote); String regex = String.format("(?x) "+ // enable comments, ignore white spaces ", "+ // match a comma "(?= "+ // start positive look ahead " (?: "+ // start non-capturing group 1 " %s* "+ // match 'otherThanQuote' zero or more times " %s "+ // match 'quotedString' " )* "+ // end group 1 and repeat it zero or more times " %s* "+ // match 'otherThanQuote' " $ "+ // match the end of the string ") ", // stop positive look ahead otherThanQuote, quotedString, otherThanQuote); String[] tokens = line.split(regex, -1); for(String t : tokens) { System.out.println("> "+t); } } } 

que produz o mesmo que o primeiro exemplo.

EDITAR

Como mencionado por @MikeFHay nos comentários:

Eu prefiro usar o Splitter da Goiaba , já que tem padrões mais simples (veja a discussão acima sobre correspondências vazias sendo aparadas pela String#split() , então eu fiz:

 Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) 

Embora eu goste de expressões regulares em geral, para este tipo de tokenização dependente do estado, acredito que um simples analisador (que neste caso é muito mais simples do que a palavra possa fazer parecer) é provavelmente uma solução mais limpa, em particular no que diz respeito à manutenção. , por exemplo:

 String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; List result = new ArrayList(); int start = 0; boolean inQuotes = false; for (int current = 0; current < input.length(); current++) { if (input.charAt(current) == '\"') inQuotes = !inQuotes; // toggle state boolean atLastChar = (current == input.length() - 1); if(atLastChar) result.add(input.substring(start)); else if (input.charAt(current) == ',' && !inQuotes) { result.add(input.substring(start, current)); start = current + 1; } } 

Se você não se importa em preservar as vírgulas dentro das aspas, você pode simplificar essa abordagem (sem manipular o índice inicial, nenhum caso especial de último caractere ), substituindo as vírgulas entre aspas por alguma outra coisa e então dividindo em vírgulas:

 String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; StringBuilder builder = new StringBuilder(input); boolean inQuotes = false; for (int currentIndex = 0; currentIndex < builder.length(); currentIndex++) { char currentChar = builder.charAt(currentIndex); if (currentChar == '\"') inQuotes = !inQuotes; // toggle state if (currentChar == ',' && inQuotes) { builder.setCharAt(currentIndex, ';'); // or '♡', and replace later } } List result = Arrays.asList(builder.toString().split(",")); 

http://sourceforge.net/projects/javacsv/

https://github.com/pupi1985/JavaCSV-Reloaded (bifurcação da biblioteca anterior que permitirá que a saída gerada tenha terminadores de linha do Windows \r\n quando não estiver executando o Windows)

http://opencsv.sourceforge.net/

API CSV para Java

Você pode recomendar uma biblioteca Java para ler (e possivelmente escrever) arquivos CSV?

Java lib ou aplicativo para converter CSV para o arquivo XML?

Eu não aconselharia uma resposta de regex de Bart, acho que a solução de análise é melhor neste caso específico (como Fabian propôs). Eu tentei a solução de regex e a própria implementação de análise. Descobri que:

  1. A análise é muito mais rápida do que a divisão com regex com referências anteriores – ~ 20 vezes mais rápido para strings curtas, ~ 40 vezes mais rápido para strings longas.
  2. Regex não consegue encontrar uma string vazia após a última vírgula. Isso não estava na questão original, porém, era minha exigência.

Minha solução e teste abaixo.

 String tested = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\","; long start = System.nanoTime(); String[] tokens = tested.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); long timeWithSplitting = System.nanoTime() - start; start = System.nanoTime(); List tokensList = new ArrayList(); boolean inQuotes = false; StringBuilder b = new StringBuilder(); for (char c : tested.toCharArray()) { switch (c) { case ',': if (inQuotes) { b.append(c); } else { tokensList.add(b.toString()); b = new StringBuilder(); } break; case '\"': inQuotes = !inQuotes; default: b.append(c); break; } } tokensList.add(b.toString()); long timeWithParsing = System.nanoTime() - start; System.out.println(Arrays.toString(tokens)); System.out.println(tokensList.toString()); System.out.printf("Time with splitting:\t%10d\n",timeWithSplitting); System.out.printf("Time with parsing:\t%10d\n",timeWithParsing); 

É claro que você é livre para trocar o interruptor para else-ifs neste trecho se você se sentir desconfortável com sua fealdade. Note então a falta de quebra após o interruptor com o separador. StringBuilder foi escolhido em vez de StringBuffer por design para aumentar a velocidade, onde a segurança do thread é irrelevante.

Tente um lookaround como (?!\"),(?!\") . Isso deve corresponder , que não esteja cercado por " .

Você está nessa área de limite irritante onde quase não se faz regexps (como foi apontado por Bart, escaping das citações tornaria a vida difícil), e ainda assim um analisador completo parece um exagero.

Se você precisar de maior complexidade em breve, eu procuraria uma biblioteca de analisadores. Por exemplo, esse aqui

Eu estava impaciente e optei por não esperar por respostas … para referência não parece tão difícil fazer algo assim (o que funciona para o meu aplicativo, eu não preciso me preocupar com aspas escapadas, como o material entre aspas está limitado a algumas formas restritas):

 final static private Pattern splitSearchPattern = Pattern.compile("[\",]"); private List splitByCommasNotInQuotes(String s) { if (s == null) return Collections.emptyList(); List list = new ArrayList(); Matcher m = splitSearchPattern.matcher(s); int pos = 0; boolean quoteMode = false; while (m.find()) { String sep = m.group(); if ("\"".equals(sep)) { quoteMode = !quoteMode; } else if (!quoteMode && ",".equals(sep)) { int toPos = m.start(); list.add(s.substring(pos, toPos)); pos = m.end(); } } if (pos < s.length()) list.add(s.substring(pos)); return list; } 

(Exercício para o leitor: estenda-se ao manuseamento de citações com escape, procurando barras invertidas também.)

Em vez de usar lookahead e outro regex louco, apenas puxe as aspas primeiro. Ou seja, para cada agrupamento de cotações, substitua esse agrupamento por __IDENTIFIER_1 ou outro indicador e mapeie esse agrupamento para um mapa de string, string.

Depois de dividir por vírgula, substitua todos os identificadores mapeados pelos valores originais da string.

Eu faria algo assim:

 boolean foundQuote = false; if(charAtIndex(currentStringIndex) == '"') { foundQuote = true; } if(foundQuote == true) { //do nothing } else { string[] split = currentString.split(','); }