Когда пишешь код, регулярно бывает нужно посмотреть справку по конкретной функции, модулю и т.д. Обычно я для этого захожу на 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’а.
Поделиться с друзьями
-->

Комментарии (14)


  1. Kondra007
    16.09.2016 11:53
    +1

    Попробуйте на замену telepot'у модуль pyTelegramBotAPI. Позволяет избавиться от кучи if-ов, заменив всё отдельными (и читаемыми!) декорированными функциями. Использую его с августа прошлого года, проблем не знаю.
    Пример кода, мне кажется, более "Pythonic-way"


    P.S. Бот интересный, спасибо. Фич-реквест: сразу выдавать ссылку на официальную документацию. Т.е., бегло ознакомился через бота — пошёл, прочитал на сайте.


    1. pkalinin
      16.09.2016 11:56

      Спасибо, но, кажется, он более низкоуровневый. telepot, например, умеет поддерживать нити беседы, просто создавая по объекту на нить. Например, я так обрабатываю команды /cont и /list. В pyTelegramBotAPI придется это делать вручную.


      1. Kondra007
        16.09.2016 12:00

        У pyTelegramBotAPI тоже есть подобная штука, называется next_step_handler, хотя, если честно, никогда его не использовал; вместо этого строю маленькие конечные автоматы.


        Не подскажете, где почитать про нити беседы в telepot?



  1. nikolay_karelin
    16.09.2016 16:01

    К парсингу питоновской доки: а не думали парсить их указатель (очень неплохо сгенерирован, в CHM искать материал достаточно просто).


    Здесь: https://docs.python.org/3/genindex-all.html


    1. pkalinin
      16.09.2016 16:06

      Не очень понял, что вы имели в виду про CHM, по ссылке я вижу обычный индекс. Я его действительно не видел, и, возможно, с ним было бы проще. Сейчас уже не будет проще, т.к. он дает только ссылки на разделы по каждой функции, а я их и так детектирую по тегам с нужными классами.


      1. nikolay_karelin
        16.09.2016 18:26

        Я когда пользуюсь питонской докой в формате CHM, то именно этого индекса (точнее, его содержимого) в большой части ситуаций хватает, чтобы найти нужный раздел.


        Еще один вопрос: а можно ли ваш парсер использовать, чтобы документацию от других проектов из экосистемы питона искать?
        (в первую очередь интересуют NumPy / SciPy / Matplotlib)


        Как правило, для приготовления документации используется пакет Sphinx, поэтому теги (но и проблемы) будут более-менее одинаковыми.


  1. deespater
    16.09.2016 19:03

    https://kapeli.com/dash


    1. JIghtuse
      16.09.2016 19:19
      +1

      Ну и Zeal туда же.


      1. pkalinin
        16.09.2016 22:03

        Интересно, да, не знал. Преимущество телеграма — он у меня и так все время запущен.


    1. pkalinin
      16.09.2016 22:03

      Если я правильно понял, оно только под mac?


  1. sergio_deschino
    16.09.2016 20:39

    Спасибо! отличная вещь, как для дальнейшей доработки, так и для демонстрации возможностей бота!


  1. Saffron
    16.09.2016 21:17
    +1

    Отлично. Осталось только научить бот отдавать форматированный текст — и мы наконец-то получим веб как он задумывался, а не как его сломали.