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 .