Як працює {m} {n} («рівно n разів» двічі)?


77

Отже, так чи інакше (граючись), я опинився із таким регулярним виразом \d{1}{2}.

Логічно, для мене це повинно означати:

(Цифра рівно один раз) рівно двічі, тобто цифра рівно двічі.

Але насправді це, здається, означає просто "цифра рівно один раз" (таким чином ігноруючи {2}).

String regex = "^\\d{1}{2}$"; // ^$ to make those not familiar with 'matches' happy
System.out.println("1".matches(regex)); // true
System.out.println("12".matches(regex)); // false

Подібні результати можна побачити, використовуючи {n}{m,n}або подібні.

Чому так трапляється? Це прямо вказано в документації щодо регулярних виразів / Java, чи це просто рішення, яке розробники Java прийняли на льоту, чи це, можливо, помилка?

Або насправді це не ігнорується і насправді означає зовсім інше?

Не те щоб це мало важливе значення, але це не повсюдна поведінка регулярних виразів , Rubular робить те, що я очікую.

Примітка - назва в основному призначена для пошуку користувачів, які хочуть знати, як це працює (а не чому).


57
Ваш шаблон означає (цифра рівно один раз), за якою слідує (нічого точно двічі).
GOTO 0

3
Якщо це допомагає, обидва pcregrepі Mathematica видають помилки для цього регулярного виразу, наприклад pcregrep: Error in command-line regex at offset 8: nothing to repeat. Я б просто використовував {m*n}, або використовував би (?:\\d{1}){2}, що є однозначним.
Джеремі

1
Я не розумію, чому ти не можеш просто використовувати \d{2}? Чи є якась різниця у тому, чого ви намагаєтесь досягти?
Карлос Кемпдеррос,

5
@ CarlosCampderrós Ну, єдине, чого я справді намагаюся досягти, це кращого розуміння регулярних виразів. Проблема є більш теоретичною, мені цікаво з’ясувати, чому вона працює так, як на відміну від регулярного виразу, який працює на прикладі.
Bernhard Barker

2
@Kaz Зовсім не: фігурні повторення в Java застосовуються лише до окремих вузлів (включаючи порожні вузли) або груп, а не до інших повторень. Ви можете створити цей шаблон і перевірити його matchRootза допомогою налагоджувача, якщо не вірите мені. Погляд на вихідний код методу Pattern.closureтакож надасть вам деяку інформацію.
GOTO 0

Відповіді:


76

Коли я ввожу ваш регулярний вираз у RegexBuddy, використовуючи синтаксис регулярного виразу Java, він відображає таке повідомлення

Перед кванторами повинен стояти маркер, який можна повторити «{2}»

Зміна регулярного виразу на явне використання групування ^(\d{1}){2}вирішує цю помилку і працює, як ви очікуєте.


Я припускаю, що движок регулярного виразу Java просто нехтує помилкою / виразом і працює з тим, що було складено до цього часу.

Редагувати

Посилання на стандарт IEEE у відповіді @ piet.t, схоже, підтверджує це припущення.

Редагувати 2 (похвала @fncomp)

Для повноти, як правило, використовують, (?:)щоб уникнути захоплення групи. Тоді стає повним регулярним виразом^(?:\d{1}){2}


Якщо \d{1}{2}це не означає (\d{1}){2}, то що це означає? Якщо асоціативність не зліва направо, то вона повинна бути справа наліво, і тому це означає \d({1}{2}), що безглуздо, якщо ми не визначимо, що означає злипання двох з цих скобкових операторів.
Kaz

@Kaz - тест OP показує, що другий символ дублювання не обчислюється за допомогою механізму регулярних виразів Java. Я вважаю, що piet.t має рацію на тому, що кожна реалізація може робити те, що хоче.
Lieven Keersmaekers

4
Чи не ^(:?\d{1}){2}$було б більш точним відтворенням наміру? (Щоб уникнути захоплення.)
fncomp

1
@fncomp - Було б, це теж те, що я використовував. Невелика друкарська помилка - це повинно бути(?: )
Кобі

@fncomp - Я сам цим займався. Це краща продуктивність, але це не так стисло. З наміром результат такий самий, що мене не турбувало. Я додав ваш коментар до відповіді для повноти.
Lieven Keersmaekers

108

Стандарт IEEE 1003.1 говорить:

Поведінка декількох сусідніх символів дублювання ('*' та інтервали) дає невизначені результати.

Тож кожна реалізація може робити як завгодно, просто не покладайтесь ні на що конкретне ...


1
+1, але чи знаєте ви, чи Java офіційно відповідає цьому стандарту?
Bernhard Barker

2
так, оскільки вихідний результат є дійсним за стандартом, тобто: він може робити все, що завгодно.
STT LCU

2
@Dukeling Я теж так вірю. Повідомлення також System.out.println("".matches("^{1}$"));повертається true. Моя ставка полягає в тому, що якщо Java не може знайти дійсний шаблон для повторення, він буде повторюватись, nullа не видавати помилку (яка відповідає де-небудь у рядку). Крім того, ви використовували тестер регулярних виразів на основі Ruby для Java !?
Джеррі

3
@STTLCU Ну, є різниця між офіційним та неофіційним дотриманням чи недотриманням. Офіційне відповідність означає, що його можна цитувати як джерело, інакше це все ще приємне посилання, але не обов'язково пояснює, чому Java робить те, що робить.
Бернхард Баркер,

3
Я цілком впевнений, що цей стандарт призначений для POSIX BRE та ERE, і він не має нічого спільного з регулярним виразом Java. Java навіть не претендує на підтримку ERE або BRE! В іншому випадку тут слід навести регулярний вираз Unicode.org/reports/tr18 .
nhahtdh,

10

Науковий підхід:
натисніть на шаблони, щоб побачити приклад на regexplanet.com, і натисніть на зелену кнопку Java .

  • Ви вже показували \d{1}{2}збіги "1"та не збігаються "12", тому ми знаємо, що це не трактується як (?:\d{1}){2}.
  • Тим НЕ менше, 1 ряд нудний, і {1} може бути оптимізований геть, давайте спробуємо що - щось більш цікаве:
    \d{2}{3}. Це як і раніше відповідає лише двом символам (а не шести), {3}ігнорується.
  • Гаразд. Існує простий спосіб побачити, що робить механізм регулярних виразів. Це захоплює?
    Давайте спробуємо (\d{1})({2}). Як не дивно, це працює. Друга група $2,, фіксує порожній рядок.
  • То навіщо нам перша група? Як щодо ({1})? Все ще працює.
  • І просто {1}? Немає проблем.
    Схоже, Java тут трохи дивна.
  • Чудово! Так {1}діє. Ми знаємо, що Java розширюється *і +до {0,0x7FFFFFFF}і{1,0x7FFFFFFF} , так буде *чи +працюватиме? Ні:

    Висячий метасимвол '+' біля індексу 0
    +
    ^

    Перевірка повинна відбуватися раніше *і +буде розширена.

У специфікації я не знайшов нічого, що пояснює це, схоже, квантор повинен бути принаймні після символу, дужок або дужок.

Більшість цих моделей вважаються недійсними за іншими ароматами регулярного виразу, і з поважної причини - вони не мають сенсу.


4

Спочатку я був здивований, що це не кидає PatternSyntaxException.

Я не можу базувати свою відповідь на будь-яких фактах, тому це лише освічена здогадка:

"\\d{1}"    // matches a single digit
"\\d{1}{2}" // matches a single digit followed by two empty strings

4

Я ніколи ніде не бачив {m}{n}синтаксису. Здається, механізм регулярних виразів на цій сторінці Rubular застосовує {2}квантор до найменшого можливого маркера до цього - що є \\d{1}. Щоб імітувати це в Java (або більшості інших механізмів регулярних виразів, здавалося б), вам потрібно згрупувати \\d{1}подібне так:

^(\\d{1}){2}$

Подивіться це в дії тут .


4

Складена структура регулярного виразу

Відповідь Кобі - про поведінку регулярного виразу Java (реалізація Sun / Oracle) для випадку "^\\d{1}{2}$", або "{1}".

Нижче представлена ​​внутрішня скомпільована структура "^\\d{1}{2}$":

^\d{1}{2}$
Begin. \A or default ^
Curly. Greedy quantifier {1,1}
  Ctype. POSIX (US-ASCII): DIGIT
  Node. Accept match
Curly. Greedy quantifier {2,2}
  Slice. (length=0)

  Node. Accept match
Dollar(multiline=false). \Z or default $
java.util.regex.Pattern$LastNode
Node. Accept match

Переглядаючи вихідний код

З мого розслідування, помилка, мабуть, пов’язана з тим фактом, який {не був належним чином перевірений приватним методом sequence().

Метод sequence()викликає до atom()синтаксичного аналізу атома, потім приєднує квантор до атома за допомогою виклику closure()та з'єднує всі атоми із замиканням в одну послідовність.

Наприклад, враховуючи цей регулярний вираз:

^\d{4}a(bc|gh)+d*$

Тоді виклик верхнього рівня , щоб sequence()отримає скомпільовані вузли для ^, \d{4}, a, (bc|gh)+, d*,$ і ланцюгів їх разом.

З огляду на цю ідею, давайте розглянемо вихідний код sequence(), скопійований з OpenJDK 8-b132 (Oracle використовує ту саму основу коду):

@SuppressWarnings("fallthrough")
/**
 * Parsing of sequences between alternations.
 */
private Node sequence(Node end) {
    Node head = null;
    Node tail = null;
    Node node = null;
LOOP:
    for (;;) {
        int ch = peek();
        switch (ch) {
        case '(':
            // Because group handles its own closure,
            // we need to treat it differently
            node = group0();
            // Check for comment or flag group
            if (node == null)
                continue;
            if (head == null)
                head = node;
            else
                tail.next = node;
            // Double return: Tail was returned in root
            tail = root;
            continue;
        case '[':
            node = clazz(true);
            break;
        case '\\':
            ch = nextEscaped();
            if (ch == 'p' || ch == 'P') {
                boolean oneLetter = true;
                boolean comp = (ch == 'P');
                ch = next(); // Consume { if present
                if (ch != '{') {
                    unread();
                } else {
                    oneLetter = false;
                }
                node = family(oneLetter, comp);
            } else {
                unread();
                node = atom();
            }
            break;
        case '^':
            next();
            if (has(MULTILINE)) {
                if (has(UNIX_LINES))
                    node = new UnixCaret();
                else
                    node = new Caret();
            } else {
                node = new Begin();
            }
            break;
        case '$':
            next();
            if (has(UNIX_LINES))
                node = new UnixDollar(has(MULTILINE));
            else
                node = new Dollar(has(MULTILINE));
            break;
        case '.':
            next();
            if (has(DOTALL)) {
                node = new All();
            } else {
                if (has(UNIX_LINES))
                    node = new UnixDot();
                else {
                    node = new Dot();
                }
            }
            break;
        case '|':
        case ')':
            break LOOP;
        case ']': // Now interpreting dangling ] and } as literals
        case '}':
            node = atom();
            break;
        case '?':
        case '*':
        case '+':
            next();
            throw error("Dangling meta character '" + ((char)ch) + "'");
        case 0:
            if (cursor >= patternLength) {
                break LOOP;
            }
            // Fall through
        default:
            node = atom();
            break;
        }

        node = closure(node);

        if (head == null) {
            head = tail = node;
        } else {
            tail.next = node;
            tail = node;
        }
    }
    if (head == null) {
        return end;
    }
    tail.next = end;
    root = tail;      //double return
    return head;
}

Візьміть на замітку рядок throw error("Dangling meta character '" + ((char)ch) + "'");. Це де помилка виникає , якщо +, *, ?звисає і не є частиною попередніх маркерів. Як бачите, {не серед випадків викидання помилок. Насправді він відсутній у списку справ sequence(), а процес компіляції буде переходити defaultбезпосередньо до кожного випадку atom().

@SuppressWarnings("fallthrough")
/**
 * Parse and add a new Single or Slice.
 */
private Node atom() {
    int first = 0;
    int prev = -1;
    boolean hasSupplementary = false;
    int ch = peek();
    for (;;) {
        switch (ch) {
        case '*':
        case '+':
        case '?':
        case '{':
            if (first > 1) {
                cursor = prev;    // Unwind one character
                first--;
            }
            break;
        // Irrelevant cases omitted
        // [...]
        }
        break;
    }
    if (first == 1) {
        return newSingle(buffer[0]);
    } else {
        return newSlice(buffer, first, hasSupplementary);
    }
}

Коли процес вступає atom(), оскільки він стикається {відразу, він обривається switchі forцикл, і створюється новий зріз довжиною 0 (довжина походить відfirst , що дорівнює 0).

Коли цей зріз повертається, квантор аналізується closure(), в результаті чого ми бачимо.

Порівнюючи вихідний код Java 1.4.0, Java 5 та Java 8, здається, що у вихідному коді sequence()та atom(). Здається, ця помилка існує з самого початку.

Стандарт для регулярних виразів

Відповідь з найбільшою оцінкою, посилаючись на стандарт IEEE 1003.1 (або стандарт POSIX), не має значення для обговорення, оскільки Java не застосовує BRE та ERE.

Існує багато синтаксису, що призводить до невизначеної поведінки відповідно до стандарту, але це чітко визначена поведінка у багатьох інших варіантах регулярних виразів (хоча погоджуються вони чи ні - це інша справа). Наприклад,\d не визначено відповідно до стандарту, але воно відповідає цифрам (ASCII / Unicode) у багатьох варіантах регулярних виразів.

На жаль, немає іншого стандарту щодо синтаксису регулярних виразів.

Однак існує стандарт регулярного виразу Unicode, який зосереджується на особливостях, які повинен мати механізм регулярних виразів Unicode. PatternКлас Java більш-менш реалізує підтримку рівня 1, як описано в UTS # 18: Регулярний вираз Unicode та RL2.1 (хоча і надзвичайно глючний).


0

Я припускаю , що у визначенні {}що - щось на зразок «погляд назад , щоб знайти дійсне вираз ( за винятком себе - {}", так що у вашому прикладі немає нічого між }і {.

У будь-якому випадку, якщо ви обернете його в дужки, він буде працювати, як ви очікували: http://refiddle.com/gv6 .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.