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


249

我隐约有这样的字符串:

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

我想按逗号分割-但我需要忽略引号中的逗号。我怎样才能做到这一点?似乎正则表达式方法失败了。我想我可以在看到报价时手动扫描并进入其他模式,但是使用预先存在的库会很好。(编辑:我想我的意思是那些已经属于JDK或已经属于诸如Apache Commons之类的常用库的库。)

上面的字符串应分为:

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

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

Answers:


435

尝试:

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,因为它的默认值更合理(请参见上面有关用修饰的空匹配的讨论String#split(),所以我这样做了:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))

根据RFC 4180:第2.6节:“包含换行符(CRLF),双引号和逗号的字段应用双引号引起来。” 秒2.7:“如果双引号用于封装领域,那么场内出现一个双引号必须由另一个双引号之前它被转义”所以,如果String line = "equals: =,\"quote: \"\"\",\"comma: ,\"",你需要做的是剥除多余的双引号字符。
保罗·汉伯里

@Bart:我的观点是,即使使用嵌入式引号,您的解决方案仍然可以使用
Paul Hanbury

6
@Alex,是的,逗号匹配,但结果中没有空匹配。添加-1到split方法param中line.split(regex, -1)。请参阅:docs.oracle.com/javase/6/docs/api/java/lang/…–
Bart

2
很棒!我更喜欢使用Guava的Splitter,因为它的默认值更合理(请参见上面有关用String#split修剪空匹配项的讨论),所以我做了Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"))
MikeFHay

2
警告!!!!这个正则表达式很慢!!!它具有O(N ^ 2)行为,因为每个逗号的前瞻一直一直到字符串的结尾。使用此正则表达式会导致大型Spark作业的速度降低4倍(例如45分钟-> 3小时)。更快的替代方法类似于findAllIn("(?s)(?:\".*?\"|[^\",]*)*")结合后处理步骤,以跳过每个非空字段之后的第一个(始终为空)字段。
Urban Vagabond

46

尽管我确实喜欢一般的正则表达式,但是对于这种与状态相关的标记化,我相信一个简单的解析器(在这种情况下,比那个单词听起来简单得多)可能是一种更干净的解决方案,尤其是在可维护性方面,例如:

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

解析字符串后,应从已解析的标记中删除引号。
Sudhir N

通过谷歌发现,不错的算法兄弟,简单易用,同意。有状态的东西应该通过解析器来完成,正则表达式是一团糟。
鲁道夫·施密特

2
请记住,如果逗号是最后一个字符,它将在最后一项的String值中。
加百利·盖茨

21

3
很好的呼叫,确认OP正在解析CSV文件。外部库非常适合此任务。
Stefan Kendall

1
但是该字符串是CSV字符串;您应该可以直接在该字符串上使用CSV API。
Michael Brewer-Davis

是的,但是此任务非常简单,并且在大型应用程序中只占很小的一部分,因此我不想拉入另一个外部库。
詹森·S

7
不一定……我的技能通常是足够的,但是他们会从磨练中受益。
杰森S

9

我不建议Bart提供正则表达式的答案,我发现在这种特殊情况下解析解决方案更好(如Fabian所建议)。我试过正则表达式解决方案和自己的解析实现,发现:

  1. 与使用带有反向引用的正则表达式进行拆分相比,解析要快得多-短字符串快20倍,长字符串快40倍。
  2. 正则表达式在最后一个逗号后找不到空字符串。但这不是最初的问题,这是我的要求。

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

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而不是StringBuffer,而线程安全无关紧要。


2
关于时间拆分与解析的有趣观点。但是,语句2是不准确的。如果-1在Bart的答案中向split方法添加a ,则将捕获空字符串(包括最后一个逗号之后的空字符串):line.split(regex, -1)
彼得

+1,因为它是我正在寻找解决方案的一个更好的解决方案:解析复杂的HTTP POST主体参数字符串
varontron

2

尝试像这样的环顾四周(?!\"),(?!\")。这应该与,不被包围的匹配"


可以肯定的是,这样的列表将被破坏:“ foo”,bar,“ baz”
Angelo Genovese

1
我认为您的意思是(?<!"),(?!"),但仍然无法正常工作。给定字符串one,two,"three,four",它可以正确匹配逗号内的字符,one,two但也可以匹配逗号内的字符,"three,four"而不能匹配一个逗号two,"three
艾伦·摩尔

它对我来说完美地工作了,恕我直言,我认为这是一个更好的答案,因为它更短且更容易理解
Ordiel

2

您正处在一个烦人的边界区域,正则表达式几乎无法使用(正如Bart所指出的那样,转义引号会使生活变得艰难),但是功能强大的解析器似乎有点过头了。

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


2

我很急躁,选择不等待答案...做这样的事情看起来并不难(这对我的应用程序有效,我不必担心转义的引号,因为引号中的内容仅限于一些约束形式):

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

(针对读者的练习:扩展到通过查找反斜杠来处理转义的引号。)


1

最简单的方法是不使用复杂的附加逻辑来匹配定界符(即逗号),以匹配实际意图(可能用引号引起来的数据),而只是排除错误的定界符,而是首先匹配预期的数据。

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

但随后,字符串将保持其引用形式。


0

不要使用前瞻和其他疯狂的正则表达式,而要先引号。也就是说,对于每个报价分组,将其替换为__IDENTIFIER_1或其他某种指示符,然后将该分组映射到string,string映射。

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


以及如何找到没有疯狂的正则表达式的报价分组?
Kai Huppmann

对于每个字符,如果字符是引号,请查找下一个引号并替换为分组。如果没有下一个报价,请完成。
Stefan Kendall

0

使用String.split()的单行代码怎么办?

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

-1

我会做这样的事情:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.