我有一个这样的字符串:

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的映射。

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

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

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

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

试着看看(?!\"),(?!\")。这应该是相匹配的,那是不被包围的。

我会这样做:

boolean foundQuote = false;

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

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

else 

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

正则表达式不能处理转义字符。对于我的应用程序,我需要能够转义引号和空格(分隔符是空格,但代码是相同的)。

以下是我在Kotlin(来自这个特定应用程序的语言)中的解决方案,基于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
}

我认为这不是一个使用正则表达式的地方。与其他观点相反,我不认为解析器是多余的。它大约有20行,并且相当容易测试。