Java: suddivisione di una stringa separata da virgola ma ignorando le virgole tra virgolette


249

Ho una stringa vagamente simile a questa:

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

che voglio dividere per virgole, ma devo ignorare le virgolette tra virgolette. Come posso fare questo? Sembra un approccio regexp fallito; Suppongo di poter scansionare manualmente ed entrare in una modalità diversa quando vedo un preventivo, ma sarebbe bello usare librerie preesistenti. ( modifica : suppongo che intendessi librerie che fanno già parte del JDK o che fanno già parte di librerie di uso comune come Apache Commons.)

la stringa sopra dovrebbe essere divisa in:

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

nota: questo NON è un file CSV, è una singola stringa contenuta in un file con una struttura complessiva più ampia

Risposte:


435

Provare:

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

Produzione:

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

In altre parole: dividi la virgola solo se quella virgola ha zero o un numero pari di virgolette davanti a sé .

O, un po 'più amichevole per gli occhi:

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

che produce lo stesso del primo esempio.

MODIFICARE

Come menzionato da @MikeFHay nei commenti:

Preferisco usare Guava's Splitter , in quanto ha impostazioni predefinite più sanitarie (vedi la discussione sopra sulle partite vuote che vengono tagliate da String#split(), quindi ho fatto:

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

Secondo RFC 4180: Sec 2.6: "I campi contenenti interruzioni di riga (CRLF), virgolette doppie e virgole devono essere racchiusi tra virgolette doppie". Sec 2.7: "Se per racchiudere i campi si usano le virgolette doppie, allora una virgoletta doppia che appare all'interno di un campo deve essere sfuggita precedendola con un'altra virgoletta doppia" Quindi, se String line = "equals: =,\"quote: \"\"\",\"comma: ,\""tutto ciò che devi fare è togliere la doppia citazione estranea personaggi.
Paul Hanbury,

@Bart: il punto è che la tua soluzione funziona ancora, anche con virgolette incorporate
Paul Hanbury

6
@Alex, sì, la virgola è abbinata, ma la corrispondenza vuota non è nel risultato. Aggiungere -1al metodo split param: line.split(regex, -1). Vedi: docs.oracle.com/javase/6/docs/api/java/lang/…
Bart Kiers,

2
Funziona alla grande! Preferisco usare Guava's Splitter, in quanto ha impostazioni predefinite più sanitarie (vedi la discussione sopra sulle corrispondenze vuote che vengono tagliate da String # split), quindi l'ho fatto Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFay

2
AVVERTIMENTO!!!! Questa regexp è lenta !!! Ha un comportamento O (N ^ 2) in quanto il lookahead di ogni virgola guarda fino alla fine della stringa. L'uso di questo regexp ha causato un rallentamento di 4x nei lavori Spark di grandi dimensioni (ad es. 45 minuti -> 3 ore). L'alternativa più veloce è qualcosa come findAllIn("(?s)(?:\".*?\"|[^\",]*)*")in combinazione con una fase di postelaborazione per saltare il primo campo (sempre vuoto) dopo ogni campo non vuoto.
Urban Vagabond,

46

Mentre mi piacciono le espressioni regolari in generale, per questo tipo di tokenizzazione dipendente dallo stato credo che un semplice parser (che in questo caso è molto più semplice di quanto la parola possa far sembrare) sia probabilmente una soluzione più pulita, in particolare per quanto riguarda la manutenibilità , per esempio:

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
    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 non ti interessa conservare le virgole all'interno delle virgolette, potresti semplificare questo approccio (nessuna gestione dell'indice iniziale, nessun caso speciale dell'ultimo carattere ) sostituendo le virgole tra virgolette con qualcos'altro e quindi dividere in virgole:

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(","));

Le virgolette devono essere rimosse dai token analizzati, dopo aver analizzato la stringa.
Sudhir N

Trovato tramite Google, simpatico algoritmo fratello, semplice e facile da adattare, d'accordo. roba con stateful dovrebbe essere fatta tramite parser, regex è un casino.
Rudolf Schmidt,

2
Tieni presente che se una virgola è l'ultimo carattere sarà nel valore String dell'ultimo elemento.
Gabriel Gates,

21

3
Buona chiamata a riconoscere che l'OP stava analizzando un file CSV. Una libreria esterna è estremamente appropriata per questa attività.
Stefan Kendall,

1
Ma la stringa è una stringa CSV; dovresti essere in grado di utilizzare un API CSV su quella stringa direttamente.
Michael Brewer-Davis,

sì, ma questo compito è abbastanza semplice, e una parte molto più piccola di un'applicazione più grande, che non mi va di inserire un'altra libreria esterna.
Jason S,

7
non necessariamente ... le mie capacità sono spesso adeguate, ma traggono beneficio dall'essere affinate.
Jason S,

9

Non consiglierei una risposta regex da Bart, trovo che la soluzione di analisi sia migliore in questo caso particolare (come proposto da Fabian). Ho provato la soluzione regex e la propria implementazione dell'analisi ho scoperto che:

  1. L'analisi è molto più veloce della divisione con regex con riferimenti indietro - ~ 20 volte più veloce per stringhe brevi, ~ 40 volte più veloce per stringhe lunghe.
  2. Regex non riesce a trovare una stringa vuota dopo l'ultima virgola. Questo non era nella domanda originale, era il mio requisito.

La mia soluzione e prova di seguito.

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

Naturalmente sei libero di cambiare passaggio agli altri se in questo frammento se ti senti a disagio con la sua bruttezza. Nota quindi mancanza di interruzione dopo interruttore con separatore. StringBuilder è stato scelto invece da StringBuffer in base alla progettazione per aumentare la velocità, dove la sicurezza del thread è irrilevante.


2
Punto interessante per quanto riguarda la suddivisione del tempo e l'analisi. Tuttavia, l'affermazione n. 2 non è precisa. Se aggiungi -1a al metodo split nella risposta di Bart, otterrai stringhe vuote (comprese le stringhe vuote dopo l'ultima virgola):line.split(regex, -1)
Peter

+1 perché è una soluzione migliore al problema per il quale stavo cercando una soluzione: analisi di una stringa di parametri del corpo POST HTTP complessa
varontron

2

Prova un lookaround come (?!\"),(?!\"). Questo dovrebbe corrispondere a ,quello che non è circondato ".


Abbastanza sicuro che si spezzerebbe per un elenco come: "foo", bar, "baz"
Angelo Genovese,

1
Penso che volevi dire (?<!"),(?!"), ma ancora non funzionerà. Data la stringa one,two,"three,four", corrisponde correttamente alla virgola in one,two, ma corrisponde anche alla virgola in "three,four"e non corrisponde a una in two,"three.
Alan Moore,

Funziona perfettamente per me, IMHO Penso che questa sia una risposta migliore perché è più breve e più facilmente comprensibile
Ordiel

2

Sei in quella fastidiosa area di confine dove le regexps quasi non lo faranno (come è stato sottolineato da Bart, sfuggire alle virgolette renderebbe la vita difficile), eppure un parser in piena regola sembra eccessivo.

Se è probabile che tu abbia bisogno di maggiore complessità in qualsiasi momento, andrei a cercare una libreria di parser. Ad esempio questo


2

Ero impaziente e ho scelto di non aspettare le risposte ... per riferimento non sembra così difficile fare qualcosa del genere (che funziona per la mia applicazione, non ho bisogno di preoccuparmi delle virgolette sfuggite, come le cose tra virgolette è limitato a poche forme vincolate):

final static private Pattern splitSearchPattern = Pattern.compile("[\",]"); 
private List<String> splitByCommasNotInQuotes(String s) {
    if (s == null)
        return Collections.emptyList();

    List<String> list = new ArrayList<String>();
    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;
}

(esercizio per il lettore: estendere alla gestione delle virgolette sfuggite cercando anche le barre rovesciate.)


1

L'approccio più semplice non è quello di abbinare i delimitatori, cioè le virgole, con una logica aggiuntiva complessa per abbinare ciò che è effettivamente inteso (i dati che potrebbero essere citati stringhe), solo per escludere falsi delimitatori, ma piuttosto abbinare i dati previsti in primo luogo.

Il modello è composto da due alternative, una stringa tra virgolette ( "[^"]*"o ".*?") o tutto fino alla virgola successiva ( [^,]+). Per supportare celle vuote, dobbiamo consentire che l'elemento non quotato sia vuoto e consumare la virgola successiva, se presente, e utilizzare l' \\Gancoraggio:

Pattern p = Pattern.compile("\\G\"(.*?)\",?|([^,]*),?");

Il modello contiene anche due gruppi di acquisizione per ottenere il contenuto della stringa tra virgolette o il contenuto normale.

Quindi, con Java 9, possiamo ottenere un array come

String[] a = p.matcher(input).results()
    .map(m -> m.group(m.start(1)<0? 2: 1))
    .toArray(String[]::new);

mentre le versioni precedenti di Java richiedono un ciclo simile

for(Matcher m = p.matcher(input); m.find(); ) {
    String token = m.group(m.start(1)<0? 2: 1);
    System.out.println("found: "+token);
}

Aggiunta degli elementi a List o una matrice viene lasciata come accisa al lettore.

Per Java 8, è possibile utilizzare l' results()implementazione di questa risposta , per farlo come la soluzione Java 9.

Per contenuti misti con stringhe incorporate, come nella domanda, puoi semplicemente usare

Pattern p = Pattern.compile("\\G((\"(.*?)\"|[^,])*),?");

Ma poi, le stringhe vengono mantenute nella loro forma quotata.


0

Invece di usare lookahead e altri regex pazzi, basta tirare fuori prima le virgolette. Cioè, per ogni raggruppamento di preventivi, sostituiscilo con__IDENTIFIER_1 o qualche altro indicatore e mappare quel raggruppamento su una mappa di stringa, stringa.

Dopo aver diviso la virgola, sostituire tutti gli identificatori mappati con i valori di stringa originali.


e come trovare raggruppamenti di citazioni senza regex pazzi?
Kai Huppmann,

Per ogni personaggio, se il personaggio è virgolette, trova la citazione successiva e sostituiscile con il raggruppamento. Se nessuna citazione successiva, fatto.
Stefan Kendall,

0

che dire di un one-liner usando String.split ()?

String s = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
String[] split = s.split( "(?<!\".{0,255}[^\"]),|,(?![^\"].*\")" );

-1

Vorrei fare qualcosa del genere:

boolean foundQuote = false;

if(charAtIndex(currentStringIndex) == '"')
{
   foundQuote = true;
}

if(foundQuote == true)
{
   //do nothing
}

else 

{
  string[] split = currentString.split(',');  
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.