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

Posted

技术标签:

【中文标题】Java:拆分逗号分隔的字符串但忽略引号中的逗号【英文标题】:Java: splitting a comma-separated string but ignoring commas in quotes 【发布时间】:2010-12-17 22:37:19 【问题描述】:

我有一个模糊的字符串:

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

我想用逗号分隔——但我需要忽略引号中的逗号。我怎样才能做到这一点?似乎正则表达式方法失败了;我想我可以在看到报价时手动扫描并输入不同的模式,但是使用预先存在的库会很好。 (edit:我想我的意思是已经是 JDK 的一部分或者已经是 Apache Commons 等常用库的一部分的库。)

上面的字符串应该分成:

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

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

【问题讨论】:

【参考方案1】:

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

这是我在 Kotlin(这个特定应用程序的语言)中的解决方案,基于 F*** 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 行,而且相当容易测试。

【讨论】:

那不是 Java 将kotlin翻译成java非常简单。我是为一个 kotlin 项目写的,并以此为例,所以我想我会分享,我没有看到需要做翻译,特别是因为上面的代码是经过测试的。要我翻译吗? 哦,我还以为是javascript什么的。如果您发布代码,您需要告诉人们它是哪种语言。 :-) 这是一个 12 岁的问题,所以我没有任何偏好,也不会改变我接受的答案。请注意,发现此问题的人可能正在寻找 Java 解决方案。 没问题。老实说,我只是在这里发布它,因为我在写它时发现了这个问题,我想如果其他人也这样做,我会很高兴他们找到它。 另外,我相信 F*** Steeg 的解决方案比公认的答案更好。如果您要更改已接受的答案,我的投票就是那个。此答案基于此,我将对其进行编辑以表扬。【参考方案2】:

虽然我一般都喜欢正则表达式,但对于这种依赖于状态的标记化,我相信一个简单的解析器(在这种情况下,它比那个词听起来更简单)可能是一个更简洁的解决方案,尤其是关于可维护性,例如:

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

如果您不关心保留引号内的逗号,则可以通过将引号中的逗号替换为其他内容来简化此方法(不处理起始索引,不处理 最后一个字符 特殊情况)然后用逗号分割:

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

【讨论】:

在解析字符串后,应从已解析的标记中删除引号。 google找到的,算法不错,简单易适应,同意。有状态的东西应该通过解析器完成,正则表达式是一团糟。 请记住,如果逗号是最后一个字符,它将在最后一项的字符串值中。【参考方案3】:

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

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

【讨论】:

【参考方案4】:

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

该模式由两个替代方案组成,一个带引号的字符串("[^"]*"".*?")或直到下一个逗号的所有内容([^,]+)。为了支持空单元格,我们必须允许未引用的项目为空并使用下一个逗号(如果有),并使用 \\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,您可以使用 this answer 的 results() 实现,就像 Java 9 解决方案一样。

对于带有嵌入字符串的混合内容,如问题中,您可以简单地使用

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

但是,字符串保留在引用的形式中。

【讨论】:

【参考方案5】:

试试:

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 在 cmets 中提到的:

我更喜欢使用Guava's Splitter,因为它具有更合理的默认值(请参阅上面关于被String#split() 修剪的空匹配项的讨论,所以我这样做了:

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

【讨论】:

根据 RFC 4180:Sec 2.6:“包含换行符 (CRLF)、双引号和逗号的字段应该用双引号括起来。” Sec 2.7:“如果使用双引号括住字段,则出现在字段中的双引号必须通过在其前面加上另一个双引号来进行转义”因此,如果String line = "equals: =,\"quote: \"\"\",\"comma: ,\"",您需要做的就是去掉多余的双引号字符。 @Bart:我的意思是你的解决方案仍然有效,即使嵌入了引号 @Alex,是的,逗号 is 匹配,但空匹配不在结果中。将-1 添加到拆分方法参数:line.split(regex, -1)。见:docs.oracle.com/javase/6/docs/api/java/lang/… 效果很好!我更喜欢使用 Guava 的 Splitter,因为它有更合理的默认值(参见上面关于 String#split 修剪空匹配的讨论),所以我做了Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")) 警告!!!!这个正则表达式很慢!!! 它具有 O(N^2) 的行为,因为每个逗号的前瞻看起来一直到字符串的末尾。使用这个正则表达式会导致大型 Spark 作业的速度降低 4 倍(例如 45 分钟 -> 3 小时)。更快的替代方案是 findAllIn("(?s)(?:\".*?\"|[^\",]*)*") 结合后处理步骤以跳过每个非空字段之后的第一个(始终为空)字段。【参考方案6】:

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

    解析比使用带有反向引用的正则表达式拆分要快得多 - 短字符串快约 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);

当然,如果您对它的丑陋感到不舒服,您可以在此 sn-p 中随意将 switch 更改为 else-ifs。请注意,使用分隔符切换后缺少中断。在设计上选择 StringBuilder 来代替 StringBuffer 是为了提高速度,这与线程安全无关。

【讨论】:

关于时间分割与解析的有趣点。然而,陈述#2 是不准确的。如果在 Bart 的回答中将 -1 添加到 split 方法中,您将捕获空字符串(包括最后一个逗号后的空字符串):line.split(regex, -1) +1 因为它是我正在寻找解决方案的问题的更好解决方案:解析复杂的 HTTP POST 正文参数字符串【参考方案7】:

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

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

http://opencsv.sourceforge.net/

CSV API for Java

Can you recommend a Java library for reading (and possibly writing) CSV files?

Java lib or app to convert CSV to XML file?

【讨论】:

识别出 OP 正在解析 CSV 文件的良好调用。外部库非常适合此任务。 但是字符串是CSV字符串;您应该可以直接在该字符串上使用 CSV api。 是的,但是这个任务很简单,而且是一个更大的应用程序的一小部分,我不想引入另一个外部库。 不一定...我的技能通常是足够的,但他们会从磨练中受益。【参考方案8】:

我会这样做:

boolean foundQuote = false;

if(charAtIndex(currentStringIndex) == '"')

   foundQuote = true;


if(foundQuote == true)

   //do nothing


else 


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

【讨论】:

【参考方案9】:

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

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;

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

【讨论】:

【参考方案10】:

您处于正则表达式几乎无法使用的烦人边界区域(正如 Bart 所指出的那样,转义引号会使生活变得艰难),但成熟的解析器似乎有点矫枉过正。

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

【讨论】:

【参考方案11】:

试试lookaround,比如(?!\"),(?!\")。这应该与未被" 包围的, 匹配。

【讨论】:

可以肯定的是这样的列表会中断:"foo",bar,"baz" 我认为您的意思是(?&lt;!"),(?!"),但它仍然无法正常工作。给定字符串one,two,"three,four",它正确匹配one,two中的逗号,但它也匹配"three,four"中的逗号,并且无法匹配two,"three中的一个。 它看起来非常适合我,恕我直言,我认为这是一个更好的答案,因为它更短且更容易理解【参考方案12】:

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

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

【讨论】:

以及如何在没有疯狂正则表达式的情况下找到报价分组? 对于每个字符,如果字符是引号,则查找下一个引号并替换为分组。如果没有下一个报价,完成。

以上是关于Java:拆分逗号分隔的字符串但忽略引号中的逗号的主要内容,如果未能解决你的问题,请参考以下文章

在逗号上拆分字符串并忽略双引号中的逗号[重复]

使用逗号拆分字符串,但忽略双引号内的逗号 - javascript

用逗号分割字符串,但忽略括号或引号中的逗号

拆分由逗号分隔的 JSON blob 列表(忽略 JSON blob 中的逗号)[重复]

如何在忽略引号内的任何逗号的情况下用逗号分隔? [复制]

Java Regex - 拆分逗号分隔列表但排除方括号内的逗号