首页 文章

Java:拆分以逗号分隔的字符串,但忽略引号中的逗号

提问于
浏览
218

我有一个模糊的字符串:

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

我想用逗号分割 - 但我需要在引号中忽略逗号 . 我怎样才能做到这一点?看起来像正则表达式方法失败;我想我可以在看到引号时手动扫描并进入不同的模式,但使用预先存在的库会更好 . (编辑:我想我的意思是已经是JDK的一部分或已经是Apache Commons等常用库的一部分的库 . )

上面的字符串应该分成:

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

note: 这不是CSV文件,它是包含在具有更大整体结构的文件中的单个字符串

9 回答

  • 20

    虽然我确实喜欢正则表达式,但是对于这种依赖于状态的标记化,我相信一个简单的解析器(在这种情况下比这个单词要简单得多)可能是一个更清晰的解决方案,特别是在可维护性方面 . ,例如:

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

    如果你不关心在引号内保留逗号,你可以通过用引号替换引号中的逗号来简化这种方法(不处理起始索引,没有最后一个字符特殊情况),然后用逗号分割:

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

    我不建议Bart的正则表达式答案,我发现在这种特殊情况下解析解决方案更好(正如Fabian提出的那样) . 我已经尝试了正则表达式解决方案和自己的解析实现我发现:

    • 解析比使用反向引用正则表达式分裂快得多 - 短字符串快20倍,长字符串快约40倍 .

    • 正则表达式在最后一个逗号后找不到空字符串 . 这不是原始问题,但这是我的要求 .

    我的解决方案和测试如下 .

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

    当然,如果您对丑陋感到不舒服,可以自由更改切换到此片段中的else-ifs . 注意在用分离器切换后没有断裂 . StringBuilder被设计为StringBuffer,以提高速度,线程安全无关紧要 .

  • 40

    试试lookaround,如 (?!\"),(?!\") . 这应匹配未被 " 包围的 , .

  • 7

    我会做这样的事情:

    boolean foundQuote = false;
    
    if(charAtIndex(currentStringIndex) == '"')
    {
       foundQuote = true;
    }
    
    if(foundQuote == true)
    {
       //do nothing
    }
    
    else 
    
    {
      string[] split = currentString.split(',');  
    }
    
  • 2

    你正处于那个令人讨厌的边界区域,其中regexps几乎不会做(正如Bart所指出的那样,逃避引用会让生活变得艰难),然而一个成熟的解析器似乎有点矫枉过正 .

    如果你很快就需要更大的复杂性,我会去寻找一个解析器库 . 例如this one

  • -1

    我很不耐烦,选择不等待答案......作为参考,看起来并不难做到这样的事情(这适用于我的应用程序,我不需要担心转义引号,因为引号中的内容仅限于一些约束形式):

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

    (为读者练习:通过查找反斜杠扩展到处理转义引号 . )

  • 2

    尝试:

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

    输出:

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

    换句话说:只有当逗号为零或前面有偶数引号时才能在逗号上拆分 .

    或者,眼睛有点友善:

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

    其产生与第一个例子相同 .

    编辑

    正如@MikeFHay在评论中所提到的:

    我更喜欢使用Guava的Splitter,因为它有saner默认值(请参阅上面关于String#split()修剪空匹配的讨论,所以我做了:Splitter.on(Pattern.compile(“,(?=(?:[^\ “] * \”[^\ “] * \”)* [^\ “] * $)”))

  • 392

    而不是使用先行和其他疯狂的正则表达式,只需先拔出引号 . 也就是说,对于每个报价分组,将该分组替换为 __IDENTIFIER_1 或其他指标,并将该分组映射到字符串,字符串的映射 .

    在逗号上拆分后,将所有映射的标识符替换为原始字符串值 .

相关问题