Подтвердить что ты не робот

Найти шаблон в файлах с помощью 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);
    }
}

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

Есть ли более простой/лучший способ? Особенно, если у меня есть несколько ключей, которые мне нравятся, например:

4b9b3361

Ответ 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, нарушая всевозможные правила о побочных эффектах и ​​т.д.

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

Взлом или оптимизация? Я предполагаю, что голосование будет зависеть.