Токенизация строки, но игнорирование разделителей в кавычках - PullRequest
23 голосов
/ 29 июля 2010

Я хочу иметь следующую строку

!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, "\"")

но это вернет "Другой" и "AndAnother" как "Другой AndAnother", что не является желаемым эффектом.

Спасибо.

EDIT: Я изменил пример еще раз, на этот раз я считаю, что это лучше всего объясняет ситуацию, хотя он ничем не отличается от второго примера.

Ответы [ 11 ]

52 голосов
/ 30 июля 2010

Гораздо проще использовать 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) + "]");
        }
    }

Приведенные выше отпечатки ( как видно на 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 все еще работает.

Ссылки

Смотри также


Приложение

Обратите внимание, что StringTokenizer - это устаревший класс . Рекомендуется использовать java.util.Scanner или String.split, или, конечно, java.util.regex.Matcher для большей гибкости.

Смежные вопросы

6 голосов
/ 30 июля 2010

Делай это по старинке. Создайте функцию, которая просматривает каждый символ в цикле for. Если символ является пробелом, возьмите все до этого (исключая пробел) и добавьте его как запись в массив. Запишите позицию и сделайте то же самое снова, добавив следующую часть в массив после пробела. Когда встречаются двойные кавычки, пометьте логическое значение с именем «inQuote» как true и игнорируйте пробелы, когда inQuote имеет значение true. Когда вы нажимаете на кавычки, когда inQuote имеет значение true, пометьте его как false и вернитесь к тому, чтобы разбить все на части при обнаружении пробела. Затем вы можете расширить это по мере необходимости для поддержки escape-символов и т. Д.

Может ли это быть сделано с помощью регулярного выражения? Я не знаю, я думаю. Но вся функция написала бы меньше, чем этот ответ.

2 голосов
/ 15 апреля 2018

Apache Помогает на помощь!

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 = ... line это способ создания String в Groovy, который содержит оба одинарные кавычки и двойные кавычки без необходимости экранирования
  4. StringMatcherFactory в apache commons-text 1.3 можно найти здесь : как вы можете видеть, INSTANCE может предоставить вам куча разных StringMatcher с. Вы можете даже свернуть свой собственный: но вам нужно изучить исходный код StringMatcherFactory, чтобы посмотри как это делается.
  5. ДА! Вы можете не только включить «другой тип цитаты», и он правильно интерпретируется как не являющийся границей токена ... но вы даже можете избежать фактической цитаты, которая используется для отключения токенизации , удваивая кавычку внутри защищенного токенизацией бита String! Попробуйте реализовать это с помощью нескольких строк кода ... или, скорее, не надо!

PS Почему лучше использовать Apache Commons, чем любое другое решение? Помимо того, что нет смысла заново изобретать колесо, я могу придумать по крайней мере две причины:

  1. Можно рассчитывать, что инженеры Apache предвосхитят все ошибки и разработают надежный, всесторонне протестированный, надежный код
  2. Это означает, что вы не загромождаете свой прекрасный код с помощью утилитарных методов - у вас просто есть хороший, чистый кусок кода, который в точности соответствует тому, что написано на банке, оставляя вам возможность заняться интересным прочее ...

PPS Ничто не обязывает вас смотреть на код Apache как на таинственные «черные ящики». Исходный код открыт и написан обычно на «доступной» Java. Следовательно, вы можете исследовать, как все делается с вашим сердцем. Это часто весьма поучительно.

позже

Достаточно заинтригованный вопросом АртБа, я взглянул на источник:

в 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.

... Я не могу больше следовать за подсказками. У вас есть выбор: либо ваше «хакерское» решение, где вы систематически предварительно обрабатываете свои строки перед тем, как отправлять их на токенизацию, превращая | \\\ "| s в | \" \ "| s ... (то есть где вы заменяете каждый | \ " | с | " " |) ...
Или ... вы изучаете org.apache.commons.text.StringTokenizer.java, чтобы выяснить, как настроить код. Это маленький файл. Я не думаю, что это будет так сложно. Затем вы компилируете, по сути, создавая форк кода Apache.

Не думаю, что это можно настроить. Но если вы нашли решение для подстройки кода, которое имело смысл, вы могли бы отправить его в Apache, а затем оно могло бы быть принято для следующей итерации кода, и ваше имя фигурировало бы, по крайней мере, в части Apache «запрос возможностей»: может быть формой kleos , благодаря которой вы достигаете бессмертия программирования ...

2 голосов
/ 30 июля 2010

По старинке:

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

Я предполагаю, что вложенные кавычки недопустимы, а также что пустые токены могут быть опущены.

1 голос
/ 02 апреля 2019

Недавно столкнулся с аналогичным вопросом, когда аргументы командной строки должны быть разделены без учета кавычек ссылка .

Один из возможных случаев:

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

Это нужно было разделить на

/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 голосов
/ 18 июня 2018

Другой способ старой школы:

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

Вывод: - [Один, два, «три четыре», пять, «шесть семь восемь», девять]

0 голосов
/ 27 августа 2016

Это старый вопрос, однако это было мое решение как конечного автомата.

Эффективные, предсказуемые и не изворотливые трюки.

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 голосов
/ 30 июля 2010

Я не знаю контекст того, что вы пытаетесь сделать, но похоже, что вы пытаетесь разобрать аргументы командной строки.В общем, это довольно сложно со всеми вытекающими проблемами;если это ваша цель, я бы лично посмотрел на что-то вроде JCommander.

0 голосов
/ 29 июля 2010

попробуйте это:

String str = "One two \"three four\" five \"six seven eight\" nine \"ten\"";
String[] strings = str.split("[ ]?\"[ ]?");
0 голосов
/ 29 июля 2010

Попробуйте это:

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

Это довольно сложно, потому что вам нужно избегать двойных кавычек. Это регулярное выражение должно маркировать строку, используя пробел (\ s) или двойные кавычки.

Вам следует использовать метод split String, поскольку он принимает регулярные выражения, тогда как аргумент конструктора для разделителя в StringTokenizer этого не делает. В конце того, что я предоставил выше, вы можете просто добавить следующее:

String s;
for(String k : strArr) {
     s += k;
}
StringTokenizer strTok = new StringTokenizer(s);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...