拆分嵌套字符串并保留引号

10

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

对于一个纯文本输入字符串,例如:

This is "a string" and this is "a \"nested\" string"

结果必须是以下内容:

[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 Collection Framework或其衍生物。

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

基于评论的问题更新:

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

@Turtle:并不总是这样。它也会分割“嵌套”的字符串。 - user2705585
那不是一个普通的语言。你需要超越普通正则表达式的功能。Look-around将正则表达式扩展到超出常规语言,但由于这听起来像是一项学校作业,目标可能是让你编写词法分析器(lexical analyzer)。 - jpmc26
1
我不这么认为 - 那个问题没有提到嵌套字符串。 - bobasti
我们可以假设字符串总是平衡的吗?比如每个 " 都有它相应的闭合 " - Pshemo
嵌套引号中的引号需要使用 \" 还是普通的 " - Matthew Wright
显示剩余8条评论
3个回答

10

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

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

请查看正则表达式演示

Java演示:

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个概率。而我的基于取消循环的版本可以一次匹配多个文本块,因此更快更可靠。

更新

由于需要输出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));

请查看另一个IDEONE演示


WTF...!你是怎么做到的..!+1 - Shafizadeh
2
@Shafizadeh 我已经添加了解释,现在要把电脑送给我唠叨的妻子 :) - Wiktor Stribiżew
@WiktorStribiżew 有意思。我应该读一些关于正则表达式及其优化的书。 - Pshemo
我一直在实现相同的想法。我用这个演示更新了答案。 - Wiktor Stribiżew
1
谢谢。这与我几分钟前所做的相同。标记为已接受的答案。 - bobasti
显示剩余12条评论

7
另一种适用的正则表达式方法使用负回顾后发现:匹配"单词"(\w+)或者"引号后跟随任何内容直到下一个引号,但不是由反斜杠引导的",并将匹配设为"全局"(不在第一个匹配上返回)。
(\w+|".*?(?<!\\)")

see it here.


1
这是一个不错的模式,+1 - Shafizadeh
但是,如果不使用“List”,如何从令牌正则表达式转换为匹配数组呢?“split” API使用分隔符表达式而不是令牌表达式。 - erickson
1
@erickson:不确定你的意思是什么..? - Scott Weaver
2
这是一个错误的解决方案,如果在“”之前有一个转义的“\”,它将失败。像这样的语法无法使用前瞻来解析。 - Wiktor Stribiżew
谢谢您的回答。它很好地运行,直到出现一个带有“=”字符的字符串。由于某些原因,它跳过了“=”字符。 - bobasti
显示剩余4条评论

2

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

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()]);
    }
}

输出:

[这是,is,"a string",and,这是,"a \"nested\" string"]


import java.util.ArrayList; -> "不使用Java集合框架或其衍生物。" - Pshemo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接