Найти шаблон в файлах с помощью java 8
считаю, что у меня есть файл (просто выдержка)
name: 'foobar'
Мне нравится извлекать foobar
, когда я обнаруживаю строку с name
.
Мой текущий подход
Pattern m = Pattern.compile("name: '(.+)'");
try (Stream<String> lines = Files.lines(ruleFile)) {
Optional<String> message = lines.filter(m.asPredicate()).findFirst();
if (message.isPresent()) {
Matcher matcher = m.matcher(message.get());
matcher.find();
String group = matcher.group(1);
System.out.println(group);
}
}
который выглядит не очень хорошо. Чрезмерное использование шаблона и сопряжения кажется неправильным.
Есть ли более простой/лучший способ? Особенно, если у меня есть несколько ключей, которые мне нравятся, например:
Ответы
Ответ 1
Я бы ожидал чего-то большего подобного, чтобы избежать совпадения шаблона дважды:
Pattern p = Pattern.compile("name: '([^']*)'");
lines.map(p::matcher)
.filter(Matcher::matches)
.findFirst()
.ifPresent(matcher -> System.out.println(matcher.group(1)));
То есть для каждого совпадения строк получите первый, который соответствует, для этого распечатайте первую группу.
Ответ 2
Вот как будет выглядеть решение Java 9:
Matcher m = Pattern.compile("name: '(.+)'").matcher("");
try(Stream<String> lines = Files.lines(ruleFile)) {
lines.flatMap(line -> m.reset(line).results().limit(1))
.forEach(mr -> System.out.println(mr.group(1)));
}
Он использует метод Matcher.results()
, который возвращает поток всех совпадений. Объединение потока строк с потоком совпадений с помощью flatMap
позволяет обрабатывать все совпадения файла. Поскольку ваш исходный код обрабатывает только первое совпадение строки, я просто добавил limit(1)
к совпадениям каждой строки, чтобы получить такое же поведение.
К сожалению, эта функция отсутствует в Java 8, однако проникновение в предстоящие выпуски помогает понять, как может выглядеть промежуточное решение:
Matcher m = Pattern.compile("name: '(.+)'").matcher("");
try(Stream<String> lines = Files.lines(ruleFile)) {
lines.flatMap(line -> m.reset(line).find()? Stream.of(m.toMatchResult()): null)
.forEach(mr -> System.out.println(mr.group(1)));
}
Чтобы упростить создание подпотока, в этом решении используется только первое совпадение, и в первую очередь создается поток отдельных элементов.
Но обратите внимание, что с шаблоном вопросов 'name: '(.+)'
не имеет значения, ограничиваем ли число совпадений как .+
, с жадностью сопоставляем все символы с последним последующим '
строки, так что другое совпадение невозможно. При использовании неохотного квантификатора, например, с name: '(.*?)'
, который потребляет до следующего '
, а не последнего, или не позволяет пропустить предыдущий '
явно, как и в случае с name: '([^']*)'
.
В приведенных выше решениях используется общий Matcher
, который хорошо работает с однопоточным использованием (и это вряд ли когда-либо выиграет от параллельной обработки). Но если вы хотите быть в потокобезопасной стороне, вы можете делиться только Pattern
и создавать Matcher
вместо вызова m.reset(line)
:
Pattern pattern = Pattern.compile("name: '(.*)'");
try(Stream<String> lines = Files.lines(ruleFile)) {
lines.flatMap(line -> pattern.matcher(line).results().limit(1))
.forEach(mr -> System.out.println(mr.group(1)));
}
соотв. с Java 8
try(Stream<String> lines = Files.lines(ruleFile)) {
lines.flatMap(line -> {Matcher m=pattern.matcher(line);
return m.find()? Stream.of(m.toMatchResult()): null;})
.forEach(mr -> System.out.println(mr.group(1)));
}
который не является кратким из-за введения локальной переменной. Этого можно избежать с помощью предыдущей операции map
, но когда мы находимся в этой точке, пока мы направляемся только на одно совпадение на строку, нам не нужно flatMap
, а затем:
try(Stream<String> lines = Files.lines(ruleFile)) {
lines.map(pattern::matcher).filter(Matcher::find)
.forEach(m -> System.out.println(m.group(1)));
}
Так как каждый Matcher
используется ровно один раз, без вмешательства, его изменчивая природа здесь не болит, и преобразование в неизменяемое MatchResult
становится ненужным.
Однако эти решения не могут быть масштабированы для обработки нескольких совпадений на строку, если это когда-либо понадобится...
Ответ 3
Ответ @khelwood приводит к созданию нового объекта Matcher
снова и снова, что может быть источником неэффективности при проверке длинных файлов.
Следующее решение создает совпадение только один раз и повторно использует его для каждой строки в файле.
Pattern p = Pattern.compile("name: '([^']*)'");
Matcher matcher = p.matcher(""); // Create a matcher for the pattern
Files.lines(ruleFile)
.map(matcher::reset) // Reuse the matcher object
.filter(Matcher::matches)
.findFirst()
.ifPresent(m -> System.out.println(m.group(1)));
Предупреждение - Подозрительный взломать вперед
Конвейер .map(matcher::reset)
- это место, где происходит магия/хак. Он эффективно вызывает matcher.reset(line)
, который сбрасывает Matcher
для выполнения следующего совпадения в строке, только что прочитанной из файла, и возвращает себя, чтобы разрешить цепочки вызовов. Оператор потока .map(...)
видит это как отображение из строки в объект Matcher
, но на самом деле мы каждый раз сохраняем отображение одного и того же объекта Matcher
, нарушая всевозможные правила о побочных эффектах и т.д.
Конечно, этот не может использоваться для параллельных потоков, но, к счастью, чтение из файла по своей сути является последовательным.
Взлом или оптимизация? Я предполагаю, что голосование будет зависеть.