Как сделать захват балансировочной группы?

Допустим, у меня есть этот текстовый ввод.

 tes{}tR{R{abc}aD{mnoR{xyz}}}

Я хочу извлечь вывод ff:

 R{abc}
 R{xyz}
 D{mnoR{xyz}}
 R{R{abc}aD{mnoR{xyz}}}

В настоящее время я могу извлекать только то, что находится внутри групп {}, используя подход сбалансированной группы, как в msdn. Вот образец:

 ^[^{}]*(((?'Open'{)[^{}]*)+((?'Target-Open'})[^{}]*)+)*(?(Open)(?!))$

Кто-нибудь знает, как включить R{} и D{} в вывод?

3 ответа

Решение

Я думаю, что здесь требуется другой подход. После того, как вы сопоставите первую большую группу R{R{abc}aD{mnoR{xyz}}} (см. мой комментарий о возможной опечатке), вы не сможете получить внутри подгруппы, так как регулярное выражение не позволяет вам захватить человека R{ ... } групп.

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

(?=([A-Z](?:(?:(?'O'{)[^{}]*)+(?:(?'-O'})[^{}]*?)+)+(?(O)(?!))))

[Я также переименовал 'Open' в 'O' и удалил именованный захват для закрывающей скобки, чтобы сделать его короче и избежать шумов в матчах]

На regexhero.net (единственный бесплатный тестер регулярных выражений.NET, которого я знаю) я получил следующие группы захвата:

1: R{R{abc}aD{mnoR{xyz}}}
1: R{abc}
1: D{mnoR{xyz}}
1: R{xyz}

Распределение регулярных выражений:

(?=                         # Opening positive lookahead
    ([A-Z]                  # Opening capture group and any uppercase letter (to match R & D)
        (?:                 # First non-capture group opening
            (?:             # Second non-capture group opening
                (?'O'{)     # Get the named opening brace
                [^{}]*      # Any non-brace
            )+              # Close of second non-capture group and repeat over as many times as necessary
            (?:             # Third non-capture group opening
                (?'-O'})    # Removal of named opening brace when encountered
                [^{}]*?     # Any other non-brace characters in case there are more nested braces
            )+              # Close of third non-capture group and repeat over as many times as necessary
        )+                  # Close of first non-capture group and repeat as many times as necessary for multiple side by side nested braces
        (?(O)(?!))          # Condition to prevent unbalanced braces
    )                       # Close capture group
)                           # Close positive lookahead

Следующее не будет работать в C#

Я действительно хотел попробовать, как это должно работать на движке PCRE, так как была возможность иметь рекурсивное регулярное выражение, и я думаю, что это было проще, так как я более знаком с ним и что привело к более короткому регулярному выражению:)

(?=([A-Z]{(?:[^{}]|(?1))+}))

regex101 demo

(?=                    # Opening positive lookahead
    ([A-Z]             # Opening capture group and any uppercase letter (to match R & D)
        {              # Opening brace
            (?:        # Opening non-capture group
                [^{}]  # Matches non braces
            |          # OR
                (?1)   # Recurse first capture group
            )+         # Close non-capture group and repeat as many times as necessary
        }              # Closing brace
    )                  # Close of capture group
)                      # Close of positive lookahead

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

Одним из решений может быть следующий алгоритм (написанный на Java, но я думаю, что перевод на C# не будет таким сложным):

/**
 * Finds all matches (i.e. including sub/nested matches) of the regex in the input string.
 * 
 * @param input
 *          The input string.
 * @param regex
 *          The regex pattern. It has to target the most nested substrings. For example, given the following input string
 *          <code>A{01B{23}45C{67}89}</code>, if you want to catch every <code>X{*}</code> substrings (where <code>X</code> is a capital letter),
 *          you have to use <code>[A-Z][{][^{]+?[}]</code> or <code>[A-Z][{][^{}]+[}]</code> instead of <code>[A-Z][{].+?[}]</code>.
 * @param format
 *          The format must follow the <a href= "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax" >format string
 *          syntax</a>. It will be given one single integer as argument, so it has to contain (and to contain only) a <code>%d</code> flag. The
 *          format must not be foundable anywhere in the input string. If <code>null</code>, <code>ééé%dèèè</code> will be used.
 * @return The list of all the matches of the regex in the input string.
 */
public static List<String> findAllMatches(String input, String regex, String format) {

    if (format == null) {
        format = "ééé%dèèè";
    }
    int counter = 0;
    Map<String, String> matches = new LinkedHashMap<String, String>();
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(input);

    // if a substring has been found
    while (matcher.find()) {
        // create a unique replacement string using the counter
        String replace = String.format(format, counter++);
        // store the relation "replacement string --> initial substring" in a queue
        matches.put(replace, matcher.group());
        String end = input.substring(matcher.end(), input.length());
        String start = input.substring(0, matcher.start());
        // replace the found substring by the created unique replacement string
        input = start + replace + end;
        // reiterate on the new input string (faking the original matcher.find() implementation)
        matcher = pattern.matcher(input);
    }

    List<Entry<String, String>> entries = new LinkedList<Entry<String, String>>(matches.entrySet());

    // for each relation "replacement string --> initial substring" of the queue
    for (int i = 0; i < entries.size(); i++) {
        Entry<String, String> current = entries.get(i);
        // for each relation that could have been found before the current one (i.e. more nested)
        for (int j = 0; j < i; j++) {
            Entry<String, String> previous = entries.get(j);
            // if the current initial substring contains the previous replacement string
            if (current.getValue().contains(previous.getKey())) {
                // replace the previous replacement string by the previous initial substring in the current initial substring
                current.setValue(current.getValue().replace(previous.getKey(), previous.getValue()));
            }
        }
    }

    return new LinkedList<String>(matches.values());
}

Таким образом, в вашем случае:

String input = "tes{}tR{R{abc}aD{mnoR{xyz}}}";
String regex = "[A-Z][{][^{}]+[}]";
findAllMatches(input, regex, null);

Возвращает:

R{abc}
R{xyz}
D{mnoR{xyz}}
R{R{abc}aD{mnoR{xyz}}}

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

Пример MSDN слишком сложен. Более простой подход для сопоставления вложенных структур:

(?>
    (?<O>)\p{Lu}\{   # Push to the O stack, and match an upper-case letter and {
    |                # OR
    \}(?<-O>)        # Match } and pop from the stack
    |                # OR
    \p{Ll}           # Match a lower-case letter
)+
(?(O)(?!))        # Make sure the stack is empty

или в одну строку:

(?>(?<O>)\p{Lu}\{|\}(?<-O>)|\p{Ll})+(?(O)(?!))

Рабочий пример для Regex Storm

В вашем примере это также соответствует "tes" в начале строки, но не беспокойтесь об этом, мы еще не закончили.

С небольшой коррекцией мы также можем зафиксировать вхождения между R{... } пары:

(?>(?<O>)\p{Lu}\{|\}(?<Target-O>)|\p{Ll})+(?(O)(?!))

каждый Match будет иметь Group называется "Target" и каждый такой Group будет иметь Capture для каждого случая - вы заботитесь только об этих снимках.

Рабочий пример для Regex Storm - Нажмите на вкладку Таблица и изучите 4 снимка ${Target}

Смотрите также:

Другие вопросы по тегам