Java: splitting a comma-separated string but ignoring commas in quotes

前端 未结 11 1554
广开言路
广开言路 2020-11-21 05:16

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 igno

相关标签:
11条回答
  • 2020-11-21 05:48

    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(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
    
    0 讨论(0)
  • 2020-11-21 05:52

    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.

    0 讨论(0)
  • 2020-11-21 06:04

    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.

    0 讨论(0)
  • 2020-11-21 06:05

    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}[^\"]),|,(?![^\"].*\")" );
    
    0 讨论(0)
  • 2020-11-21 06:07

    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(","));
    
    0 讨论(0)
  • 2020-11-21 06:08

    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

    0 讨论(0)
提交回复
热议问题