Вопрос-ответ

Java: splitting a comma-separated string but ignoring commas in quotes

Java: разделение строки, разделенной запятыми, но игнорирование запятых в кавычках

У меня есть строка, смутно похожая на эту:

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

я хочу разделить ее запятыми - но мне нужно игнорировать запятые в кавычках. Как я могу это сделать? Похоже, что подход с регулярными выражениями дает сбой; Я полагаю, я могу вручную отсканировать и перейти в другой режим, когда увижу цитату, но было бы неплохо использовать уже существующие библиотеки. (редактировать: полагаю, я имел в виду библиотеки, которые уже являются частью JDK или уже являются частью часто используемых библиотек, таких как Apache Commons.)

приведенная выше строка должна быть разделена на:

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

примечание: это НЕ CSV-файл, это отдельная строка, содержащаяся в файле с большей общей структурой

Переведено автоматически
Ответ 1

Попробуйте:

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 в комментариях:


Я предпочитаю использовать разделитель Guava, поскольку он имеет более разумные значения по умолчанию (см. Обсуждение выше о том, что пустые совпадения обрезаются с помощью String#split(), поэтому я так и сделал:


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

Ответ 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(","));
Ответ 3
Ответ 4

Я бы не советовал отвечать регулярным выражением от Bart, я нахожу решение для синтаксического анализа лучшим в данном конкретном случае (как предложил Fabian). Я попробовал решение для регулярных выражений и собственную реализацию синтаксического анализа, я обнаружил, что:


  1. Синтаксический анализ выполняется намного быстрее, чем разделение с помощью регулярного выражения с обратными ссылками - ~ в 20 раз быстрее для коротких строк, ~ в 40 раз быстрее для длинных строк.

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

Мое решение и тест ниже.

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

Конечно, вы можете изменить switch на else-ifs в этом фрагменте, если вам неудобно из-за его уродства. Обратите внимание на отсутствие разрыва после switch с разделителем. StringBuilder был выбран вместо StringBuffer специально для увеличения скорости, где потокобезопасность не имеет значения.

java regex string