【问题标题】:Splitting a nested string keeping quotation marks拆分嵌套字符串保留引号
【发布时间】:2016-03-30 04:28:15
【问题描述】:

我正在开发一个需要嵌套字符串的 Java 项目。

对于纯文本格式的输入字符串,如下所示:

这是“一个字符串”,这是“一个\”嵌套\“字符串”

结果必须如下:

[0] This
[1] is
[2] "a string"
[3] and
[4] this
[5] is
[6] "a \"nested\" string"

注意,我希望保留 \" 序列。
我有以下方法:

public static String[] splitKeepingQuotationMarks(String s);

我需要根据给定的规则从给定的s 参数中创建一个字符串数组,而不使用Java 集合框架 或其衍生物。

我不确定如何解决这个问题。
可以制作一个正则表达式来解决这个问题吗?

根据来自 cmets 的问题更新:

  • 每个未转义的" 都有其结束的未转义的"(它们是平衡的)
  • 每个转义字符 \ 也必须转义,如果我们要创建表示它的文字(要创建表示 \ 的文本,我们需要将其写为 \\)。

【问题讨论】:

  • @Turtle:并非总是如此。它也会拆分nested 字符串。
  • 即使在空格上分割?
  • 这不是常规语言。您需要超越普通正则表达式的功能。 Look-arounds 将正则表达式扩展到常规语言之外,但由于这听起来像是一项学校作业,因此目标可能是让您编写一个词法分析器(词法分析器)。
  • 这正是我正在做的 - 编写 Lexer。
  • 我不这么认为 - 这个问题没有提到嵌套字符串。

标签: java regex string


【解决方案1】:

您可以使用以下正则表达式:

"[^"\\]*(?:\\.[^"\\]*)*"|\S+

见regex demo

Java demo:

String str = "This is \"a string\" and this is \"a \\\"nested\\\" string\""; 
Pattern ptrn = Pattern.compile("\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|\\S+");
Matcher matcher = ptrn.matcher(str);
while (matcher.find()) {
    System.out.println(matcher.group(0));
}

解释:

  • "[^"\\]*(?:\\.[^"\\]*)*" - 双引号后跟除 " 和 \ ([^"\\]) 之外的任何 0+ 个字符,后跟任何转义序列 (\\.) 的 0+ 个序列,后跟任何 0+ 个字符除了 " 和 \
  • | - 或者...
  • \S+ - 1 个或多个非空白字符

注意

@Pshemo's suggestion - "\"(?:\\\\.|[^\"])*\"|\\S+"(或"\"(?:\\\\.|[^\"\\\\])*\"|\\S+" 会更正确) - 是相同的表达式,但效率要低得多,因为它使用的是用* 量化的交替组。这个结构涉及更多的回溯,因为正则表达式引擎必须测试每个位置,并且每个位置有 2 个概率。我的基于 unroll-the-loop 的版本将一次匹配文本块,因此更快更可靠。

更新

由于需要String[] 类型作为输出,您需要分两步完成:计算匹配项,创建数组,然后再次重新运行匹配器:

int cnt = 0;
String str = "This is \"a string\" and this is \"a \\\"nested\\\" string\""; 
Pattern ptrn = Pattern.compile("\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|\\S+");
Matcher matcher = ptrn.matcher(str);
while (matcher.find()) {
    cnt++;
}
System.out.println(cnt);
String[] result = new String[cnt];
matcher.reset();
int idx = 0;
while (matcher.find()) {
    result[idx] = matcher.group(0);
    idx++;
}
System.out.println(Arrays.toString(result));

见another IDEONE demo

【讨论】:

  • WTF ...!你是怎么做到的 ..! +1
  • @Shafizadeh 我添加了解释,现在把补偿交给我唠叨的妻子:)
  • Pattern.compile("\"(?:\\\\.|[^\"])*\"|\\S+"); 应该也可以工作。
  • @Pshemo:我认为基于 unroll-the-loop 的正则表达式比未展开的正则表达式更有效。只需在 regex101.com 上比较它们,您就会感觉到不同。您甚至可以使用 Java 进行测试。我确信我的版本比您的带有交替组的版本更不容易发生堆栈溢出错误。
  • 谢谢。这和我刚才做的一样。标记为已接受的答案。
【解决方案2】:

另一种有效的正则表达式方法使用否定的lookbehind:“words”(\w+)OR“引号后跟任何不带反斜杠的下一个引号”,并将你的匹配设置为“全局”(不要在第一次匹配时返回)

(\w+|".*?(?<!\\)")

see it here.

【讨论】:

  • 这是一个不错的模式,+1
  • 但是如何在不使用List 的情况下从令牌正则表达式转到匹配数组? split API 使用分隔符表达式,而不是标记表达式。
  • @erickson:不确定你的意思..?
  • 这是一个错误的解决方案,如果在 " 之前有一个转义的 \,则会失败。 不能用像这样的前瞻来解析这样的语法。
  • OP 说,“我需要用给定的 s 参数创建一个字符串数组”你如何从正则表达式到数组?
【解决方案3】:

不使用正则表达式的替代方法:

import java.util.ArrayList;
import java.util.Arrays;

public class SplitKeepingQuotationMarks {
    public static void main(String[] args) {
        String pattern = "This is \"a string\" and this is \"a \\\"nested\\\" string\"";
        System.out.println(Arrays.toString(splitKeepingQuotationMarks(pattern)));
    }

    public static String[] splitKeepingQuotationMarks(String s) {
        ArrayList<String> results = new ArrayList<>();
        StringBuilder last = new StringBuilder();
        boolean inString = false;
        boolean wasBackSlash = false;

        for (char c : s.toCharArray()) {
            if (Character.isSpaceChar(c) && !inString) {
                if (last.length() > 0) {
                    results.add(last.toString());
                    last.setLength(0); // Clears the s.b.
                }
            } else if (c == '"') {
                last.append(c);
                if (!wasBackSlash)
                    inString = !inString;
            } else if (c == '\\') {
                wasBackSlash = true;
                last.append(c);
            } else
                last.append(c); 
        }

        results.add(last.toString());
        return results.toArray(new String[results.size()]);
    }
}

输出:

[this, is, "a string", and, this, is, "a \"nested\" string"]

【讨论】:

  • import java.util.ArrayList; -> “不使用 Java 集合框架或其衍生产品。”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-18
  • 2020-09-13
  • 2023-01-13
  • 2019-11-09
  • 1970-01-01
相关资源
最近更新 更多