Наша взаимовыгодная связь https://banwar.org/
Я думаю всі чули про Регулярні вирази і знають навіщо вони потрібні - по суті вони дозволяють дізнатися чи відповідає рядок регулярному виразу, і якщо так - то які саме ділянки рядки відповідають яких ділянках вираження.
А що якщо написати свій движок регулярних виразів?
Отже, згадаємо, що регулярні вирази - це по суті кінцевий автомат. Коли ми зустрічаємо якийсь символ - автомат переходить в новий стан (якщо це можливо). Наприклад, для вираження ab (cd | ef) g вийде такий автомат:
Я не буду вдаватися в подробиці про детермінованих і недетермінірованних автоматах. Просто будемо пам'ятати, що саме слово описує всілякі стану і зв'язку між ними, а вхідний рядок буква за буквою перемикає стану автомата. Тут, наприклад, в стані 2 якщо на вхід надійде буква 'd' - перейдемо в стан 3, якщо буква 'c' - в стан 4. Іноді можна буде сказати в який стан переходити не заглядаючи наперед рядки.
Найпростіший движок регулярних виразів
Існує чудова книжка Роба Пайка і (здається) Брайна Керніган - The Practice Of Programming (TPOP) . Там наводиться код найпростішого движка для регулярних виразів. Кому лінь читати всю книгу - тут описаний основний підхід і наводяться ті самі легендарні 30 рядків коду.
Движок здатний розпізнавати початок / кінець рядка і задавати wildcard - маску для безлічі однакових символів.
Геніальність цієї реалізації в рекурсивном підході. Код виходить дуже лаконічний, а різниця в швидкості майже не помітна.
Витягуємо більше користі
Я вирішив використовувати цей код за основу, але отримати трохи більше, а саме:
- Дізнаватися де починається відповідність і скільки символів рядка потрапляє під вираз
- Додати спеціальні групи символом - ну хоча б \ d для цифр і \ s для прогалин
- Додати квантіфікатори (ну або як це назвати?) Для знаходження 0 і більше входжень ( «*»), 1 і більше ( «+»), 0 або 1 ( «?») І жодного оператора для 0 і більше ( «-» - як в Lua)
Спробуємо вкластися рядків десь в 100.
Починати будемо так само - нам потрібна єдина головна функція і функція, яка визначає відповідність початку рядка висловом - regex_match і regex_match_here відповідно:
int regex_match (const char * regex, char * s, int * len) {char * p = s; / * Force match from the beginning of the string * / if (regex [0] == '^') {return (regex_match_here (regex + 1, s, len)? 0: -1); } / * Iterate the string to find matching position * / do {* len = 0; if (regex_match_here (regex, p, len)) {return p - s; }} While (* p ++! = '\ 0'); return -1; } Int regex_match_here (const char * regex, char * s, int * len) {// TODO}З regex_match все ясно, якщо «^» - перевіряємо початок рядка, інакше - танцюємо зліва направо поки не знайдемо відповідність.
Найголовніше знаходиться в regex_match_here:
static int regex_match_here (const char * regex, char * s, int * len) {int c = regex [0]; if (regex [0] == '\ 0') {/ * end of regex = full match * / return 1; } Else if (regex [0] == '$' && regex [1] == '\ 0') {/ * check end of string * / return (* s == '\ 0'); } Else if (regex [0] == '\\' && regex [1]! = '\ 0') {/ * check escaped symbol * / c = regex [1]; if (c! = '^' && c! = '$' && c! = '\\' && c! = '+' && c! = '*' && c! = '-' && c! = '? ') {c = c | 0x100; } Regex = regex + 1; } / * Check for special operators *, +,?, - * / if (regex [1] == '*' || regex [1] == '+' || regex [1] == '-' | | regex [1] == '?') {return regex_match_quantity (regex [1], c, regex + 2, s, len); } Else if (* s! = '\ 0' && regex_match_group (* s, c)) {* len = * len + 1; return regex_match_here (regex + 1, s + 1, len); } Return 0; }Тут перевіряємо стан нашого неявного автомата:
- якщо вираз закінчилося (досягнуто кінцевий стан) - повернути 1, відповідність знайдено
- якщо в регулярному виразі дійшли до останнього символу, і це - «$» (тобто кінець рядка) - повернути 1 якщо рядок теж закінчилася.
- якщо знайшли «\» - екранує символ, але ми маємо справу з особливою групою символів. Взагалі, в звичайному синтаксисі «\» грає дві ролі - для «\.», «\ ^», «\ $», '\?', '\ *', '\ +', '\ -' він означає використовувати просто символи «.», «^» і т.д. замість з мета-значень ( «будь-який символ», «початок рядка», «кінець рядка», ...). Для інших ( «\ d», «\ s», ...) він означає використовувати мета-значення ( «будь-яка цифра», «будь-який символ пробілу», ...). Домовимося, що значення вище 256 означатимуть мета-символ, тобто ( 'D' | 0x100) - це «будь-яка цифра», а 'd' - буква 'd'. Ах да, з точною ( «.») Все буде навпаки. Ну і синтаксис.
- отже, символ або групу ми вважали. Тепер перевіряємо оператори «+», «*», «?» І «-«. Для них - особлива функція перевірки regex_match_quantity, а в інших випадках - просто перевіряємо чи потрапляє наступний символ рядка в групу і йде далі (рекурсивно).
Функція перевірки попадання символу в групу проста і легко розширюється:
static int regex_match_group (int c, int group) {if ((group & 0xff) == '.') group ^ = 0x100; if (group <0x100) {/ * a single char * / return c == group; } / * A meta char, like \ d, ... * / switch (group & 0xff) {case 'd': return isdigit (c); case 's': return isspace (c); case 'D': return! isdigit (c); case 'S': return! isspace (c); case '.': return 1; } Return 0; }У цій функції ми дивимося - для простих символів крім точки - просто порівнюємо. Для інших - перевіряємо групи. З точкою - все навпаки. Я використовую функції з ctypes.h, але для простих груп можна і без них обійтися.
Ну і самий кошмарний код в функції regex_match_quantity:
static int regex_match_quantity (int quant, int c, const char * regex, char * s, int * len) {if (quant == '?') {if (regex_match_group (* s, c)) {* len = * len + 1; s = s + 1; } Return regex_match_here (regex, s, len); } If (quant == '+' || quant == '*') {/ * match as much as possible * / char * p; for (p = s; * p! = '\ 0' && regex_match_group (* p, c); p ++) {* len = * len + 1; } If (quant == '+' && p == s) {return 0; } Do {if (regex_match_here (regex, p, len)) {return 1; } * Len = * len - 1; } While (p--> s); } Else if (quant == '-') {/ * match as little as possible * / do {if (regex_match_here (regex, s, len)) {return 1; } * Len = * len + 1; } While (* s! = '\ 0' && regex_match_group (* s ++, c)); } Return 0; }Ну для «?» Завжди повертаємо 1, але якщо символ дійсно входить в групу - збільшуємо довжину збігається регіону на 1.
Для жадібних операторів «+» і «*», які захоплюють якомога більше символів рядка - так і робимо. Перевіряємо скільки взагалі символів рядка може потрапити в задану групу, а потім повертаємося назад поки не знайдемо відповідність залишку рядка залишку регулярного виразу. Для «+» хоча б один символ рядка має потрапити в групу.
Для нежадібні оператора «-» просто йде зліва направо поки не співпаде залишок рядка із залишком вираження.
От і все. У мене вийшло 98 рядків. Жодної залежності. З libc - тільки ctype.h, тобто при особливому вмінні збереться навіть якимось неповноцінним SmallC. Також ніяких особливих булочок З я не використовував, код повинен бути переносимо і на інші мови, на той же Forth.
Весь код - як завжди на bitbucket .
Там же і жменька тестів щоб бути впевненим що все працює як треба.
замість висновків
Спасибі Робу Пайку за елегантний і корисний код. Я розумію, що для серйозних завдань такі мікро-регекспів не годяться, але для чогось простенького в embedded - цілком. Тим більше особисто я вважаю, що не варто робити і регулярних виразів тьюірнг-повний мега-мову, вже краще б був спосіб комбінування простих регулярних виразів з домішкою деякої простої логіки типу умовних-циклів.
Тут до речі теж варто відзначити роботу Роба Пайка про структурні регулярних виразах . І його текстовий редактор Sam барвистий приклад їх простоти і потужності. Спасибі, Роб.
PS Окремо дякую, Роб, за мову Go 🙂
А що якщо написати свій движок регулярних виразів?Ну або як це назвати?
«?
Regex [1] == '*' || regex [1] == '+' || regex [1] == '-' | | regex [1] == '?
», «\ ^», «\ $», '\?