Несколько совпадений в одном регулярном выражении Java

#java #regex

#java #регулярное выражение

Вопрос:

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

 this 10 12 3 44 5 66 7 8    # should return "this", "10", "12", ...
another 1 2 3               # should return "another", "1", "2", "3"
 

EDIT1: мои фактические данные не так просты, цифры на самом деле представляют собой более сложные шаблоны, но для иллюстрации я сократил проблему до простых цифр, поэтому мне требуется ответ с регулярным выражением.

Длина чисел в каждой строке неизвестна, но все они соответствуют простому шаблону.

Следующее соответствует только «this» и «10»:

 ([p{Alpha}]  )(d  ?) ?
 

Удаление окончательных ? совпадений «this» и «8».

Я думал, что последняя группа (d ?) будет выполнять сопоставление цифр несколько раз, но это не так, и я не могу найти синтаксис для этого, если это возможно.

Я могу сделать это за несколько проходов, только выполняя поиск имени и последних чисел отдельно, но мне было интересно, возможно ли это в одном выражении? (А если нет, есть ли причина?)


EDIT2: Как я упоминал в некоторых комментариях, это был вопрос в Advent of Code (день 7, 2020). Я искал самое чистое решение (кто не любит немного полировать?)

Вот мое окончательное решение (kotlin) Я использовал, но потратил слишком много времени, пытаясь сделать это в 1 регулярном выражении, поэтому я опубликовал этот вопрос.

 val bagExtractor = Regex("""^([p{Alpha} ] ) bags contain""")
val rulesExtractor = Regex("""([d] ) ([p{Alpha} ] ) bag""")

// bagRule is a line from the input
val bag = bagExtractor.find(bagRule)?.destructured!!.let { (n) -> Bag(name = n) }
val contains = rulesExtractor.findAll(bagRule).map { it.destructured.let { (num, bagName) -> Contain(num = num.toInt(), bag = Bag(bagName)) } }.toList()
Rule(bag = bag, contains = contains)
 

Несмотря на то, что теперь я знаю, что это можно сделать в 1 строке, я не реализовал это, так как считаю, что в 2 это чище.

Комментарии:

1. Глядя на это, разве вы не можете просто разделить на пробелы? А если нет, то почему?

2. это очень упрощенная версия фактического ввода, где конечные числа представляют собой более сложные шаблоны (фактически шаблона «<число> <слово1> <слово2> <другие биты>»), которые демонстрируют одинаковое поведение, совпадая только с первым или последним выражением, а не с полным списком элементов.

3. Да, используйте String pat = "(\G(?!^)|\b\p{L} \b)\s (\d )"; . Группа 1 будет сопоставлена только при совпадении начального слова. Вам нужно использовать его с matcher.find некоторой дополнительной логикой кода.

4. Это волшебство! Я проверил это на freeformatter.com/java-regex-tester.html#ad-output и, как вы говорите, начальная группа немного перекошена, но в остальном довольно хороша. совпадения дают «другое 1», «2», «3».

Ответ №1:

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

 import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String str = "this 10 12 3 44 5 66 7 8";
        String[] parts = str.split("\s ");
        System.out.println(Arrays.toString(parts));
    }
}
 

Вывод:

 [this, 10, 12, 3, 44, 5, 66, 7, 8]
 

Если вы хотите выбрать только алфавитный текст и целочисленный текст из строки, вы можете сделать это следующим образом

 import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        String str = "this 10 12 3 44 5 66 7 8";
        Matcher matcher = Pattern.compile("(\b\p{Alpha} \b)|(\b\d \b)").matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}
 

Вывод:

 this
10
12
3
44
5
66
7
8
 

или как

 import java.util.List;
import java.util.regex.MatchResu<
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        String str = "this 10 12 3 44 5 66 7 8";

        List<String> list = Pattern.compile("(\b\p{Alpha} \b)|(\b\d \b)")
                            .matcher(str)
                            .results()
                            .map(MatchResult::group)                                                        
                            .collect(Collectors.toList());

        System.out.println(list);
    }
}
 

Вывод:

 [this, 10, 12, 3, 44, 5, 66, 7, 8]
 

Комментарии:

1. Я должен был прокомментировать быстрее 🙂 Нет, это невозможно с реальными данными, «цифры» в моих фактических данных представляют собой более сложные структуры, состоящие из нескольких слов, но соответствуют шаблону, который я могу сопоставить

2. @MarkFisher — Не могли бы вы опубликовать здесь фактический образец (после скрытия PII, если таковой имеется)?

3. Приведенные примеры данных должны быть достаточно хорошими для тестирования. У меня есть решение, которое состоит в том, чтобы разделить пример регулярного выражения, который я дал, на 2 части и дважды сканировать входные данные с каждым регулярным выражением. Это работает нормально, я просто не понимаю, почему их комбинация не работает.

4. @MarkFisher — я опубликовал обновление. Если ввод и вывод не соответствуют вашим ожиданиям, не стесняйтесь комментировать пример ввода и ожидаемый результат.

5. Это работает! Хороший ответ. Я использовал ([p{Alpha} ] ) bags contain|(d ) ([p{Alpha} ] ) bag фактические входные данные, которые соответствуют всему, что мне нужно в строке. Приветствия.

Ответ №2:

Нет. Понятие «найди мне все определенное регулярное выражение» просто не выполняется с увеличением групп. Вы действительно спрашиваете, почему регулярное выражение такое, какое оно есть? Это… эпический тезис, который углубляется в какую-то древнюю историю вычислений и множество интервью Ларри Уолла (автора Perl, из которого более или менее пришли регулярные выражения), который кажется немного выходящим за рамки SO. Они работают таким образом, потому что регулярные выражения работают именно так, а те работают именно так, потому что они работали так десятилетиями, и их изменение нарушило бы ожидания людей; давайте не будем углубляться в это.

Вместо этого вы можете сделать это с помощью сканеров:

 Scanner s = new Scanner("this 10 12 3 44 5 66 7 8");
assertEquals("this", s.next());
assertEquals(10, s.nextInt());
// etc
 

или даже:

 Scanner s = new Scanner("this 10 12 3 44 5 66 7 8");
assertEquals("this", s.next());
assertEquals(10, s.nextInt());
// etc
 

или даже:

 Scanner s = new Scanner("this 10 12 3 44 5 66 7 8");
assertEquals("this", s.next(Pattern.compile("[p{Alpha}] "));
assertEquals(10, s.nextInt());

s = new Scanner("--00invalid-- 10 12 3 44 5 66 7 8");
// the line below will throw an InputMismatchException
s.next(Pattern.compile("[p{Alpha}] "));
 

ПРИМЕЧАНИЕ: сканеры токенизируют (они разделяют входные данные на последовательность токенов, разделителей, токенов, разделителей и т. Д., Затем отбрасывают разделители и выдают вам токены). .next(Pattern) не означает: продолжайте сканирование, пока не найдете что-то подходящее. Это просто означает: возьмите следующий токен. Если оно соответствует этому регулярному выражению, отлично, верните его. В противном случае произойдет сбой.

Итак, настоящая магия заключается в том, чтобы сделать сканер токенизированным, как вы хотите. Это делается с помощью use .useDelimiter() и также основано на регулярных выражениях. Некоторая причудливая работа ногами с позитивным прогнозом и со может завести вас далеко, но это не бесконечно мощно. Вы не расширили фактическую структуру вашего ввода, поэтому я не могу сказать, будет ли этого достаточно для ваших нужд.

Комментарии:

1. Примером фактического ввода является posh crimson bags contain 2 mirrored tan bags, 1 faded red bag, 1 striped gray bag. то, что некоторые могут распознать из 7-го дня AOC 2020 сегодня. Я получил ответ, используя 2 регулярных выражения: ^([p{Alpha} ] ) bags contain и ([d] ) ([p{Alpha} ] ) bag но хотел, чтобы одно выражение работало, если это возможно, сопоставляя начало, а затем несколько значений в конце строки.

Ответ №3:

Предполагая, что вы говорите об этом: adventofcode, где входные данные являются правилами

 light red bags contain 1 bright white bag, 2 muted yellow bags.
dark orange bags contain 3 bright white bags, 4 muted yellow bags.
bright white bags contain 1 shiny gold bag.
muted yellow bags contain 2 shiny gold bags, 9 faded blue bags.
shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags.
dark olive bags contain 3 faded blue bags, 4 dotted black bags.
vibrant plum bags contain 5 faded blue bags, 6 dotted black bags.
faded blue bags contain no other bags.
dotted black bags contain no other bags.
 

Зачем искать сложное регулярное выражение, когда вы можете легко разделить слово contain или ,

 String str1 = "light red bags contain 1 bright white bag, 2 muted yellow bags.";
String str2 = "dotted black bags contain no other bags.";
String[] split1 = str1.split("\scontain\s|,");
String[] split2 = str2.split("\scontain\s|,");

System.out.println(Arrays.toString(split1));
System.out.println(Arrays.toString(split2));

//[light red bags, 1 bright white bag,  2 muted yellow bags.]
//[dotted black bags, no other bags.]
 

Комментарии:

1. Да, это загадка на сегодня. Я решил это нормально, я просто пытался найти одно регулярное выражение для обслуживания всей строки, отсюда и вопрос. На самом деле я использую Kotlin, но регулярное выражение одинаково между ними. В моем первом решении я использовал разделение по пробелу и брал по 4 слова за раз, но оно было ужасно длинным и запутанным, а затем переработано в регулярное выражение, удалив половину кода. Я опубликую свое собственное решение в вопросе, поскольку оно плохо форматируется в комментариях. Спасибо за ваш ответ!

Ответ №4:

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

 String[] data = { "this 10 12 3 44 5 66 7 8",
        "Bad Data 5 5 5",
        "another 1 2 3" };

for (String text : data) {
    Optional<List<String>> op = parseText(text);
    if (!op.isEmpty()) {
        System.out.println(op.get());
    }
}
 

С принтами

 [this, 10, 12, 3, 44, 5, 66, 7, 8]
[another, 1, 2, 3]
 
 static String pattern = "([a-zA-Z] )(\s \d ) ";
    
public static Optional<List<String>> parseText(String text) {
    if (text.matches(pattern)) {
        return Optional.of(Arrays.stream(text.split("\s "))
                .collect(Collectors.toList()));
    }
    return Optional.empty();
}
 

Комментарии:

1. спасибо за ваш ответ. я пытался не увязнуть в вопросе слишком подробно, чтобы идея потерялась. вопрос действительно касался анализа нескольких записей во входных данных с помощью регулярных выражений, а не этих конкретных значений, и, оглядываясь назад, можно понять, почему некоторые (очень хорошие) ответы больше склонялись к разделению на пробелы и тому подобное. Это помогло бы, если бы я сказал, что входные данные хорошо сформированы, поэтому мне не нужно было беспокоиться о том, чтобы сначала убедиться, что они совпадают перед синтаксическим анализом. Советы для меня в следующий раз, когда я задам вопрос!

2. Я понимаю — никаких проблем. Проблема была не в разделении на пробелы (по крайней мере, для меня). Он пытался захватить неповторяющуюся группу (альфа), за которой следует некоторое количество чисел. Но важно то, что у вас есть ответ, который вы можете использовать.