284

I have a string vaguely like this:

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

that I want to split by commas -- but I need to ignore commas in quotes. How can I do this? Seems like a regexp approach fails; I suppose I can manually scan and enter a different mode when I see a quote, but it would be nice to use preexisting libraries. (edit: I guess I meant libraries that are already part of the JDK or already part of a commonly-used libraries like Apache Commons.)

the above string should split into:

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

note: this is NOT a CSV file, it's a single string contained in a file with a larger overall structure

Jason S
  • 184,598
  • 164
  • 608
  • 970

12 Answers12

501

Try:

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

Output:

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

In other words: split on the comma only if that comma has zero, or an even number of quotes ahead of it.

Or, a bit friendlier for the eyes:

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

which produces the same as the first example.

EDIT

As mentioned by @MikeFHay in the comments:

I prefer using Guava's Splitter, as it has saner defaults (see discussion above about empty matches being trimmed by String#split(), so I did:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
Dawngerpony
  • 3,288
  • 2
  • 34
  • 32
Bart Kiers
  • 166,582
  • 36
  • 299
  • 288
  • 1
    According to RFC 4180: Sec 2.6: "Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes." Sec 2.7: "If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote" So, if `String line = "equals: =,\"quote: \"\"\",\"comma: ,\""`, all you need to do is strip off the extraneous double quote characters. – Paul Hanbury Nov 18 '09 at 17:41
  • @Bart: my point being that your solution still works, even with embedded quotes – Paul Hanbury Nov 18 '09 at 17:43
  • @Bart Kiers: Lools like it fails when you have a comma inside the string value: e.g. "op","ID","script","Mike,s","Content-Length" – Misha Narinsky May 05 '12 at 22:53
  • @MichaelNarinsky, no, it will not split on the comma in `"Mike,s"`. If you run the example code I posted, you'd see that it doesn't. – Bart Kiers May 07 '12 at 06:34
  • @BartKiers I seem to be having trouble if my last column is empty e.g. `"val1, "val, 2","` I would expect to get an array of size three, but it ignores the last blank column and the size is 2. – Alex Apr 23 '14 at 14:43
  • 7
    @Alex, yeah, the comma *is* matched, but the empty match is not in the result. Add `-1` to the split method param: `line.split(regex, -1)`. See: http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#split(java.lang.String,%20int) – Bart Kiers Apr 23 '14 at 14:55
  • I ran into a test case, which fails: `String line = "\"EXCEPTION\",\"ER-0124\",\"10/09/2013 10:01:37\",814867,-1,\"SYSTEM\",\"ERROR\",\"[[F1:b4vvCaFBsG2Spk5cMCfiTt2dF2hO+f5ORcKWcBuLFZgY1EJg; ]] Message not available\",\"[[F1:b4vvCaFBsG2Spk5cMCfiTt2dF2hO+f5ORcKWcBuLFZgY1EJg; ]] TC6342-4: Test LOG_TAGGED with F1! tag, size as int\",3,\"ER-0121\",\"AB-9876,\"ER-0123\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",\"-\",1001,\"9.9.9.d\",\"ERXA_log_id_test [22388]\",\"ERXA_log_id_test.c\",\"?.?\",2301,\"801FFFFF\",\";;;;;;;;;\",\"Automatic\",\"00000-00000000-m0000-00\",\"END\"";` – Gerrit Brouwer Mar 16 '15 at 14:11
  • The first token contains all text until `...F1! tag` – Gerrit Brouwer Mar 16 '15 at 14:17
  • @GerritBrouwer, that isbecause `"AB-9876` does not have a closing quote. And its also a perfect example why to choose a proper CSV parser in production ;) – Bart Kiers Mar 16 '15 at 14:22
  • @BartKiers: thanx for the quick response! My confidence in your regex is completely restored :-) It turned out that I was sent a hand-crafted test file, rather than a machine-generated output file. – Gerrit Brouwer Mar 16 '15 at 14:44
  • I strongly suggest adding -1 to the split method to catch empty strings. `line.split(regex, -1)` – Peter Mar 28 '15 at 10:34
  • @BartKiers: Shouldn't we be adding ``-1`` to ``line.split()`` in the first example, too? – user1438038 May 29 '15 at 10:30
  • 2
    Works great! I prefer using Guava's Splitter, as it has saner defaults (see discussion above about empty matches being trimmed by String#split), so I did `Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"))`. – MikeFHay Jan 04 '16 at 12:23
  • You don't have to catch the group, you can do like this: `Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))` – Wilt May 26 '16 at 12:15
  • 6
    **WARNING!!!! This regexp is slow!!!** It has O(N^2) behavior in that the lookahead at each comma looks all the way to the end of the string. Using this regexp caused a 4x slowdown in large Spark jobs (e.g. 45 minutes -> 3 hours). The faster alternative is something like `findAllIn("(?s)(?:\".*?\"|[^\",]*)*")` in combination with a postprocessing step to skip the first (always-empty) field following each non-empty field. – Urban Vagabond Nov 26 '16 at 05:30
57

While I do like regular expressions in general, for this kind of state-dependent tokenization I believe a simple parser (which in this case is much simpler than that word might make it sound) is probably a cleaner solution, in particular with regards to maintainability, e.g.:

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

If you don't care about preserving the commas inside the quotes you could simplify this approach (no handling of start index, no last character special case) by replacing your commas in quotes by something else and then split at commas:

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(","));
matts
  • 6,738
  • 1
  • 33
  • 50
Fabian Steeg
  • 44,988
  • 7
  • 85
  • 112
22

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

https://github.com/pupi1985/JavaCSV-Reloaded (fork of the previous library that will allow the generated output to have Windows line terminators \r\n when not running Windows)

http://opencsv.sourceforge.net/

CSV API for Java

Can you recommend a Java library for reading (and possibly writing) CSV files?

Java lib or app to convert CSV to XML file?

Community
  • 1
  • 1
Jonathan Feinberg
  • 44,698
  • 7
  • 80
  • 103
13

I would not advise a regex answer from Bart, I find parsing solution better in this particular case (as Fabian proposed). I've tried regex solution and own parsing implementation I have found that:

  1. Parsing is much faster than splitting with regex with backreferences - ~20 times faster for short strings, ~40 times faster for long strings.
  2. Regex fails to find empty string after last comma. That was not in original question though, it was mine requirement.

My solution and test below.

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

Of course you are free to change switch to else-ifs in this snippet if you feel uncomfortable with its ugliness. Note then lack of break after switch with separator. StringBuilder was chosen instead to StringBuffer by design to increase speed, where thread safety is irrelevant.

Marcin Kosinski
  • 311
  • 3
  • 7
  • 2
    Interesting point regarding time splitting vs parsing. However, statement #2 is inaccurate. If you add a `-1` to the split method in Bart's answer, you will catch empty strings (including empty strings after the last comma): `line.split(regex, -1)` – Peter Mar 28 '15 at 10:39
  • +1 because it is a better solution to the problem for which I was searching for a solution: parsing a complex HTTP POST body parameter string – varontron Apr 30 '17 at 02:36
2

You're in that annoying boundary area where regexps almost won't do (as has been pointed out by Bart, escaping the quotes would make life hard) , and yet a full-blown parser seems like overkill.

If you are likely to need greater complexity any time soon I would go looking for a parser library. For example this one

djna
  • 54,992
  • 14
  • 74
  • 117
2

I was impatient and chose not to wait for answers... for reference it doesn't look that hard to do something like this (which works for my application, I don't need to worry about escaped quotes, as the stuff in quotes is limited to a few constrained forms):

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

(exercise for the reader: extend to handling escaped quotes by looking for backslashes also.)

Jason S
  • 184,598
  • 164
  • 608
  • 970
1

Try a lookaround like (?!\"),(?!\"). This should match , that are not surrounded by ".

Matthew Sowders
  • 1,640
  • 1
  • 19
  • 32
  • Pretty sure that would break for a list like: "foo",bar,"baz" – Angelo Genovese May 30 '13 at 22:57
  • 1
    I think you meant `(?<!"),(?!")`, but it still won't work. Given the string `one,two,"three,four"`, it correctly the matches the comma in `one,two`, but it also matches the comma in `"three,four"`, and fails to match one in `two,"three`. – Alan Moore May 19 '14 at 15:04
1

The simplest approach is not to match delimiters, i.e. commas, with a complex additional logic to match what is actually intended (the data which might be quoted strings), just to exclude false delimiters, but rather match the intended data in the first place.

The pattern consists of two alternatives, a quoted string ("[^"]*" or ".*?") or everything up to the next comma ([^,]+). To support empty cells, we have to allow the unquoted item to be empty and to consume the next comma, if any, and use the \\G anchor:

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

The pattern also contains two capturing groups to get either, the quoted string’s content or the plain content.

Then, with Java 9, we can get an array as

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

whereas older Java versions need a loop like

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

Adding the items to a List or an array is left as an excise to the reader.

For Java 8, you can use the results() implementation of this answer, to do it like the Java 9 solution.

For mixed content with embedded strings, like in the question, you can simply use

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

But then, the strings are kept in their quoted form.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

what about a one-liner using String.split()?

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

A regular expression is not capable of handling escaped characters. For my application, I needed the ability to escape quotes and spaces (my separator is spaces, but the code is the same).

Here is my solution in Kotlin (the language from this particular application), based on the one from Fabian Steeg:

fun parseString(input: String): List<String> {
    val result = mutableListOf<String>()
    var inQuotes = false
    var inEscape = false
    val current = StringBuilder()
    for (i in input.indices) {
        // If this character is escaped, add it without looking
        if (inEscape) {
            inEscape = false
            current.append(input[i])
            continue
        }
        when (val c = input[i]) {
            '\\' -> inEscape = true // escape the next character, \ isn't added to result
            ',' -> if (inQuotes) {
                current.append(c)
            } else {
                result += current.toString()
                current.clear()
            }
            '"' -> inQuotes = !inQuotes
            else -> current.append(c)
        }
    }
    if (current.isNotEmpty()) {
        result += current.toString()
    }
    return result
}

I think this is not a place to use regular expressions. Contrary to other opinions, I don't think a parser is overkill. It's about 20 lines and fairly easy to test.

Sean
  • 2,632
  • 2
  • 27
  • 35
  • that's not Java – Jason S Oct 18 '21 at 13:41
  • It's pretty simple to translate kotlin to java. I wrote it for a kotlin project and used this as an example, so I thought I'd share and I didn't see the need to do the translation, particularly because the above code is tested. Do you want me to translate it? – Sean Oct 19 '21 at 04:02
  • Oh, I thought it was Javascript or something. If you post code, you need to tell people which language it is. :-) This is a 12-year-old question so I don't have any preference and won't change my accepted answer. Just realize that people who find this question are likely looking for a solution in Java. – Jason S Oct 19 '21 at 19:12
  • No problem. I honestly just posted it here because I found this question when I was writing it, and I thought if someone else did the same, I'd be happy that they found it. – Sean Oct 19 '21 at 21:25
  • Also, the solution from Fabian Steeg is, I believe, better than the accepted answer. If you were to change the accepted answer, my vote is that one. This answer is based on that, which I'm about to edit to give credit. – Sean Oct 19 '21 at 21:31
0

Rather than use lookahead and other crazy regex, just pull out the quotes first. That is, for every quote grouping, replace that grouping with __IDENTIFIER_1 or some other indicator, and map that grouping to a map of string,string.

After you split on comma, replace all mapped identifiers with the original string values.

Stefan Kendall
  • 66,414
  • 68
  • 253
  • 406
-2

I would do something like this:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
Jason Plank
  • 2,336
  • 5
  • 31
  • 40
Woot4Moo
  • 23,987
  • 16
  • 94
  • 151