Pull to refresh

Progrobot: бот справки по языкам программирования

Reading time 5 min
Views 15K
Когда пишешь код, регулярно бывает нужно посмотреть справку по конкретной функции, модулю и т.д. Обычно я для этого захожу на 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’а.
Tags:
Hubs:
+14
Comments 30
Comments Comments 30

Articles