我有一个这样的字符串:

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

我想用逗号分隔,但我需要忽略引号中的逗号。我该怎么做呢?regexp方法似乎失败了;我想我可以手动扫描,并在看到报价时进入不同的模式,但如果使用已有的库就更好了。(编辑:我想我指的是已经是JDK的一部分或者已经是Apache Commons等常用库的一部分的库。)

上面的字符串应该分成:

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

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


当前回答

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

解析比使用带反向引用的regex进行拆分要快得多——短字符串快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-if。注意在使用分离器开关后没有中断。选择StringBuilder代替StringBuffer是为了提高速度,而线程安全是无关紧要的。

其他回答

与其使用超前和其他疯狂的正则表达式,不如先取出引号。也就是说,对于每个引号分组,将该分组替换为__IDENTIFIER_1或其他指示符,并将该分组映射为string,string的映射。

在使用逗号分隔之后,将所有映射的标识符替换为原始字符串值。

我没有耐心,没有选择等待答案。作为参考,它看起来并不难做这样的事情(这适用于我的应用程序,我不需要担心转义引号,因为引号中的东西仅限于几个约束形式):

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

(读者练习:也可以通过寻找反斜杠来处理转义引号。)

最简单的方法是不匹配分隔符,即逗号,使用复杂的附加逻辑来匹配实际需要的内容(可能是带引号的字符串的数据),只是为了排除错误的分隔符,而是首先匹配所需的数据。

该模式由两个选项组成,一个带引号的字符串("[^"]*"或".*?")或下一个逗号前的所有内容([^,]+)。为了支持空单元格,我们必须允许未加引号的项为空,并使用下一个逗号(如果有的话),并使用\\G锚点:

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

该模式还包含两个捕获组,用于获取带引号的字符串的内容或普通内容。

然后,在Java 9中,我们可以得到一个数组为

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

而旧的Java版本需要像这样的循环

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

将项添加到List或数组中是留给阅读器的附加工作。

对于Java 8,您可以使用这个答案的results()实现,就像Java 9解决方案一样。

对于包含嵌入字符串的混合内容,就像问题中那样,您可以简单地使用

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

但是,字符串以引号形式保存。

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

https://github.com/pupi1985/JavaCSV-Reloaded (前一个库的分支,它将允许生成的输出在不运行Windows时具有Windows行结束符\r\n)

http://opencsv.sourceforge.net/

适用于Java的CSV API

你能推荐一个用于读取(可能是写入)CSV文件的Java库吗?

Java库或应用程序转换CSV到XML文件?

您正处于一个恼人的边界区域,regexp几乎不起作用(正如Bart所指出的,转义引号会使生活变得困难),然而一个成熟的解析器似乎有点过头了。

如果您可能需要更大的复杂性,我会去寻找一个解析器库。比如这个