Первые трудности
Выбор инструмента
Возможно для кого-то уже стало очевидным, что за подгруздку материала (истории) на странице будет отвечать ajax. Но конечно же дьявол как говориться, “кроется в мелочах”, и в данном случае мы получаем запрос в котором содержаться ctoken , своеобразный ключ который генерируется из JS, а он в свою очередь обращаться к cookie из которых и собирает данный ключ. Конечно я бы мог запариться, и найти библиотеку для Requests , которая бы смогла обрабатывать мои ‘печеньки’ и вставлять их в JS, но я посчитал что подобные решения просто нерациональны. Зачем изобретать велосипед если есть selenium который создает экземпляр браузера, физически его открывает и работает с всеми его инструментами, к тому же я могу видеть собственными глаза как идет выполнение сценария.
Пример POST запроса
Определение цели сбора
Начал я с сбора истории моих поисковых запросов, из соображения простаты данной операции (как мне тогда казалось). В этом мне помог замечательный плагин для браузера “Katalon Recorder” который обладает инструментом для определения элементов веб интерфейса, что я бы уже непосредственно вставил полученный ‘адрес’ в свой код. На выходи я получил вот это: driver.find_elements_by_xpath(“//div[@id=’dismissable’]/a/div”)
Для начал я собрал тестовую сборку кода для определение количества найденных объектов на странице.
from selenium import webdriver
# Настройки профиля бразуера.
options = webdriver.ChromeOptions()
options.add_argument(r"user-data-dir=C:\Users\moonz\AppData\Local\Google\Chrome")
driver = webdriver.Chrome(executable_path="C:\\chromedriver.exe", chrome_options=options)
driver.get("https://www.youtube.com/feed/history/search_history")
def coments():
try:
a = driver.find_elements_by_xpath("//div[@id='dismissable']/a/div")
print(len(a)) # Выводим количество найденных объектов.
finally:
driver.close() # Закрываем окно браузера.
time.sleep(2) # Даем процессу корректно завершиться.
driver.quit() # Завершаем процесс веб драйвера.
coments()
При однократном вызове функции я получаю список лишь из 17ти объектов класса “selenium.webdriver.remote.webelement.WebElement” , разумеется я задал себе логичный вопрос почему Так мало ?
Ответ кроется в скорости загрузки страницы и исполнении самого кода. Стоило добавить:
time.sleep(2)
Размер получаемых элементов возрос Аж до 115ти , так произошло потому что страница успела загрузить Всё содержимое, однако jquery отвечающие за загрузку материла в таком случае не срабатывал, и получить Больше объектов не удавалось.
Трудности прокрутки страницы
Пожалуй Самый сложный этап в этом проекте.
На нахождение Рабочего решения я убил не меньше 5 часов кропотливого перебора различных примеров из гугла. И чего я только не перепробовал =)
Вот такие решения:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
абсолютно бесполезны поскольку они задействуют ‘теневую’ прокрутку, а не физическую (визуальную), и результатом их работы была обычная прогрузка страница до всё тех же 114ти элементов списка. Пытался я реализовать и нажатие клавиш. Например я подумал почему бы не имитировать нажатие arrow_down что бы просто прокручивать страницу до упора. Но увы, я столкнулся с еще рядом проблем, одна из которых это конфликт библиотеки selenium.webdriver.common.keys c веб драйвером от Хрома. Но когда я всё тоже самое проделал на лисе оказалось что данный метод можно вызвать только по отношению к конкретному объекту класса, а поскольку текст был Невидимым элементом, то и фокусировать на нем нажатие было невозможно =_=
Я уже начал было выходить из себя, как осознал тот факт что я задаю не Те вопросы, я подумал а почему бы мне не реализовать именно Фокусировку на нужно элементе списка, например на самом последнем, что бы страница проливалась до него, иии запустился ajax скрипт за которым следовало следующие 114 элементов, но это в идеале как думал Я 😀
На деле я конечно же соснул тунца ) потому чтооо см.пункт выше (фокусироваться можно только на Видимых объектах), а текст был бы частью объекта //div[@id=’dismissable’]/a/div
Решение можно было найти только путем понимания глубинных процессов взаимодействия объектов класса и его функция, чего я естественно делать не стал )) Но мир сжалился и подкинул мне уже знакомое решение, но выглядело оно совершенно иначе.
driver.execute_script("window.scrollTo(0,10000);")
Данный метод Максимально прокручивает страницу (физически) вниз, до скажем так ‘упора’ что в свою очередь уже вызывало срабатывание злощястного ajax скрипта и загружало вторую часть страница, Аминь !
Обожаю эксперименты 😀
Опытным путем было доказано, что использовать GUI версию браузера хром для подобных операций с большими страницами, чревато…
Не очевидное решение
В процессе познания функций веб драйвера (когда-то давно я искал способ запустить браузер с профилем), я нашел такой аргумент запуска экземпляра веб драйвера: options.add_argument(‘headless’) он запускает версию браузера без GUI вообще, то есть в процессах появляется просто еще один процесс, но никакого визуального подтверждения этому я не получаю. В итоге расход ресурсов сократился раз 10 не меньше, потому что при каждом новой загрузки результатов в обыном хроме, загрузка ЦП подскакивала процентов на 5, и в результате после 10-15 загрузок у меня просто вылетал синий экран. Однако при запуске в headless режиме всё работает прекрасно и без просадок ресурсов. В итоге мы получаем функциональный парсер работающий в невидимом режиме.
В итоге за примерно 7 минут работы, было получено 4200 результатов Моего поиска, которые я благополучно записал в текстовый файл и сохранил для истории.
Исходник данной версии скрипта находиться тут
Собираем Коментарии
Тут механизм кода не сильно отличается от предыдущей реализации, разве что объект поиск изменился на:
driver.find_elements_by_xpath("//div[@id='content']")
Но в таком случае мы получаем ‘грязный’ текст, который содержит помимо текста самого комментария, еще и название ролика + комментарий на которой я отвечал, а нам то нужен только текст =)
driver.find_elements_by_css_selector("div[class='style-scope ytd-expander']")
Данный метод возвращает как и в первом случае Список элементов, который содержит текст Только моих комментариев, если Вам требуется полное содержание то используйте первый ключ на здоровье ^__^
На этот раз запись будем реализовывать в формате csv, я решил так поступить что бы было нагляднее и проще читать полученный материал, в конечно счете можно записать и просто в txt, но тогда всё будет сливаться в один сплошной столбик, в теории его можно было Бы разбавить отступами и переносами, Нооо я не ищу легких путей, и по этому дополнительно реализовал данную функцию записи в csv.
Исходный код самого скрипта как всегда можно найти на моем гите
def read(text):
with open('coments.csv', 'a') as f:
write = csv.writer(f)
write.writerow([text])
Собираем минуты
Самое интересное
Винцом проекта естественно будет реализация Главной идеи, я хочу получить суммарное время ВСЕХ просмотренных мною роликов на youtube, и ничего страшного если некоторые, а может и многие я смотрел не до конца, однако некоторые были просмотрены два и более раза. Наверное может показаться что суть задачи крайне проста, поменять элемент поиска на селектор минут, собрать его в список и сложить. Ноооо я бы не стал писать целую статью =)
Начнем с того что изучим специфику поведения страницы с искомыми элементами: youtube.com/feed/history и видим вот такую картину
Для тех кто не понял, при загрузки страницы происходит плавная догрузка тайминга. Осуществляется оно тоже с помощью jquery, по этому возникает логичный вопрос ‘А что будет получено в результате итерации элементов списка’, и ответ будет вполне логичным исходя из увиденного выше , это 10 результатов которые загружаются в прямой видимости браузера.
Тут у внимательного читателя может сложиться впечатление что вопрос можено решить по аналогии с другими решениями, просто поставив задержку после пролистывания что бы дать странице время на прогрузку содержимого. К сожалению на деле оказалось намного сложнее. Сам процесс работы веб драйвера никак не иметирует движение мышки, и уж тем более действия пользователя, что в свою очередь приводит к тому что сценарий загрузки иконки с временем не происходит, и вот тут я понял насколько важным было моё решение в выборе фреймворка для решения задачи, если Бы я использовал Requests то никак не смог решить данную проблему, но поскольку мы имеем дело с selenium решение было, и я разумеется его нашел =)
move_to_element
Имитируем поведение пользователя
Как можно догадаться из подзаголовка, идея стоит в том что Бы имитировать фокусировку пользователя на отдельном элементе интерфейса, в нашем случае это тот самый прямоугольник с цифрами. В этом нам поможет библиотека from selenium.webdriver.common.action_chains import ActionChains которая дает возможность имитировать действия по наведению мышки, и еще многое другое что нам пока не нужно, но если интересно то всё есть в официальной документации.
Итак перейдем к собственно самому решению которое обеспечит желаемый результат:
# Ищем фрэйм с цифрами
b = driver.find_elements_by_xpath("//div[@id='overlays']/ytd-thumbnail-overlay-time-status-renderer")
print(len(b)) # Выводим количество полученных объектов.
action = ActionChains(driver)
for i in b: # Итерируемся по каждому найденному объекту.
action.move_to_element(i).perform() # Имитируемым наведение мышки, и действие "отпускания" =)
print(i.text) # Печатаем результат.
Складываем результаты
Переходим к заключительному этапу проекта, суть задачи в том что бы сложить числа получаемые при парсинге. Проблему состоит в том что входные данные приходят вот такого вида [38:17] = [минуты:секунды] а в некоторых случаях первым числом будет например 1, что ровняется одному часу. По этому нужно написать правило которое бы принимало Любое сочетание чисел на вход, а отдавало уже произведение. Реализовал я данную конструкцию вот таким образом:
sp = i.text.split(':') if len(sp) == 3: hour = int(sp[0]) minut = int(sp[1]) summ += hour * 60 + minut # секунды брать не будет =) мне лень :P elif len(sp) == 2: minut = int(sp[0]) summ += minut else: # Секундные ролики в расчет брать не будем. continue
Возможно Можно было БЫ сделать это как-то проще, и изящнее … Нооо я не волшебник, я только учусь :3
Не сдаюсь
Прошло уже около двух-трёх часов, а питончик всё еще пыхтит в попытках обработать всего то 998 объектов *видео. Это меня сильно огорчает, ведь я хотел совершенно не этого, и в связи с невозможностью многопоточного решения я подумал а почему бы не задействовать такую популярную Штуку как beautifulsoup он собственно для этого и существует, опыт работы у меня с ним уже имеется. Хоть он и довольно капризный, однако с ним всё же можно совладать, просто следует быть очень Точным в своих указаниях )
Идея заключается в следующем:
- Загрузить всю страницу (желательно до самого первого видео)
- Получить HTML код всей страницы.
- Вытащить необходимые данные с помощью beautifulsoup
- Перемножить
- PROFIT
Будем посмотреть =)
b = driver.find_elements_by_xpath("//div[@id='overlays']/ytd-thumbnail-overlay-time-status-renderer") print(len(b)) html = driver.page_source # Получаем HTML код загруженной страницы spisok = [] summ = int() pattern = r'\d' soup = BeautifulSoup(html, 'lxml') # Создаем объект класса BeautifulSoup test = soup.find_all(class_='style-scope ytd-thumbnail-overlay-time-status-renderer') # Искомое поле с цифрами for i in test: a = i.text.strip() # Отсеиваем цифры от мусора. b = a.split(':') # Разделям цифры для подсчета. if len(b) == 3: hour = int(b[0]) minut = int(b[1]) summ += hour * 60 + minut # секунды брать не будет =) мне лень :P elif len(b) == 2: minut = int(b[0]) summ += minut else: # Секундные ролики в расчет брать не будем. continue
Итоги
В результате такой сборки, итерация по 166ти объектам программа справилась всего то за 21 секунду. За час однако удалось обработать 1262 объекта (видео), возможно это предельное количество доступное для загрузки, или хранения. Данный результат был получен из итерации в 1.000 прокруток, я еще попробую увеличить это количество что бы быть уверенным.
Полезные ссылки
Эпилог
Заключение
Хочу сказать что работа с Selenium мне не принесла вообще никакого удовольствие, одни муки и баги. Конечно я получил желаемый результат но какой ценой ?… Надеюсь мне никогда больше не предаться использовать данное решение потому что оно требует абсолютного подчинения что бы получить желаемый (корректный) результат. Одной из ключевых неприятностей я могу отметить это зависающие в процессе веб драйвера от хрома. Это происходило повсеместно, даже при том что я обработал исключение с помощью метода driver.close() , однако в процесса подвисало это дерьмо которое приходилось закрывать вручную КАЖДЫЙ раз, потом конечно я доработал конструкцию но она всё же иногда давала осечки. Итак собственно по традиции своего блога каждый СВОЙ пост я заканчиваю посланием для себя будущего.
massege
Для не программиста, а сисадмна, мог бы подробнее объяснить пошагово все действия? Какое ПО установить, куда вставлять эти коды, где открывать Ютуб…