Периодически, чтобы не покрыться пылью, я стараюсь создавать интересные штуки, которые смогли бы облегчить чью-то жизнь. Я стремлюсь к тому, чтобы они были, полезнее, чем социальная сеть для кошек. Один из недавних примеров — Телеграм-бот, который позволяет в указанных координатах найти известные Wi-Fi-точки и посмотреть пароли к ним.
Этот раз не стал исключением и я задумал создать бота, который позволял бы с наибольшим комфортом и минимумом усилий смотреть любимые фильмы и сериалы, да еще и предоставлял контент в нескольких вариантах озвучки. Сказано — сделано. И теперь, когда железный друг человека радостно раздает пользователям их любимые шоу, я бы хотел поговорить о том, что сопутствовало созданию бота, какие проблемы вставали на моем пути и как они были решены. В первой главе я расскажу о Go глазами PHP-разработчика, во второй главе о поиске дзена для парсинга Кинопоиска, а в третьей — о недокументированной фиче Telegraph.
1. $alexander->useLanguage(GOLANG);
Меня зовут Александр, мне 21 год. Я занимаюсь веб-разработкой и чаще всего пишу на PHP.
Не могу сказать, что PHP — язык мечты. У него, как и у любого другого языка есть сильные и слабые стороны. Однако, я стал замечать, что устаю от PHP — постепенно надоедает разработка на этом языке, его детские болячки вроде похожих функций, которые принимают похожие аргументы, но в разном порядке, не всегда предсказуемого поведения и, конечно, слабой типизации. Таким образом, для очередного продукта я решил использовать Golang. На момент, когда я начинал, я знал о нем вот что:
- Строгая типизация
- Не очень много ключевых слов
- Горутины — это удобная параллельность из коробки
- Говорят, что язык прост и предсказуем
Так же, я когда-то от скуки листал Golang-book. Сначала все было весьма непривычным… Ну, первые 3-5 часов. Да, вход в язык очень прост. Отсутствие магии и обилия ключевых слов, а так же, предсказуемое поведение делают свое дело — если вы уже знакомы с каким-либо языком программирования, наверняка погружение в Go не займет много времени. Тут важная ремарка: Если вы три года пишете одностраничники, и на этом опыт заканчивается, я забираю свои слова обратно. Предсказуемость языка и строгая типизация позволяют писать очень большое количество кода не компилируя бинарник для запуска и проверок. Безусловно, есть и ошибки рантайма, но после PHP это глоток свежего воздуха — понимаешь, сам ошибся, да и ошибка не очевидная.
C организацией кода в Golang все просто: «Вот тебе директория, заодно это и неймспейс, кстати. Держи все здесь». И… Это работает. Это настолько просто в разработке и поддержке, что слезы счастья наворачиваются сами собой. Если честно, я не знаю, насколько большой проект можно создать с таким подходом. Я заглядывал в репозитории нескольких крупных библиотек — выглядит вменяемо, но про поддержку рассказать не могу. Субъективно, одинаковую по размеру кодовую базу на PHP поддерживать сложнее, чем на Go.
Справедливости ради, удобная и очевидная работа с массивами (слайсами) — это не про Go:
//...
s.KeyBoard = [][]string{}
s.KeyBoard = append(s.KeyBoard, []string{})
s.KeyBoard[0] = append(s.KeyBoard[0], s.text.GetAction(locale, "view-prev"))
//...
С точки зрения Golang все выглядит логично, но с человеческой точки зрения — слегка странно. Подробнее эта тема раскрыта здесь.
Так же, для параллельной работы в Golang используются горутины (потоки), в то время, как в PHP принято использовать форки (процессы). В моем проекте не так много мест, где я смог применить горутины. Однако, там, где они используются, это выглядит настолько логично и просто, что возвращаться к форкам совсем не хочется. Так как форки — это независимые друг от друга процессы, для их общения между собой обычно используют третью сторону: Redis или Memcache. Аналогичная проблема в Golang решается с помощью каналов — части языка, которая доступна из коробки. Только вдумайтесь! Параллельная работа из коробки, да еще и с поддержкой синхронизации. Раньше мне такое даже не снилось. Я не считаю, что требую от PHP слишком многого, ведь задачи параллельной работы в современной backend-разработке — обычное дело. Так же, я не хочу сказать, что Golang является панацеей от всех проблем человечества, но после опыта разработки на PHP, решение аналогичных задач на Go кроме результата доставляет еще и удовольствие.
2. Alexander.NeedInfo()
В какой-то момент, API, которым я пользовался для получения информации о фильмах с Кинопоиска закончился.
Было принято решение писать собственный парсер Кинопоиска (ребята из команды Кинопоиска, не кидайтесь тапками, лучше сделайте публичный API).
v1 — Одинокий герой
Первая реализация была простой и в лоб — в проекте поселился одинокий PHP-скрипт, который при обращении к нему доставал из очереди адрес случайного прокси-сервера и через него отправлялся за фильмом на Кинопоиск. Сам разбор страницы происходил тоже на PHP. Из-за того, что одинокий герой не использовали куки, Кинопоиск банил (начинал показывать капчу) каждый адрес после единственного запроса, а ведь еще не все прокси-сервера были быстрыми.
Казалось бы, реализовать поддержку куки, да и дело с концом. Однако, я заметил, что даже с поддержкой куки Кинопоиск показывает капчу моему парсеру чаще, чем мне в браузере. Я решил не исследовать защиту Кинопоиска от парсинга более детально, так как понимал, что это начинает пахнуть выполнением js-кода на клиенте.
v2 — Полноценный клиент
Следующая версия парсера представляла собой веб-сервер на Go, который по GET-запросу запускает PhantomJS с нужными параметрами и переданным ID фильма. Это работало. Мне больше не были нужны прокси-сервера, я ходил на Кинопоиск прямо со своего IP. У меня была поддержка сессий, полноценный браузер и, в целом, все было удобно. Но это было очень медленно. PhantomJS честно ждал, пока загрузится вся статика и выполнится весь необходимый JS-код. Кроме того, что это было медленно, это было очень дорого по ресурсам. На разбор одной страницы уходило 100-150mb RAM. Поводом для выстрела в голову этой версии послужила прожорливость PhantomJS и его нестабильная работа — например, его процессы не всегда завершались, оставаясь висеть в запущенных и не освобождая после себя память. Я пробовал разные версии PhantomJS, я пробовал завершать процессы за ним с помощью веб-сервера, который инициирует его запуск, но итог всегда был одним: Да, работает, но прожорливо и нестабильно, хотя, конечно, удобно.
v99
В процессе поиска Святого Грааля для парсинга Кинопоиска я сбился со счету, сколько версий разных парсеров и их модификаций я успел создать. В итоге, очередную версию я назвал девяносто девятой. Девяносто девятая версия была написана на PHP. Я использовал Guzzle (HTTP-клиент для PHP), поддерживал сессии и старался быть максимально похожим в своем поведении на браузер пользователя. От поддержки JS я отказался. Капчи, конечно, показываются, но намного реже, чем в первой версии парсера и, в принципе, этот вариант можно назвать комфортным. На этой версии я и остановился.
Так же, мне известно, что по запросу Кинопоиск может предоставить доступ к своему API, но я не рассматривал этот вариант: даже если бы мне открыли доступ, это могло стать потенциальной точкой отказа, ведь доступ в любой момент можно отобрать.
3. Video.Publish()
После войны с Кинопоиском я оказался в ситуации, когда я был готов отдать пользователю ссылку на видео, а воспроизвести его было негде: Telegram Bot API не предоставляет удобного функционала для показа видео по ссылке, а регистрировать домен, хостить что-то кроме бота с парсером и заниматься разработкой фронта я совершенно не хотел.
Что же делать?
Будем публиковать видео где-то. Немного подумав я решил, что Телеграф может вполне сойти за «где-то». Сайт, который де-факто используется для публикации статей из Телеграм? То, что надо! Одна беда — нельзя публиковать видео по ссылке (кроме YouTube или Vimeo).
А если поискать?
Глядя на то, как легко и динамично создаются блоки на странице, а по нажатию лишь одной кнопки публикуется статья, невольно задумываешься: А как это работает? Особенно, если ищешь место для публикации контента. Я решил это узнать.
[{
"tag": "p",
"children": ["Story"]
}, {
"tag": "p",
"children": [{
"tag": "br"
}
]
}, {
"tag": "figure",
"children": [{
"tag": "div",
"attrs": {
"class": "figure_wrapper"
},
"children": [{
"tag": "img",
"attrs": {
"src": "/file/a2e8087fbc53679c14fa1.jpg"
}
}
]
}, {
"tag": "figcaption",
"children": ["Pff"]
}
]
}, {
"tag": "p",
"children": [{
"tag": "br"
}
]
}
]
POST-запрос на публикацию содержит JSON, который подозрительно похож на HTML-разметку. А давайте попробуем добавить тег video, согласно структуре, которую имеем? А давайте. Немного терпения и получаем такую…
[{
"tag": "p",
"children": ["Story"]
}, {
"tag": "p",
"children": [{
"tag": "br"
}
]
}, {
"tag": "figure",
"children": [{
"tag": "div",
"attrs": {
"class": "figure_wrapper"
},
"children": [{
"tag": "img",
"attrs": {
"src": "/file/a2e8087fbc53679c14fa1.jpg"
}
}
]
}, {
"tag": "figcaption",
"children": ["Pff"]
}
]
}, {
"tag": "p",
"children": [{
"tag": "video",
"attrs": {
"src": "https://www.w3schools.com/html/mov_bbb.mp4"
}
}
]
}
]
Если выполнить POST-запрос на редактирование с приведенной выше структурой, то, в публикацию добавится произвольное видео по ссылке. То, что надо.
Не тут то было
Все работает и с этим проблем нет. Беда в том, что большинство атрибутов не поддерживаются, а это значит, что о субтитрах, или, например, постере для видео можно забыть. То есть, получилось решение из разряда «скажи спасибо, что вообще есть». Недолго думая, я решил использовать XSS для того, чтобы иметь возможность настраивать плеер. Наверное, где-то в этом месте нормальная разработка заканчивается, но отступать было некуда: нужно было организовать публикацию видео. Я пробовал разные варианты внедрения стороннего кода в страницу, даже через картинку, но все было тщетно и Телеграф героически выстоял. Впрочем, я и не эксперт в области ИБ. Возможно, если бы я потратил больше времени, то смог бы найти рабочий вариант XSS для Телеграф, который я бы использовал исключительно для кастомизации плеера, однако, я оставил эту затею. Я пробовал еще несколько площадок для публикации своего контента, но везде чего-то не хватало. И, все таки, пришлось реализовывать плеер на своей стороне…
P.S. Если эту статью читают разработчики Телеграф: Пожалуйста, добавьте публикацию видео по ссылке в интерфейс, раз такой функционал доступен.
P.P.S. У меня нет цели развязать холивар «GO vs PHP», я лишь поделился своими наблюдениями, как PHP-разработчик.
Автор: Narrator69