Утилита для парсинга html Ютуба и скачивания комментариев к видео. Здесь будет история написания в обратном хронологическом порядке.
Программа эмулирует поведение браузера в части загрузки комментариев. А именно, тот момент, когда мы пролистываем очередные прогруженные 20 комментов, или нажимаем "показать ответы". В этот момент делается ajax-post-запрос на эндпойнт "/youtubei/v1/next", этот запрос содержит в себе контекст текущей сессии и некий хеш-идентификатор текущей операции "пролистывания" (в YT API этот "объект пролистывания" называется continuation). Получаем ответ, который содержит собственно, очередные <=20 комментариев, а также массив этих самых continuation'ов, на каждый из которых мы тоже будем делать такой же ajax-запрос, после того как спарсим комментарии. И так пока не перестанут приходить новые continuation'ы.
Кстати, первичные "континуэйшены", а также первые 20 комментов (или меньше) мы берем из html страницы, которая изначально к нам пришла. Оттуда же берём данные для контекста сессии (поиск по "INNERTUBE_CONTEXT":"), а также api-токен "INNERTUBE_API_KEY" (но кстати почему-то всё работает и без него)
Рефактор снова откладыввается: мне некогда, но вернулся сюда, потому что понадобились субтитры.
На странице при нажатии на show transcript отсылается ajax-запрос на эндпойнт "/get_transcript?prettyPrint=false", с параметрами context: {сюда идет наш INNERTUBE_CONTEXT} и params:{сюда хэш унакальной операции,который можно взять из того же DATA по JsonPath "$.engagementPanels.engagementPanelSectionListRenderer.content.continuationItemRenderer.continuationEndpoint.getTranscriptEndpoint" }
впринципе моржем использовать тот же ajaxRequest(),но название проперти отличается, не будем ломать старый метод и скопируем в новый с params вместо continuation в body запроса. Ну и эндпоинт поменяем
в ответ приходит куча ненужной инфы, нас интересует JsonPath до массива $.actions .updateEngagementPanelAction .content .transcriptRenderer .content .transcriptSearchPanelRenderer .body .transcriptSegmentListRenderer .initialSegments[]
там мы берем JsonArray и для каждого элемента берем JsonPath $.transcriptSegmentRenderer .snippet .runs[1] .text
Так. оказывается, если автор добавил разделы с таймкодами (например, "введение", "часть 1" и т.д),то к ним в ответе немного другой путь,но все они лежат в том же самом массиве. Разобъём JsonPath на 2 части - до массива и после, и вторая часть будет отличаться. итерируемся по массиву, проверяем keySet на containsKey, выводим заколовки и сырой текст. Работает!
Но теперь проект еще больше нуждается в рефакторинге: тут-то я вообще всё в main() накидал :/
Косяков много, например сразу бросается в глаза повторный перебор элементов json, чтобы найти одельно все "reloadContinuationItemsCommand" и "appendContinuationItemsAction". Хорошо бы каждый ajax-ответ парсить только один раз. И вообще этот jsonSearch() надо упразднить, заменив его тремя узкоспециальными методами, которые будут брать данные по заранее известному пути в ответе, вместо того чтобы три и более раз делать поиск по огромному json'у в 15тыс строк. Сделаем отдельный метод, который будет вызываться один раз после того, как придёт ajax-ответ, чтобы его распарсить и наполнить всё что нам надо - actions и mutations.
Можно найти еще с десяток подобных проблем. Но на самом деле это не главное. Основная проблема производительности в том, как делаются ajax-запросы. Большое кол-во запросов (этого мы изменить не можем) и долгое ожидание ответа очень сильно вилияют перфоманс. Янаписал небольшой колхозный бенчмарк, и вот результаты по выполнению фофч-запросов при разных условиях подключения к интернету (и для сравнения результаты ping до сайта ютуба)
|Моб интернет |wi-fi оптика |оптика ethernet
ping youtube.com | 180-900 | 125 | на 2мс меньше..
мин время | 294 | 171 | -
макс время | 2052 | 841 | -
сред время | 536 | 213 | -
кол-во запросов = 111
В общем, надо пойти путём многопоточности запросов, чтобы не ждать пока придет ответ, а к примеру, спустя 50мс отправлять следующий запрос в отдельном потоке, и в нем же обрабатывать ответ, который придет еще спустякакое-то время. а мы уже ктому времени 10 новых тредов создали и 10 запросов отправили,вот так.. Вообще для этого есть webFlux, но чтобы им пользоваться, надо сперва переехать на Спринг. Ничего против я не имею, особенноесли в будущем это будет сервисом.
Видимо, для этого надо еще довольно много порефакторить. Например, вынести запрос с параметрами в отдельный класс, чтобы webFlux создавал его инстансы. Возможно даже начать новый Спринг-бутовый проект?
Надо бы порефакторить и подготовить побольше бинов, но вот встал такой интересный вопрос: Вот у нас цикл while(continuations), и в каждой итерации мы делаем ajaxRequest(), но перед этим делаем continuations.remove() Вопрос в том, если axajRequest() станет многопоточным - сможем ли мы адкеватно итерироваться по continuations? На вервый взгляд кажется, что в данном виде нет. Но надо бы поисследовать. Но исследовать в таком виде сложно, потому что по прежнему гоняем jsonы и ищем по ключам,довольно сложно все это удержать в голове. Надо сделать как можно больше абстракций. Самое простое - Continuation....ладно,это всё еще надо подумать.
На даннный момент скорость оставляет желать лучшего :/ Ёще бы - гоняем туда-сюда json'ы по нескольку раз. По-хорошему, надо выделить сразу все константы из html и первоначальные данные, оптимизировать поиск по json (особенно ответ на ajax-запрос парсится много раз), потом переписать всё на Мапы, но начнём с другого. Чтобы было удобнее улучшать код, сперва порефакторим. Очень хочется, раз уж пишем новый класс, написать всё сразу правильно и оптимизированно, но надо быть последовательными, и не сломать код, пока не написаны тесты. Так что, скрепя сердце, вооружимся Ctrl+C и Ctrl+V...., ограничась небольшими модификациями.
Вместо статического main сделаем "умный" объект Downloader, и начнем рефакторить с двух концов.
На конце Downloader'а сделаем дата-класс Comment. Поля нам уже известны - пoрядковый номер в нашем алгоритме (который выдает их по новизне, вроде бы), имя автора, текст комментария, isReply и commentId. в итерациях по commentEntityPayload будем добавлять в итоговый лист новые объекты с заполненными полями. На начале - переместим все подготовительные операции в конструктор, да передадим в него произвольный url. Экстрактим методы, сложное пока не трогаем. Переместим кое-что кое-куда. Добавим таймер в main().
28 секундна видео с 500 комментариями, много! будем оптимизировать.
В исходном скрипте не реализована группировка, то есть если коммент вляется ответом, то непонятно, на что конкретно это ответ. Строго говоря, есть 2 вида "ответов" - список ответов к комментарию верхнего уровня, и ответ пользователю с обращернием к нему по имени в тексте сообщения. Пока поработаем только с первым типом ответов. В выводе есть только boolean reply = true и айдишник коммента, который состоит из двух частей. Попробуем проанализировать айдишники, надо составить таблицу
|id оригинала | id ответа | текст для поиска
+---------------------------+---------------------------------------------------+-------------------------------------
|Ugz25cqFRKgIIVYpzZp4AaABAg | |Земля плоская! Электричества не существует!
| |Ugz25cqFRKgIIVYpzZp4AaABAg.A547Lcuvd9AA548QuyCbBn |Три слона уже устали
| |Ugz25cqFRKgIIVYpzZp4AaABAg.A547Lcuvd9AA549Chp7qJJ |Настоящий бог Колбас!
|UgwSZa9HUSBkMsloDBh4AaABAg | |Тргда может бахнем ?
| |UgwSZa9HUSBkMsloDBh4AaABAg.A544MKu4LeBA549aCz_x0K |Может и бахнем , но позже
|UgyhTnBOZUlAKdKPKhV4AaABAg | |Посмотрим какой это фейк, когда бахнут
| |UgyhTnBOZUlAKdKPKhV4AaABAg.A543zZxY-FlA54ErR7cyE2 |Не бахнут,ато фейк расскроется
Ну, тут всё очевидно: root-комментарий имеет короткий id, а ответы на него имеют такой же id, но с добавлением точки и дополнительного айдишника. Напрашивается отличный простой способ отобразить ветки ответов. Добавим в наш вывод поля reply и id, и отсортируем по ним конечный список, для этого напишем анонимный компаратор по двум пропертям - первая часть айдишника , и порядковый номер в изначальном списке выдачи.
Утилита для парсинга html Ютуба и скачивания комментариев к видео. Переписываю аналогичный код на Питоне, пока всё подряд накидываю в Мейне, потом порефакторю. В перспективе будет сервис.
Сначала получаем html, оттуда берем конфигурацию, часть которой будем использовать в ajax-запросах
находим эндпойнты, характерные для прокрутки и дальнейшей подгрузки, для каждого (они называются continuations) делаем ajax-запрос и парсим ответ. Если в ответе находим другие эндпойнты для следующих ajax-запросов, их тоже добавляем в список continuations
Вроде работает, но количество выданных комментариев немного совпадает со счетчиком, который отображает ЮТ над блоклм комментов. При ручном подсчете вышло расхождение в 1 коммент, так что скорее всего дело в скрытых или удаленных модератором. Для моих целей это хороший результат.