Робота з DSL: створення власного аналізатора з використанням бібліотек Python



В нашому блозі на Хабре ми пишемо не лише про темах, пов'язаних з інформаційною безпекою, але приділяємо значну увагу питанням розробки софта — наприклад, ведемо цикл про створенні та впровадженні інструментів DevOps. Сьогодні ж мова піде про використання предметно-орієнтованих мов (Domain-specific language, DSL) для рішення конкретних завдань за допомогою Python.

Матеріал підготовлений на основі виступу розробника Positive Technologies Івана Циганова на конференції PYCON Russia (слайди, відео).

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



Начебто все добре — список файлів, період ротації теж є, все збігається. Єдина відмінність — в Log Rotate ми можемо вказувати одиниці виміру розміру файлу, а в нашій конфігурації — поки немає. Перша думка — дозволити ставити їх у спеціальній рядку і обробляти її, розбиваючи по прогалині.



Це хороший спосіб, який буде працювати до моменту, поки користувачі нашої системи не попросять дати їм можливість вказувати якусь дельту до використовуваного розміру («1 мегабайт + 100 кілобайт»). Навіщо їм це потрібно, вони не сказали, але ми ж любимо своїх користувачів. Щоб реалізувати таку функціональність, скористаємося регулярними виразами.



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

Що таке DSL
Згідно з визначенням Мартіна Фаулера, предметно-орієнтована мова (DSL) — це мова з обмеженими виразними можливостями, орієнтований на конкретну предметну область.

Існують внутрішні і зовнішні DSL. До перших відносяться такі бібліотеки як PonyORM, WTForm і Django models, а до других SQl, REGEXP, TeX/LaTeX. Внутрішні DSL представляють собою деяке розширення базової мови, а зовнішні — це абсолютно незалежні мови.

У разі розробки внутрішнього DSL для нашого завдання ми можемо створити функцію або константу, яку можна буде використовувати всередині конфігураційного файлу.



Але обмеження, що накладаються базовою мовою збережуться і позбутися при записі від зайвих дужок і знаків множення між числом і змінної (MB, KB) нам ніяк не вдасться.

При використанні зовнішнього DSL ми зможемо самі придумувати синтаксис — це дозволить нам позбутися від непотрібних нам дужок і знаків множення. Але нам доведеться розробити аналізатор нашої мови.

Повернемось до нашого завдання
А що якщо нам потрібно зберігати файл конфігурації окремо від коду? З цим не буде проблем. Просто збережемо наш словник з конфігурацією в YAML-файл і розв'яжемо користувачам редагувати його.

Технічно, цей YAML — вже і є зовнішній DSL, при цьому для нього не потрібні ніякі аналізатори для розбору. Можна його завантажити, використовуючи існуючі бібліотеки, і обробити тільки поле size:



Аналізатори в Python
Поглянемо на те, що в Python є для написання аналізаторів.

Бібліотека PLY (Python Lex-Yacc)

Аналізатор складається з лексичного і синтаксичного аналізаторів. Вихідний текст потрапляє на вхід лексичного аналізатора, завдання якого — розбити текст на потік токенів, тобто примітивів нашої мови. Цей потік токенів потрапляє в синтаксичний аналізатор, який перевіряє правильність їх розташування один відносно одного. Якщо все в порядку, то виконується генерація коду, або його виконання, або побудова абстрактної синтаксичного дерева.

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



Але якщо йому передати семантично неправильну рядок, то ми отримаємо безглуздий набір токенів:



Для того, щоб уникнути зайвої роботи з аналізу правильності розташування токенів нам необхідно описати граматику нашої мови.



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

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



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

Проте зовсім без мінусів не обійтися — поріг входу при початку використання інструменту дуже високий, а аналізатори з використанням PLY виходять дійсно багатослівними.

Бібліотека funcparserlib

Ще один цікавий інструмент для створення аналізаторів — бібліотека funcparserlib. Це комбінатор функціональних парсерів. Розробка аналізатора із застосуванням цієї бібліотеки починається з оголошення токенів у вигляді регулярних виразів. Потім описується сам парсер — задаються примітиви, описуються використовувані операції, які для зручності обробки ще і групуються по пріоритету (множення і ділення/додавання і віднімання).



Тепер потрібно описати всю решту граматику — для цього ми оголошуємо описуємо як будуть виглядати вираження, а потім описуємо пріоритети операцій.



До переваг funcparserlib можна віднести компактність цієї бібліотеки і її гнучкість. З-за цієї ж компактності багато в ній доводиться робити руками — з коробки доступно не так багато можливостей. І так як ця бібліотека є комбінатором функціональних парсерів — вона припаде до душі любителям функціонального програмування.

Бібліотека pyparsing

Ще один варіант для створення аналізатора — бібліотека pyparsing. Відразу поглянемо на код програми:



Ніде не описуються токени, вся увага приділяється відразу кінцевому мови та опису операцій над виразами з урахуванням пріоритетів.

У pyparsing «з коробки» є корисні базові елементи, наприклад методи роботи з пріоритетами — це спрощує код і робить його більш зрозумілим. Крім того, існує можливість розширення функціональності та створення власних компонентів. З іншого боку — інструмент не може похвалитися наявністю якісної документації, а налагоджувати вийшов компактний аналізатор набагато складніше ніж багатослівний аналізатор з використанням PLY.

щодо швидкодії
Поговоримо про швидкості роботи в разі використання кожної з описаних бібліотек. Наш аналіз показує, що при обробці нескладних кейсів, швидше всіх виявляється PLY.



У ході тестів ми «згодували» всім аналізаторам завдання складання всіх чисел від нуля до 9999. Ось який результат в мілісекундах показали кандидати:



Повідомлення про помилки
Не варто забувати, що ми писали наш аналізатор для того, щоб розпарсити одне з полів в конфіги системи ротації логів. Очевидно, що якщо в роботі аналізатора виникнуть якісь помилки, користувачеві необхідно повідомити про це в зрозумілому форматі — якому рядку, на якій позиції і що саме пішло не так.

Ще один плюс PLY — в бібліотеці є вбудований механізм обробки помилок, що виникають на етапах лексичного та синтаксичного аналізу. При цьому не втрачається стан парсера — після помилки можна спробувати продовжити роботу



Що в підсумку вибрати
Кінцевий вибір інструменту створення парсера залежить від поставлених завдань і умов їх виконання. Можна виділити ряд таких комбінацій:

  • Якщо потрібно швидко все описати, а швидкодія не головне — цілком підійде pyparsing.
  • У разі, якщо ви любите функціональне програмування, а швидкодія також не дуже важливо — очевидним вибором буде funcparserlib.
  • Але якщо швидкість роботи найважливіше і також хотілося б описувати всі правила «як годиться» за підручниками — звичайно, потрібно вибрати PLY.
Якщо існує можливість обробки даних засобами самої мови — варто так і зробити, або використовувати регулярні вирази. У більш складних випадках є сенс почати з внутрішнього застосування DSL, а якщо цей варіант не підходить — почати використовувати готові мови для структурування даних (Yaml, Json, XML). Писати власні аналізатори слід в крайніх випадках, коли нічого з перерахованого вище не дозволяє вирішити задачу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.