Java: Teilen einer durch Kommas getrennten Zeichenfolge, Ignorieren von Kommas in Anführungszeichen

249

Ich habe eine vage Saite wie folgt:

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

dass ich durch Kommas teilen möchte - aber ich muss Kommas in Anführungszeichen ignorieren. Wie kann ich das machen? Scheint, als ob ein Regexp-Ansatz fehlschlägt. Ich nehme an, ich kann manuell scannen und in einen anderen Modus wechseln, wenn ich ein Zitat sehe, aber es wäre schön, bereits vorhandene Bibliotheken zu verwenden. ( Bearbeiten : Ich denke, ich meinte Bibliotheken, die bereits Teil des JDK oder bereits Teil einer häufig verwendeten Bibliothek wie Apache Commons sind.)

Die obige Zeichenfolge sollte aufgeteilt werden in:

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

Hinweis: Dies ist KEINE CSV-Datei, sondern eine einzelne Zeichenfolge, die in einer Datei mit einer größeren Gesamtstruktur enthalten ist

Jason S.
quelle

Antworten:

435

Versuchen:

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

Ausgabe:

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

Mit anderen Worten: Teilen Sie das Komma nur, wenn dieses Komma Null oder eine gerade Anzahl von Anführungszeichen vor sich hat .

Oder etwas freundlicher für die Augen:

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

das ergibt das gleiche wie das erste Beispiel.

BEARBEITEN

Wie von @MikeFHay in den Kommentaren erwähnt:

Ich bevorzuge die Verwendung von Guavas Splitter , da er vernünftigere Standardeinstellungen hat (siehe Diskussion oben über leere Übereinstimmungen, die von gekürzt werden String#split(), also habe ich Folgendes getan:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
Bart Kiers
quelle
Gemäß RFC 4180: Abschnitt 2.6: "Felder mit Zeilenumbrüchen (CRLF), doppelten Anführungszeichen und Kommas sollten in doppelte Anführungszeichen eingeschlossen werden." Abschnitt 2.7: "Wenn zum Einschließen von Feldern doppelte Anführungszeichen verwendet werden, muss ein doppeltes Anführungszeichen, das in einem Feld angezeigt wird, maskiert werden, indem ein weiteres doppeltes Anführungszeichen vorangestellt wird." Wenn String line = "equals: =,\"quote: \"\"\",\"comma: ,\""Sie also nur das überflüssige doppelte Anführungszeichen entfernen müssen Zeichen.
Paul Hanbury
@ Bart: Mein Punkt ist, dass Ihre Lösung auch mit eingebetteten Anführungszeichen noch funktioniert
Paul Hanbury
6
@ Alex, ja, das Komma ist angepasst, aber das leere Spiel ist im Ergebnis nicht. In -1auf die Split - Methode param: line.split(regex, -1). Siehe: docs.oracle.com/javase/6/docs/api/java/lang/…
Bart Kiers
2
Funktioniert super! Ich bevorzuge die Verwendung von Guavas Splitter, da er vernünftigere Standardeinstellungen hat (siehe Diskussion oben über leere Übereinstimmungen, die durch String # split gekürzt werden), also habe ich es getan Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFHay
2
WARNUNG!!!! Dieser reguläre Ausdruck ist langsam !!! Es hat das Verhalten O (N ^ 2), dass der Lookahead bei jedem Komma bis zum Ende der Zeichenfolge reicht. Die Verwendung dieses regulären Ausdrucks führte bei großen Spark-Jobs zu einer vierfachen Verlangsamung (z. B. 45 Minuten -> 3 Stunden). Die schnellere Alternative ist etwa findAllIn("(?s)(?:\".*?\"|[^\",]*)*")in Kombination mit einem Nachbearbeitungsschritt, bei dem das erste (immer leere) Feld nach jedem nicht leeren Feld übersprungen wird.
Urban Vagabond
46

Während ich im Allgemeinen reguläre Ausdrücke mag, glaube ich, dass für diese Art der zustandsabhängigen Tokenisierung ein einfacher Parser (der in diesem Fall viel einfacher ist, als dieses Wort es klingen lässt) wahrscheinlich eine sauberere Lösung ist, insbesondere im Hinblick auf die Wartbarkeit , z.B:

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

Wenn Sie die Kommas in den Anführungszeichen nicht beibehalten möchten, können Sie diesen Ansatz vereinfachen (keine Behandlung des Startindex, kein Sonderfall für das letzte Zeichen ), indem Sie Ihre Kommas in Anführungszeichen durch etwas anderes ersetzen und dann durch Kommas teilen:

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
quelle
Anführungszeichen sollten aus den analysierten Token entfernt werden, nachdem die Zeichenfolge analysiert wurde.
Sudhir N
Gefunden über Google, netter Algorithmus bro, einfach und leicht anzupassen, stimme zu. Stateful Sachen sollten über Parser gemacht werden, Regex ist ein Chaos.
Rudolf Schmidt
2
Beachten Sie, dass ein Komma, wenn es das letzte Zeichen ist, im String-Wert des letzten Elements enthalten ist.
Gabriel Gates
21

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

https://github.com/pupi1985/JavaCSV-Reloaded (Verzweigung der vorherigen Bibliothek, die es der generierten Ausgabe ermöglicht, Windows-Zeilenabschlüsse zu verwenden, \r\nwenn Windows nicht ausgeführt wird)

http://opencsv.sourceforge.net/

CSV-API für Java

Können Sie eine Java-Bibliothek zum Lesen (und möglicherweise Schreiben) von CSV-Dateien empfehlen?

Java lib oder App zum Konvertieren von CSV in XML-Datei?

Jonathan Feinberg
quelle
3
Guter Anruf, der erkennt, dass das OP eine CSV-Datei analysiert hat. Eine externe Bibliothek ist für diese Aufgabe äußerst geeignet.
Stefan Kendall
1
Die Zeichenfolge ist jedoch eine CSV-Zeichenfolge. Sie sollten in der Lage sein, eine CSV-API direkt für diese Zeichenfolge zu verwenden.
Michael Brewer-Davis
Ja, aber diese Aufgabe ist einfach genug und ein viel kleinerer Teil einer größeren Anwendung, als dass ich keine Lust hätte, eine andere externe Bibliothek aufzurufen.
Jason S
7
nicht unbedingt ... meine Fähigkeiten sind oft ausreichend, aber sie profitieren davon, geschliffen zu werden.
Jason S
9

Ich würde Bart keine reguläre Antwort empfehlen, ich finde die Parsing-Lösung in diesem speziellen Fall besser (wie Fabian vorgeschlagen hat). Ich habe Regex-Lösung und eigene Parsing-Implementierung ausprobiert. Ich habe Folgendes festgestellt:

  1. Das Parsen ist viel schneller als das Teilen mit Regex mit Rückreferenzen - ~ 20-mal schneller für kurze Zeichenfolgen, ~ 40-mal schneller für lange Zeichenfolgen.
  2. Regex findet nach dem letzten Komma keine leere Zeichenfolge. Das war jedoch nicht in der ursprünglichen Frage, es war meine Anforderung.

Meine Lösung und Test unten.

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

Natürlich können Sie in diesem Snippet den Wechsel zu else-ifs ändern, wenn Sie sich mit seiner Hässlichkeit unwohl fühlen. Beachten Sie dann die fehlende Unterbrechung nach dem Schalter mit Trennzeichen. StringBuilder wurde stattdessen als StringBuffer ausgewählt, um die Geschwindigkeit zu erhöhen, wenn die Thread-Sicherheit keine Rolle spielt.

Marcin Kosinski
quelle
2
Interessanter Punkt in Bezug auf Zeitaufteilung und Analyse. Aussage Nr. 2 ist jedoch ungenau. Wenn Sie -1der Teilungsmethode in Barts Antwort ein hinzufügen , werden Sie leere Zeichenfolgen (einschließlich leerer Zeichenfolgen nach dem letzten Komma) abfangen:line.split(regex, -1)
Peter
+1, weil es eine bessere Lösung für das Problem ist, für das ich nach einer Lösung gesucht habe: Parsen einer komplexen HTTP-POST-Body-Parameterzeichenfolge
Varontron
2

Versuchen Sie einen Lookaround wie (?!\"),(?!\"). Dies sollte übereinstimmen ,, die nicht von umgeben sind ".

Matthew Sowders
quelle
Ziemlich sicher, dass für eine Liste wie "foo", bar, "baz"
Angelo Genovese
1
Ich denke du meintest (?<!"),(?!"), aber es wird immer noch nicht funktionieren. Wenn die Zeichenfolge angegeben ist one,two,"three,four", stimmt sie korrekt mit dem Komma überein one,two, stimmt jedoch auch mit dem Komma "three,four"überein und stimmt nicht mit dem Komma überein two,"three.
Alan Moore
Es scheint perfekt für mich zu funktionieren, IMHO denke ich, dass dies eine bessere Antwort ist, da es kürzer und leichter verständlich ist
Ordiel
2

Sie befinden sich in einem nervigen Grenzbereich, in dem Regexps fast nicht funktionieren (wie Bart betont hat, würde das Entkommen aus den Zitaten das Leben schwer machen), und dennoch scheint ein ausgewachsener Parser übertrieben zu sein.

Wenn Sie wahrscheinlich bald eine größere Komplexität benötigen, würde ich nach einer Parser-Bibliothek suchen. Zum Beispiel dieser

djna
quelle
2

Ich war ungeduldig und entschied mich, nicht auf Antworten zu warten ... als Referenz sieht es nicht so schwer aus, so etwas zu tun (was für meine Anwendung funktioniert, ich muss mich nicht um entkommene Anführungszeichen kümmern, wie das Zeug in Anführungszeichen ist auf einige eingeschränkte Formen beschränkt):

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

(Übung für den Leser: Erweitern Sie den Umgang mit maskierten Anführungszeichen, indem Sie auch nach Backslashes suchen.)

Jason S.
quelle
1

Der einfachste Ansatz besteht darin, Trennzeichen, dh Kommas, nicht mit einer komplexen zusätzlichen Logik abzugleichen, die mit dem übereinstimmt, was tatsächlich beabsichtigt ist (die Daten, bei denen es sich möglicherweise um Zeichenfolgen handelt), sondern nur falsche Trennzeichen auszuschließen, sondern zunächst mit den beabsichtigten Daten übereinzustimmen.

Das Muster besteht aus zwei Alternativen, einer Zeichenfolge in Anführungszeichen ( "[^"]*"oder ".*?") oder allem bis zum nächsten Komma ( [^,]+). Um leere Zellen zu unterstützen, müssen wir zulassen, dass das nicht zitierte Element leer ist, und gegebenenfalls das nächste Komma verwenden und den \\GAnker verwenden:

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

Das Muster enthält auch zwei Erfassungsgruppen, um entweder den Inhalt der angegebenen Zeichenfolge oder den einfachen Inhalt abzurufen.

Dann können wir mit Java 9 ein Array als erhalten

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

Während ältere Java-Versionen eine Schleife wie benötigen

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

Das Hinzufügen der Elemente zu einem Listoder einem Array bleibt dem Leser als Verbrauchsteuer überlassen.

Für Java 8 können Sie die results()Implementierung dieser Antwort verwenden , um dies wie bei der Java 9-Lösung zu tun.

Für gemischte Inhalte mit eingebetteten Zeichenfolgen, wie in der Frage, können Sie einfach verwenden

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

Aber dann werden die Zeichenfolgen in ihrer zitierten Form gehalten.

Holger
quelle
0

Anstatt Lookahead und andere verrückte Regex zu verwenden, ziehen Sie einfach zuerst die Anführungszeichen heraus. Das heißt, ersetzen Sie für jede Anführungszeichengruppierung diese Gruppierung durch __IDENTIFIER_1oder einen anderen Indikator und ordnen Sie diese Gruppierung einer Zuordnung von Zeichenfolge, Zeichenfolge zu.

Ersetzen Sie nach dem Teilen durch Komma alle zugeordneten Bezeichner durch die ursprünglichen Zeichenfolgenwerte.

Stefan Kendall
quelle
und wie finde ich Zitatgruppierungen ohne verrückte RegexS?
Kai Huppmann
Wenn es sich bei dem Zeichen um ein Anführungszeichen handelt, suchen Sie für jedes Zeichen das nächste Anführungszeichen und ersetzen Sie es durch Gruppierung. Wenn kein nächstes Zitat, fertig.
Stefan Kendall
0

Was ist mit einem Einzeiler mit String.split ()?

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

Ich würde so etwas machen:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
Woot4Moo
quelle