对字符串进行标记但忽略引号内的分隔符

问题描述 投票:23回答:11

我希望有以下字符串

!cmd 45 90 "An argument" Another AndAnother "Another one in quotes"

成为以下阵列

{ "!cmd", "45", "90", "An argument", "Another", "AndAnother", "Another one in quotes" }

我试过了

new StringTokenizer(cmd, "\"")

但这将返回“另一个”和“和另一个”另一个和另一个“这不是预期的效果。

谢谢。

编辑:我再次改变了这个例子,这次我认为它解释了最好的情况,尽管它与第二个例子没有什么不同。

java
11个回答
52
投票

在这种情况下,使用java.util.regex.Matcher并使用find()而不是任何类型的split要容易得多。

也就是说,不是为标记之间的分隔符定义模式,而是定义标记本身的模式。

这是一个例子:

    String text = "1 2 \"333 4\" 55 6    \"77\" 8 999";
    // 1 2 "333 4" 55 6    "77" 8 999

    String regex = "\"([^\"]*)\"|(\\S+)";

    Matcher m = Pattern.compile(regex).matcher(text);
    while (m.find()) {
        if (m.group(1) != null) {
            System.out.println("Quoted [" + m.group(1) + "]");
        } else {
            System.out.println("Plain [" + m.group(2) + "]");
        }
    }

以上版画(as seen on ideone.com):

Plain [1]
Plain [2]
Quoted [333 4]
Plain [55]
Plain [6]
Quoted [77]
Plain [8]
Plain [999]

模式基本上是:

"([^"]*)"|(\S+)
 \_____/  \___/
    1       2

有2个替代品:

  • 第一个备用匹配开头双引号,除了双引号(在第1组中捕获)之外的任何序列,然后是结束双引号
  • 第二个备用匹配在第2组中捕获的任何非空白字符序列
  • 替代品的顺序在这种模式中很重要

请注意,这不会处理引用段中的转义双引号。如果你需要这样做,那么模式会变得更复杂,但Matcher解决方案仍然有效。

References

See also


Appendix

请注意,StringTokenizer是一个遗留类。建议使用java.util.ScannerString.split,或者当然使用java.util.regex.Matcher以获得最大的灵活性。

Related questions


-1
投票

我不知道你尝试做什么的上下文,但看起来你试图解析命令行参数。总的来说,这对于所有逃避问题都非常棘手;如果这是你的目标,我会亲自看看像JCommander这样的东西。


-1
投票

试试这个:

String str = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
String strArr[] = str.split("\"|\s");

这有点棘手,因为你需要逃避双引号。这个正则表达式应该使用空格(\ s)或双引号来标记字符串。

你应该使用String的split方法,因为它接受正则表达式,而StringTokenizer中的分隔符的构造函数参数则不然。在上面提供的内容结尾处,您可以添加以下内容:

String s;
for(String k : strArr) {
     s += k;
}
StringTokenizer strTok = new StringTokenizer(s);

6
投票

这是老式的方式。创建一个查看for循环中每个字符的函数。如果角色是空格,请将所有内容(不包括空格)取出并添加为数组的条目。注意位置,并再次执行相同的操作,将空格后的下一部分添加到数组中。遇到双引号时,将名为'inQuote'的布尔值标记为true,并在inQuote为true时忽略空格。当inQuote为true时命中引号时,将其标记为false并在遇到空格时返回到破坏状态。然后,您可以根据需要扩展它以支持转义字符等。

这可以用正则表达式完成吗?我想,我不知道。但是整个功能的写入要比这个回复少。


2
投票

以一种老式的方式:

public static String[] split(String str) {
    str += " "; // To detect last token when not quoted...
    ArrayList<String> strings = new ArrayList<String>();
    boolean inQuote = false;
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (c == '"' || c == ' ' && !inQuote) {
            if (c == '"')
                inQuote = !inQuote;
            if (!inQuote && sb.length() > 0) {
                strings.add(sb.toString());
                sb.delete(0, sb.length());
            }
        } else
            sb.append(c);
    }
    return strings.toArray(new String[strings.size()]);
}

我假设嵌套引号是非法的,并且也可以省略空标记。


2
投票

Apache Commons拯救!

import org.apache.commons.text.StringTokenizer
import org.apache.commons.text.matcher.StringMatcher
import org.apache.commons.text.matcher.StringMatcherFactory
@Grab(group='org.apache.commons', module='commons-text', version='1.3')

def str = /is this   'completely "impossible"' or """slightly"" impossible" to parse?/

StringTokenizer st = new StringTokenizer( str )
StringMatcher sm = StringMatcherFactory.INSTANCE.quoteMatcher()
st.setQuoteMatcher( sm )

println st.tokenList

输出:

[是,这,完全“不可能”,或者,“稍微”不可能,要解析?]

几点说明:

  1. 这是用Groovy编写的......它实际上是一个Groovy脚本。 @Grab行给出了你需要的依赖行的线索(例如在build.gradle中)......或者只是在你的类路径中包含.jar
  2. StringTokenizer这里不是java.util.StringTokenizer ...因为import线显示它是org.apache.commons.text.StringTokenizer
  3. def str = ...系列是一种在Groovy中生成String的方法,它包含单引号和双引号,无需进入转义状态
  4. StringMatcherFactory在apache commons-text 1.3中可以找到here:正如你所看到的,INSTANCE可以为你提供一堆不同的StringMatchers。您甚至可以自己动手:但是您需要检查StringMatcherFactory源代码以了解它是如何完成的。
  5. 是!您不仅可以包含“其他类型的引用”,而且它被正确解释为不是令牌边界......但您甚至可以通过将令牌化中的引用加倍来逃避用于关闭令牌化的实际引用字符串的保护位!尝试使用几行代码来实现它......或者不是!

PS为什么使用Apache Commons比任何其他解决方案更好?除了重新发明轮子这一事实之外,我至少可以想到两个原因:

  1. Apache工程师可以指望所有的陷阱,并开发出强大,全面测试,可靠的代码
  2. 这意味着你不会使用stoopid实用程序方法混乱你漂亮的代码 - 你只需要一个很好的,干净的代码,它完全按照它在锡上所说的那样,让你继续使用,这些有趣的东西。 。

PPS没有什么可以让你把Apache代码视为神秘的“黑盒子”。源是开放的,通常是完全“可访问”的Java。因此,您可以自由地检查内容是如何完成的。这样做通常很有启发性。

后来

对ArtB的问题充满了兴趣,我看了一眼来源:

在StringMatcherFactory.java中我们看到:

private static final AbstractStringMatcher.CharSetMatcher QUOTE_MATCHER = new AbstractStringMatcher.CharSetMatcher(
            "'\"".toCharArray());

...相当沉闷......

所以这导致人们看看StringTokenizer.java:

public StringTokenizer setQuoteMatcher(final StringMatcher quote) {
        if (quote != null) {
            this.quoteMatcher = quote;
        }
        return this;
}

好的...然后,在同一个java文件中:

private int readWithQuotes(final char[] srcChars ...

其中包含评论:

// If we've found a quote character, see if it's followed by a second quote. If so, then we need to actually put the quote character into the token rather than end the token.

......我不能再费心去追踪这些线索了。您可以选择:要么是“hackish”解决方案,要么在提交字符串之前系统地预处理字符串,将| \\\“| s转换为| \”\“| s ...(即替换的位置)每个| \“|带|”“|)...... 或者......你检查org.apache.commons.text.StringTokenizer.java以弄清楚如何调整代码。这是一个小文件。我认为这不会那么困难。然后你编译,基本上是一个Apache代码的分支。

我不认为它可以配置。但是如果你找到一个有意义的代码调整解决方案,你可以将它提交给Apache,然后它可能会被接受用于代码的下一次迭代,并且你的名字至少会出现在Apache的“功能请求”部分:这个可能是kleos的一种形式,通过它你可以实现编程永生......


1
投票

最近面临一个类似的问题,其中必须拆分命令行参数而忽略引号link

一个可能的情况:

“/opt/jboss-eap/bin/jboss-cli.sh --connect --controller = localhost:9990 -c command = \”deploy /app/jboss-eap-7.1/standalone/updates/sample.war - 力\””

这必须分成两部分

/opt/jboss-eap/bin/jboss-cli.sh
--connect
--controller=localhost:9990
-c
command="deploy /app/jboss-eap-7.1/standalone/updates/sample.war --force"

只是添加到@polygenelubricants的答案,在引号匹配器之前和之后有任何非空格字符可以解决。

"\\S*\"([^\"]*)\"\\S*|(\\S+)"

例:

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Tokenizer {

    public static void main(String[] args){

        String a = "/opt/jboss-eap/bin/jboss-cli.sh --connect --controller=localhost:9990 -c command=\"deploy " +
                "/app/jboss-eap-7.1/standalone/updates/sample.war --force\"";
        String b = "Hello \"Stack Overflow\"";
        String c = "cmd=\"abcd efgh ijkl mnop\" \"apple\" banana mango";
        String d = "abcd ef=\"ghij klmn\"op qrst";
        String e = "1 2 \"333 4\" 55 6    \"77\" 8 999";

        List<String> matchList = new ArrayList<String>();
        Pattern regex = Pattern.compile("\\S*\"([^\"]*)\"\\S*|(\\S+)");
        Matcher regexMatcher = regex.matcher(a);
        while (regexMatcher.find()) {
            matchList.add(regexMatcher.group());
        }
        System.out.println("matchList="+matchList);
    }
}

输出:

matchList = [/ opt / jboss -eap / bin / jboss-cli.sh,--connect,-controller = localhost:9990,-c,command =“deploy /app/jboss-eap-7.1/standalone/updates/ sample.war --force“]


0
投票

你在这里的例子只需要用双引号字符分开。


0
投票

这是一个老问题,但这是我作为有限状态机的解决方案。

高效,可预测,没有花哨的技巧。

100%的测试覆盖率。

拖放到您的代码中。

/**
 * Splits a command on whitespaces. Preserves whitespace in quotes. Trims excess whitespace between chunks. Supports quote
 * escape within quotes. Failed escape will preserve escape char.
 *
 * @return List of split commands
 */
static List<String> splitCommand(String inputString) {
    List<String> matchList = new LinkedList<>();
    LinkedList<Character> charList = inputString.chars()
            .mapToObj(i -> (char) i)
            .collect(Collectors.toCollection(LinkedList::new));

    // Finite-State Automaton for parsing.

    CommandSplitterState state = CommandSplitterState.BeginningChunk;
    LinkedList<Character> chunkBuffer = new LinkedList<>();

    for (Character currentChar : charList) {
        switch (state) {
            case BeginningChunk:
                switch (currentChar) {
                    case '"':
                        state = CommandSplitterState.ParsingQuote;
                        break;
                    case ' ':
                        break;
                    default:
                        state = CommandSplitterState.ParsingWord;
                        chunkBuffer.add(currentChar);
                }
                break;
            case ParsingWord:
                switch (currentChar) {
                    case ' ':
                        state = CommandSplitterState.BeginningChunk;
                        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
                        matchList.add(newWord);
                        chunkBuffer = new LinkedList<>();
                        break;
                    default:
                        chunkBuffer.add(currentChar);
                }
                break;
            case ParsingQuote:
                switch (currentChar) {
                    case '"':
                        state = CommandSplitterState.BeginningChunk;
                        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
                        matchList.add(newWord);
                        chunkBuffer = new LinkedList<>();
                        break;
                    case '\\':
                        state = CommandSplitterState.EscapeChar;
                        break;
                    default:
                        chunkBuffer.add(currentChar);
                }
                break;
            case EscapeChar:
                switch (currentChar) {
                    case '"': // Intentional fall through
                    case '\\':
                        state = CommandSplitterState.ParsingQuote;
                        chunkBuffer.add(currentChar);
                        break;
                    default:
                        state = CommandSplitterState.ParsingQuote;
                        chunkBuffer.add('\\');
                        chunkBuffer.add(currentChar);
                }
        }
    }

    if (state != CommandSplitterState.BeginningChunk) {
        String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining());
        matchList.add(newWord);
    }
    return matchList;
}

private enum CommandSplitterState {
    BeginningChunk, ParsingWord, ParsingQuote, EscapeChar
}

0
投票

另一种老派的方式是:

public static void main(String[] args) {

    String text = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
    String[] splits = text.split(" ");
    List<String> list = new ArrayList<>();
    String token = null;
    for(String s : splits) {

        if(s.startsWith("\"") ) {
            token = "" + s; 
        } else if (s.endsWith("\"")) {
            token = token + " "+ s;
            list.add(token);
            token = null;
        } else {
            if (token != null) {
                token = token + " " + s;
            } else {
                list.add(s);
            }
        }
    }
    System.out.println(list);
}

输出: - [一,二,“三四”,五,“六七八”,九]


-1
投票

试试这个:

String str = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
String[] strings = str.split("[ ]?\"[ ]?");
© www.soinside.com 2019 - 2024. All rights reserved.