Привет, Хабр!

Несколько лет назад я начал разрабатывать свой агрегатор контента, что бы упростить свой серфинг в сети. Изначально я парсил только rss, vk и facebook, но в прошлом году решил сделать полный рефакторинг проекта: отказаться от парсинга на клиенте, сделать нормальный back-end, использовать базу данных для хранения данных и расширить список поддерживаемых ресурсов.

Помимо стандартного набора из rss, fb, vk, twitter, instagram, youtube я добавил поддержку произвольных открытых каналов из telegram.

image

Под катом пошаговая инструкция, как парсить любые каналы в telegram без регистрации и смс.

Изначально я предполагал, что парсить каналы можно через популярный BotApi, на который имелось много инструкций в сети. Но оказалось, что для того, что бы бот мог читать какой-то канал — бота надо в этот канал добавить. Для сторонних каналов этот вариант не возможен. Я перешел к чтению мануалов на основной API телеграма.

Через 30 минут изучения документации я был в отчаянии. Все данные у телеграма шифруются, что бы получить что то от их серверов нужно обладать степенью магистра по криптографии… А еще вместо http запросов используется socket, с которым я ранее не сталкивался. Вообщем чистый хардкор и никаких внятных примеров в сети… Это было почти фиаско.

Последней надеждой было найти какое-то готовое решение. И тут, наконец, удача мне улыбнулась. На сайте telegram я наткнулся на ссылку на неофициальный opensource php клиент. Да-да! Можно использовать telegram под php, и там даже есть поддержка звонков! Это чудо называется madelineProto. Оно может подключаться к серверам используя криптографическую магию и отдавать нужные мне данные в виде нормального, человеческого ассоциативного массива.

Я приступил к настройке php клиента.

1. Регистрация своего клиента.

К сожалению, в начале поста я вас обманул и нам всеже потребуется регистрация и смс авторизация в телеграме…

image

Если аккаунт в телеграме уже есть, остается зарегистрировать свое приложение/клиент, и получить ключи для доступа к серверам telegram.

Это стандартная процедура, похожая на аналогичную у соц. сетей для доступа к API. Инструкция для создания своих ключей.

После регистрации клиента нам потребуются только «App api_id» и «App api_hash» со страницы my.telegram.org/apps

2. Установка madelineProto.

Для работы требуется php7, но в Readme написано, что есть способ запустить на php5.6.

С запуском на MacOs с php7 из пакета Mamp, и простеньком хостинге за 150 руб в мес проблем не возникло.

Процесс не хитрый: скачать релиз, установить зависимости через composer и можно приступать к настройке.

Для уменьшения размера я удалил лишние зависимости и оставил только danog, paragonie и phpseclib. На работе клиента это никак не сказалось.

3. Настройка madelineProto и первый запуск.

Все примеры по использованию и настройке описаны в репозитории клиента, но я приведу свой код с комментариями.

Сначала необходимо запустить клиент в режиме создания ключей.

На этом этапе потребуется авторизовать новое подключение и ввести код верификации, который придет в ранее авторизованный telegram клиент. Запускать код необходимо из консоли, так как в процессе работы скрипта необходимо будет вводить код авторизации. В случае перезапуска скрипта код будет меняться.

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

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

            set_time_limit(60); //Убедитесь что ваш сервер может работать минимум 60 секунд, так как для первой настройки и создания ключей требуется много времени.
require_once ROOT_DIR.'/libs/MadelineProto/vendor/autoload.php';

//мой список настроек минимален, остальные - по умолчанию. Cписок всех значений по умолчанию можно посмотреть в ReadMe клиента на github.
            $settings = [
                            'authorization' => [
                                'default_temp_auth_key_expires_in' => 315576000, // я установил 10 лет, что бы не авторизовывать приложение повторно.
                            ],
                            'app_info' => [ // Эти данные мы получили после регистрации приложения на https://my.telegram.org
                                'api_id'          => XXXXX,
                                'api_hash'        => XXXXXXXXXX
                            ],
                            'logger' => [ // Вывод сообщений и ошибок
                                'logger' => 3, // выводим сообещения через echo
                                'logger_level' => 'FATAL ERROR', // выводим только критические ошибки.
                            ],
                            'max_tries' => [ // Количество попыток установить соединения на различных этапах работы. Лучше не уменьшать, так как телеграм не всегда отвечает с первого раза
                                'query' => 5, 
                                'authorization' => 5,
                                'response' => 5,
                            ],
                            'updates' => [ // Я обновляю данные прямыми запросами, поэтому обновления с каналов и чатов мне не требуются.
                                'handle_updates' => false, 
                                'handle_old_updates' => false,
                            ],
            ];
            
            $MadelineProto = new \danog\MadelineProto\API($settings);
            
            $MadelineProto->phone_login(readline('Enter your phone number: ')); //вводим в консоли свой номер телефона
            $authorization = $MadelineProto->complete_phone_login(readline('Enter the code you received: ')); // вводим в консоли код авторизации, который придет в телеграм
            if ($authorization['_'] === 'account.noPassword') {
                throw new \danog\MadelineProto\Exception('2FA is enabled but no password is set!');
            }
            if ($authorization['_'] === 'account.password') {
                $authorization = $MadelineProto->complete_2fa_login(readline('Please enter your password (hint '.$authorization['hint'].'): ')); //если включена двухфакторная авторизация, то вводим в консоли пароль.
            }
            if ($authorization['_'] === 'account.needSignup') {
                $authorization = $MadelineProto->complete_signup(readline('Please enter your first name: '), readline('Please enter your last name (can be empty): '));
            }
            $MadelineProto->session = 'session.madeline';
            $MadelineProto->serialize(); // Сохраняем настройки сессии в файл, что бы использовать их для быстрого подключения. 

После успешной авторизации код выше можно удалить, он больше не потребуется.

В корне проекта будут создан файл «session.madeline», в котором, в бинарном виде, будут храниться данные нашей сессии. Для смены настроек клиента надо либо создавать новую сессию, либо попытаться править этот файл в бинарном редакторе. С учетом того, что количество попыток авторизаций в день у телеграма ограничено, рекомендую подбирать настройки с умом, что бы не пришлось их часто менять.

Теперь мы можем использовать быстрое возобновление сессии, поэтому пишем новый код.

4. Получение постов из произвольного открытого telegram канала.

Возобновление сессии работает довольно быстро. У меня на получение данных уходит 2-4 секунды, так что set_time_limit нам больше не требуется.

            require_once ROOT_DIR.'/libs/MadelineProto/vendor/autoload.php';
            
            $MadelineProto = new \danog\MadelineProto\API('session.madeline');
            
            $settings = array(
                    'peer' => '@'.$val['url'], //название_канала, должно начинаться с @, например @breakingmash, все остальные параметры, кроме limit, можно оставить равными 0
                    'offset_id' => $val['offset_id']?:0, 
                    'offset_date' => $val['offset_date']?:0, 
                    'add_offset' => $val['add_offset']?:0, 
                    'limit' => $val['limit']?:10, //Количество постов, которые вернет клиент
                    'max_id' => $val['max_id']?:0, //Максимальный id поста
                    'min_id' => $val['min_id']?:0, //Минимальный id поста - использую для пагинации, при  0 возвращаются последние посты.
                    'hash' => 0
             );

             $data = $MadelineProto->messages->getHistory($settings);
             

Так как у меня обновляется много каналов за один раз, то имеет смысл использовать одну и туже сессию, а не тратить по 2 секунды на каждый канал.

Финальный код выглядит следующим образом:

        if (!is_array($url)){
            if (mb_strpos($url,',')!==false){
                $url = explode(',',$url);
            }else{
                $url = [$url];
            }
        }

        if (!empty($url)) {
            require_once ROOT_DIR.'/libs/MadelineProto/vendor/autoload.php';
            $file_contents = [];
            foreach ($url as $val){
                
                if (!is_array($val)){
                    $val = array(
                       'url' => $val
                    );
                }
                
                $settings = array(
                    'peer' => '@'.$val['url'], 
                    'offset_id' => $val['offset_id']?:0, 
                    'offset_date' => $val['offset_date']?:0, 
                    'add_offset' => $val['add_offset']?:0, 
                    'limit' => $val['limit']?:10, 
                    'max_id' => $val['max_id']?:0, 
                    'min_id' => $val['min_id']?:0, 
                    'hash' => 0
                );

                $file_contents[$val['url']] = $MadelineProto->messages->getHistory($settings);
            }
        }

После выполнения мы получаем массив с нужным нам количеством сообщений/постов, разбитых по каналам. Так же передаются данные о медиа-вложениях.

Дальше остается сохранить текст поста, при наличии фото/видео получить превью и подпись к медиа файлу и сформировать ссылку для просмотра поста.

4. Получение медиа-вложений.

К счастью, с недавнего времени, telegram внедрил html превью постов, поэтому можно не сохранять на свой сервер бинарные данные, полученные из клиента, а просто взять ссылку на фото и ведео, хранящееся на серверах телеграма.

По названию канала и id поста формируем ссылку формата: t.me/НАЗВАНИЕ_КАНАЛА/ID_ПОСТА?embed=1, например t.me/breakingmash/4193?embed=1

Ну а дальше все просто:

private function telegram_media_parse($posts_data, $source){
        include_once(ROOT_DIR.'/libs/phpQuery.php'); //для парсинга html использую очень быструю  и удобную библиотеку phpQuery

        foreach ($posts_data as &$post_data) {
            if (!empty($post_data['media'])){
                $file_contents = self::loader($post_data['post_url'],'site');//Через curl получаем html код страницы поста.
                $document = phpQuery::newDocumentHTML($file_contents); //Формируем dom-дерево из html строки

                $post_data['post_image'] = preg_replace('/[\s\S]*background-image:[ ]*url\(["\']*([\s\S]*[^"\'])["\']*\)[\s\S]*/u','$1',$document->find($source['rules']['post_img_path'])->eq(0)->attr('style')); //адрес изображения хранится в background-image свойстве.
                $post_data['post_description'] = $document->find($source['rules']['post_text_path'])->eq(0)->text(); //Получаем caption медиафайла. 
            }
            unset($post_data['media']);
        }
        unset($post_data);
        
        return $posts_data;
}

На этом парсинг закончен и можно сохранять посты в базу или вывести на странице.

Надеюсь, что мой первый пост будет кому нибудь полезен. Ссылку на свой агрегатор не оставляю, так как не уверен, разрешено ли это.

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


  1. chezzy
    26.02.2018 15:10
    +1

    Здесь всё великолепно: от ликования про то, что можно работать с Telegram через PHP до инклудинга «очень быстрой» библиотеки phpQuery (особенно про память, которую она сжирает) в функцию.

    Это было почти фиаско.


    Согласен.


    1. xtrime Автор
      26.02.2018 15:43

      Спасибо за комментарий. Сарказм уважаю, но согласен не со всеми замечаниями :)

      Ликование у меня уместно, потому что 3 месяца назад в сети я вообще не нашел никаких примеров, как же, все-таки, парсить каналы telegram. Ну и читать сухие посты без эмоций не так интересно :)

      Да, код у меня не самый лучший , но он мой и я его люблю, но пока все работает очень быстро и нагрузка на php и mysql на сервере почти нулевая. На парсинг 100+ источников уходит около минуты, и большая часть этого времени — это ожидание ответов серверов.

      Насчет скорости phpQuery тоже не пойму упрека. Сначала я попробовал Ganon, но он оказался в разы, а то и на порядки медленнее (да, в 10 раз). Кроме того phpQuery прекрасно работает не только с html, но и с xml, что очень удобно для парсинга RSS. По цифрам: парсинг крупных страниц, типа пикабу занимает доли секунды, точные цифры не вспомню, но ожидание ответа сервера длится намного дольше, чем сам парсинг. Так же я проверяю изменился ли hash html/xml строки перед тем, как ее парсить и искать дубликаты в базе. И подсчет md5 строки получается не намного быстрее, чем парсинг через phpQuery этой же строки.

      Если вы знаете как парсить быстрее и так же удобно (привычный синтаксис селекторов это большой плюс), буду признателен за наводку.

      Насчет include phpQuery внутри функции — да, решение не очень оптимальное, но функция находится внутри API, который вызывается часто, поэтому делать include в общем коде тоже не очень хорошо, а phpQuery требуется редко (раз в 5 минут). А если функция не запускается, то и include не срабатывает — это я проверил в документации php. Так что память будет расходоваться только при необходимости. Но все же, соглашусь, что надо этот момент еще раз подумать. Может быть вынести парсинг в отдельное API…


  1. medvedevia
    26.02.2018 18:46

    У меня вопрос: правила телеграмм не запрещают хранить сообщения на своём сервере? А удаленные сообщения?


    1. xtrime Автор
      26.02.2018 20:43

      Это же общедоступная информация, которую можно получить просто из браузера даже без аккаунта в телеграме (в статье есть пример ссылки на пост из mash). Это не личная переписка. Так что, думаю, что никакой конфиденциальности я не нарушаю. Да и пользователей пока человек 20-30 от силы, так что проект слишком маленький для разбирательств :)


      1. medvedevia
        26.02.2018 21:53

        Я ни в чем не обвиняю, просто я тоже хотел хранить сообщения, а вот тут в п. 1.4. написано:

        preventing self-destructing content from disappearing

        Вот и думаю, это относится ко всем удаленным сообщениям, либо это касается секретных чатов, которые могут самоуничтожаться?


        1. xtrime Автор
          26.02.2018 22:19

          Мне кажется, что эти правила вообще не касаются парсеров.

          We welcome all developers to use our API and source code to create Telegram-like messaging applications on our platform free of charge. In order to ensure consistency and security across the Telegram ecosystem, all third-party client apps must comply with the following Terms of Service.


          It is forbidden to interfere with the basic functionality of Telegram.


          Т.е. смысл такой: если пользователь на моем сайте вводит свой логин и пароль от своего телеграма и начинает использовать мой сайт, как клиент телеграма: читать секретные чаты, и его чаты хранятся у меня в базе и не удаляются — то это запрещено. Но пользователь, то не использует мой сайт как клиент телеграма, я не создаю «third-party client apps». Я использую клиент телеграма и свой аккаунт, что бы получать нужные мне данные и делюсь этими общедоступными данными со своими пользователями. Мои пользователи при этом не являются пользователями телеграма.

          Так что не вижу никакой разницы с тем же vk api c точки зрения закона. В vk я тоже иногда парсю какой нибудь пост, а его потом удаляют. Но я то у себя его храню (впрочем как и google cache).


  1. medvedevia
    26.02.2018 18:49
    +1

    Для таких случаев они выпустили core.telegram.org/tdlib


    1. xtrime Автор
      26.02.2018 20:51

      Хм, интересно! Но что то по php примеров нет, и ответы на github не воодушевляют…

      image

      Вообще мне с телеграмом работать было очень сложно. Особенно если сравнивать с vk api… Надеюсь что, в будущем, с ростом популярности, они упростят жизнь сторонних разработчиков, но пока что-то все СЛОЖНА, на мой взгляд.


  1. n_demitsuri
    27.02.2018 13:24

    А можно как-нибудь читать телеграм-каналы через rss? Нигде не попадалось.


    1. xtrime Автор
      27.02.2018 15:05

      Сервисов, что бы читать телеграм каналы где то кроме телеграма вообще почти нет, поэтому я пост и запилил.

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

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