Progrobot: бот довідки з мов програмування

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

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

Так вийшов бот @Progrobot. Йому можна надіслати назву функції і отримати її короткий опис, можна послати назва модуля (в пітоні) або заголовкого файлу (c++) і отримати список всіх функцій в цьому модулі, і т. д. Поки є довідка з c++ (з cppreference) і python3 (з docs.python.org). Ще планував зробити пошук по stackoverflow, але виявилося, що API-шний пошук працює погано, та ще й є жорстке обмеження на кількість запитів — коротше, поки відключив, потім, може бути, выкачаю offline-базу і допилю.

Про власне бота
Дані зберігаю в mongo, на кожен мова дві таблиці. У першій — власне довідка по об'єктах (функцій, класів, модулів і т. д.):«канонічне» ім'я, посилання на сторінку, звідки взята документація, модуль (питоновский модуль або cpp-шний header), до якого належить об'єкт, формат користування (usage), опис, список дочірніх елементів (методів для класу тощо) та рядок copyright. До кожному дочірньому елементу зберігається також його короткий опис, що я брав як перше речення опису цього елемента. (Причому детектування першого речення виявилося теж не зовсім простим завданням.)

У другій таблиці зберігаю індекс: для кожного об'єкта зберігаю його можливі імена, наприклад, для std::vector::push_back в індексі буде лежати «push_back», «push_back vector» і «push_back std vector», з посиланням на довідку в першій таблиці. А саме, розбиваю повна назва об'єкта на токени, беру всі суфікси отриманого списку і для кожного суфікса сортую його токени за алфавітом. Для кожного рядка в індексі може бути кілька документів (наприклад, push_back є не тільки у векторі).

Тепер логіка бота досить проста: розбиваємо запит на токени, сортуємо їх за алфавітом, і шукаємо в індексі відповідний запис. Знайшли — ура, не знайшли — мабуть, такого об'єкта немає. Якщо є кілька відповідних записів, то вибираємо з них найбільш підходящу (я вирішив для простоти вибирати приблизно ту, у якої «канонічне» ім'я містить мінімальну кількість токенів, наприклад, запит «get» поверне std::get, а не який-небудь xml.etree.ElementTree.Element.get). Всі взагалі відповідні записи можна переглянути командою /list.

В базі у мене зберігаються описи в html, щоб зберегти форматування коду і т. п. Телеграм також дозволяє використовувати у повідомленнях деякий просте підмножина html, тому написав конвертор, який викидає непідтримувані теги і розставляє в підходящим місцях переклади рядків. З спецефектів тут — в описах зустрічалися локальні посилання (<a href="#anchor">). Я залишав їх, і все працювало, просто такі посилання не працювали в клієнті телеграма, але і не страшно. У черговий день я виявив, що бот не може відправити майже жодного повідомлення. Мабуть, телеграм додав додаткову перевірку на коректність адрес у посиланнях, і перестав пропускати локальні посилання. Довелося залишати тільки посилання з повноцінним адресою.

Ще довелося трохи повозитися з-за того, що в телеграме довжина повідомлення обмежена 4096 символами (саму константу насилу знайшов у документації телеграму), а опису деяких об'єктів виявляються більше. Додав трохи зарозуміла код, що розрізає довгі повідомлення на більш короткі в підходящих місцях, і команду /cont, щоб отримати продовження. З числа несподіваних приколів тут — я робив так, щоб усі дужки у відрізаній частині повідомлення були збалансовані. А потім натрапив на питоновский модуль random, в описі якого є фраза «...generates a random float uniformly in the semi-open range [0.0, 1.0)». Довелося рахувати квадратні та круглі дужки еквівалентними.

Про парсинг
Парсити html з cppreference виявилося суцільним задоволенням. Одна сторінка на сутність, хороший текст в стилі саме що reference, адекватні класи і id у html-тегів, список дочірніх об'єктів прямо на сторінці, і т. д. Взяв три сторінки в якості прикладів, написав досить простий код з використанням BeautifulSoup, який добре парсил б ці сторінки, і все запрацювало. Потім тільки підкручував за дрібниці; зараз там ще є деякі шорсткості, які руки не доходять виправити, але загалом і в цілому все працює. З нетривіальних подкручиваний було наповнення опису та дочірніх елементів для заголовних файлів (щоб за запитом «algorithm» можна було отримати список всіх функцій в цьому файлі), а також більш акуратна обробка спеціалізацій шаблонів (спочатку std::vector у мене розбивався на токени std vector bool, в результаті чого він знаходиться просто за запитом bool; довелося спеціалізацію викидати перед токенизацией).

А ось парсинг питоновской документації було набагато веселіше. Вона написана книга, яку можна читати підряд. В результаті там перемішані ідеологія, поради щодо використання, приклади, і власне потрібна мені reference, а в довершення всього є фрази-зв'язки типу «The pprint module defines one class:», які ніяк не відрізниш від прямував вище опису самого модуля. Тому, після того, як все запрацювало на трьох сторінках-прикладах, парсинг питоновской документації довелося ще довго допілівать, та й зараз ще є більше проблем, ніж з cpp. Наприклад, ця фраза про pprint так і присутній зараз у відповіді бота, і виглядає там дивно.

З проблем, які довелося фиксить — опису ряду сутностей починаються зі слів «New in version x.x» або «Source code: ...», а я брав перше речення як короткий опис цієї сутності. Не знайшов рішення краще, ніж просто захардкодить, що рядки такого виду не можуть бути коротким описом. У декораторів місцями доводилося обрізати символ @. Початок опису нової сутності визначається тегом, у якого є клас «class» або «classmethod» або «exception» або щось ще, всього 9 варіантів, і я далеко не відразу виявив їх всі (а в cpp кожен файл — це окрема сутність і проблеми немає). Деякі сутності мої скрипт детектировал відразу в двох місцях (модуль unittest.mock детектировался тут і тут). У текстах є таблиці та інші структури, які погано переводяться у формат повідомлення у телеграме (та й не хотілося б їх переводити), за таким структурам лідер — itertools, довелося при виявленні рядка, яка повністю жирним шрифтом, вважати, що опис закінчилося. Нарешті, на docs.python.org дуже складно зрозуміти, яка ліцензія поширюється на власне документацію; мені довелося навіть писати на docs@python.org. Зате тут немає цих проблем зі спеціалізацією шаблонів, а також немає поняття «заголовковий файл» взагалі — для кожного об'єкта однозначно і природно визначено «батьківський».

Про фреймворк
Щоб не смикати Telegram API безпосередньо, я використовую python framework для telegram-ботів telepot. Він багато чого вміє, аж до підтримки бесід з користувачами, і писати на ньому бота виявилося досить просто. Правда, він регулярно оновлюється і має якесь неймовірне кількість варіантів використання, так що досить складно розібратися, який варіант потрібен в конкретному випадку.

Деякою помилкою виявилося те, що різні повідомлення від телеграма мають суттєво різну структуру. У деяких об'єктів є просто поле id, у деяких в назві поля також зазначено, чого це id (message_id або file_id). Чи, наприклад, об'єкт Message є поля chat і text, а в об'єкта CallbackQuery поля chat немає, а замість поля text — поле data. Мені б обробляти Message та Callback взагалі однаково, але це не виходить, доводиться дописувати дрібні хакі. Правда, я писав це на початку літа, а сам фреймворк активно влітку допрацьовувався, може бути, зараз у них вже і краще.

Код
Github: github.com/petr-kalinin/progrobot, код там досить негарний — наслідок моїх численних спроб розібратися з інтерфейсом telepot'а.
Джерело: Хабрахабр

0 коментарів

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