Java: dividir una cadena separada por comas pero ignorar las comas entre comillas

Resuelto Jason S asked hace 15 años • 13 respuestas

Tengo una cadena vagamente como esta:

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

que quiero dividir por comas, pero necesito ignorar las comas entre comillas. ¿Cómo puedo hacer esto? Parece que un enfoque de expresión regular falla; Supongo que puedo escanear manualmente e ingresar a un modo diferente cuando veo una cotización, pero sería bueno usar bibliotecas preexistentes. ( editar : supongo que me refiero a bibliotecas que ya forman parte del JDK o que ya forman parte de bibliotecas de uso común como Apache Commons).

la cadena anterior debe dividirse en:

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

nota: este NO es un archivo CSV, es una cadena única contenida en un archivo con una estructura general más grande

Jason S avatar Nov 18 '09 23:11 Jason S
Aceptado

Intentar:

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

Producción:

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

En otras palabras: divídalo por la coma solo si esa coma tiene cero o un número par de comillas delante .

O, un poco más amigable para los ojos:

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 produce lo mismo que el primer ejemplo.

EDITAR

Como lo menciona @MikeFHay en los comentarios:

Prefiero usar Guava's Splitter , ya que tiene valores predeterminados más sensatos (consulte la discusión anterior sobre las coincidencias vacías recortadas por String#split(), así que lo hice:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
Bart Kiers avatar Nov 18 '2009 16:11 Bart Kiers

Si bien me gustan las expresiones regulares en general, para este tipo de tokenización dependiente del estado creo que un analizador simple (que en este caso es mucho más simple de lo que esa palabra podría parecer) es probablemente una solución más limpia, en particular con respecto a la mantenibilidad. , p.ej:

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

Si no le importa conservar las comas entre comillas, puede simplificar este enfoque (sin manejo del índice inicial, sin caso especial del último carácter ) reemplazando las comas entre comillas por algo más y luego dividiéndolas en comas:

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<String> result = Arrays.asList(builder.toString().split(","));
Fabian Steeg avatar Jan 22 '2010 21:01 Fabian Steeg

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

https://github.com/pupi1985/JavaCSV-Reloaded (bifurcación de la biblioteca anterior que permitirá que la salida generada tenga terminadores de línea de Windows \r\ncuando no se ejecute Windows)

http://opencsv.sourceforge.net/

API CSV para Java

¿Puede recomendar una biblioteca Java para leer (y posiblemente escribir) archivos CSV?

¿Librería o aplicación Java para convertir archivos CSV a XML?

Jonathan Feinberg avatar Nov 18 '2009 16:11 Jonathan Feinberg

No recomendaría una respuesta de expresión regular de Bart, creo que la solución de análisis es mejor en este caso particular (como propuso Fabian). Probé la solución de expresiones regulares y la implementación de análisis propia. Descubrí que:

  1. El análisis es mucho más rápido que dividir con expresiones regulares con referencias inversas: ~20 veces más rápido para cadenas cortas, ~40 veces más rápido para cadenas largas.
  2. Regex no encuentra una cadena vacía después de la última coma. Sin embargo, esa no estaba en la pregunta original, era mi requisito.

Mi solución y prueba a continuación.

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<String> tokensList = new ArrayList<String>();
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);

Por supuesto, eres libre de cambiar a else-ifs en este fragmento si te sientes incómodo con su fealdad. Obsérvese entonces la falta de rotura tras el interruptor con separador. Se eligió StringBuilder en lugar de StringBuffer por diseño para aumentar la velocidad, donde la seguridad de los subprocesos es irrelevante.

Marcin Kosinski avatar Jun 06 '2014 09:06 Marcin Kosinski