Перейти к основному содержимому

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

· 6 мин. чтения

1. Обзор

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

В этом руководстве мы рассмотрим различные подходы к игнорированию запятых внутри кавычек при разделении строки , разделенной запятыми .

2. Постановка задачи

Предположим, нам нужно разделить следующий ввод, разделенный запятыми:

String input = "foreach,tutorial,splitting,text,\"ignoring this comma,\"";

После разделения этого ввода и печати результата мы ожидаем следующий вывод:

foreach
tutorial
splitting
text
"ignoring this comma,"

Другими словами, мы не можем рассматривать все запятые как разделители. Мы должны игнорировать запятые, которые встречаются внутри подстрок в кавычках.

3. Реализация простого парсера

Создадим простой алгоритм парсинга:

List<String> tokens = new ArrayList<String>();
int startPosition = 0;
boolean isInQuotes = false;
for (int currentPosition = 0; currentPosition < input.length(); currentPosition++) {
if (input.charAt(currentPosition) == '\"') {
isInQuotes = !isInQuotes;
}
else if (input.charAt(currentPosition) == ',' && !isInQuotes) {
tokens.add(input.substring(startPosition, currentPosition));
startPosition = currentPosition + 1;
}
}

String lastToken = input.substring(startPosition);
if (lastToken.equals(",")) {
tokens.add("");
} else {
tokens.add(lastToken);
}

Здесь мы начинаем с определения списка с именем tokens , который отвечает за хранение всех значений, разделенных запятыми.

Затем мы перебираем символы во входной строке .

В каждой итерации цикла нам нужно проверять, является ли текущий символ двойной кавычкой . Когда обнаружена двойная кавычка, мы используем флаг isInQuotes , чтобы указать, что все последующие запятые после двойных кавычек следует игнорировать. Флаг isInQuotes будет установлен в false, когда мы обнаружим двойные кавычки.

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

Тогда новая startPosition будет позицией после запятой.

Наконец, после цикла у нас все еще будет последний токен, который идет от startPosition до последней позиции ввода. Поэтому для его получения мы используем метод substring() . Если этот последний токен — просто запятая, это означает, что последний токен должен быть пустой строкой. В противном случае мы добавляем последний токен в список токенов .

Теперь давайте протестируем код парсинга:

String input = "foreach,tutorial,splitting,text,\"ignoring this comma,\"";
var matcher = contains("foreach", "tutorial", "splitting", "text", "\"ignoring this comma,\"");
assertThat(splitWithParser(input), matcher);

Здесь мы реализовали наш код синтаксического анализа в статическом методе с именем splitWithParser . Затем в нашем тесте мы определяем простой тестовый ввод , содержащий запятую, заключенную в двойные кавычки. Затем мы используем среду тестирования hamcrest, чтобы создать сопоставитель содержимого для ожидаемого вывода. Наконец, мы используем метод тестирования assertThat , чтобы проверить, возвращает ли наш синтаксический анализатор ожидаемый результат.

В реальном сценарии мы должны создать больше модульных тестов, чтобы проверить поведение нашего алгоритма с другими возможными входными данными.

4. Применение регулярных выражений

Реализация синтаксического анализатора является эффективным подходом. Однако полученный алгоритм является относительно большим и сложным. Таким образом, в качестве альтернативы мы можем использовать регулярные выражения.

Далее мы обсудим две возможные реализации, основанные на регулярных выражениях. Тем не менее, их следует использовать с осторожностью, поскольку время их обработки велико по сравнению с предыдущим подходом. Таким образом, использование регулярных выражений для этого сценария может быть недопустимым при обработке больших объемов входных данных.

4.1. Метод разделения строки ()

В этом первом варианте регулярного выражения мы будем использовать метод split() из класса String . Этот метод разбивает строку на соответствие заданному регулярному выражению:

String[] tokens = input.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);

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

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

Последний параметр метода split() — это лимит. Когда мы указываем отрицательный предел, шаблон применяется столько раз, сколько возможно, и результирующий массив токенов может иметь любую длину.

4.2. Разделитель класса Гуавы

Другой альтернативой, основанной на регулярных выражениях, является использование класса Splitter из библиотеки Guava:

Pattern pattern = Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
Splitter splitter = Splitter.on(pattern);
List<String> tokens = splitter.splitToList(input);

Здесь мы создаем объект- разделитель на основе того же шаблона регулярного выражения, что и раньше. После создания разделителя мы используем метод splitToList() , который возвращает список токенов после разделения входной строки .

5. Использование библиотеки CSV

Хотя представленные альтернативы интересны, может оказаться необходимым использовать библиотеку разбора CSV , такую как OpenCSV .

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

Более того, библиотека CSV может быть лучшим подходом, когда мы не уверены в форме нашего ввода . Например, входные данные могут быть экранированы кавычками, которые не будут должным образом обрабатываться предыдущими подходами.

Чтобы использовать OpenCSV, нам нужно включить его в качестве зависимости. В проект Maven мы включаем зависимость opencsv :

<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>4.1</version>
</dependency>

Затем мы можем использовать OpenCSV следующим образом:

CSVParser parser = new CSVParserBuilder()
.withSeparator(',')
.build();

CSVReader reader = new CSVReaderBuilder(new StringReader(input))
.withCSVParser(parser)
.build();

List<String[]> lines = new ArrayList<>();
lines = reader.readAll();
reader.close();

Используя класс CSVParserBuilder , мы начинаем с создания синтаксического анализатора с разделителем-запятой. Затем мы используем CSVReaderBuilder для создания средства чтения CSV на основе нашего синтаксического анализатора на основе запятых.

В нашем примере мы предоставляем StringReader в качестве аргумента конструктору CSVReaderBuilder . Однако при необходимости мы можем использовать другие программы чтения (например, программу чтения файлов).

Наконец, мы вызываем метод readAll() из нашего объекта чтения , чтобы получить список массивов строк . Поскольку OpenCSV предназначен для обработки многострочных входных данных, каждая позиция в списке строк соответствует строке нашего ввода. Таким образом, для каждой строки у нас есть массив String с соответствующими значениями, разделенными запятыми.

В отличие от предыдущих подходов, с OpenCSV двойные кавычки удаляются из сгенерированного вывода.

6. Заключение

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

Как всегда, образцы кода, используемые в этом руководстве, доступны на GitHub .