Идея:

Разумеется любой проект начинается с Идеи, в данном случае она уже вложена в название поста ). Я сижу на ютубе еще с момента своего первого захода в интернет, но свой профиль там я получил немного позже. Так или иначе, но ютуб хранит всю историю просмотров, и оставленных мною комментариев. Разумеется мне стало любопытно, а что же я такого писал в коментах лет этак пять назад, а Самое главное я хотел получить Общее количество часов просмотренных мною роликов. Если кто не знал, находиться данная информация по адресу [ /feed/history/comment_history ], к сожалению на самом сайте вообще нет никаких инструментов для работы с этой историей, и по этому я решил их создать. Пока я хотел бы начать просто с автоматизации самого процесса сбора (парсинга), а потом уже обработки полученной информации.

Первые трудности

Выбор инструмента

Возможно для кого-то уже стало очевидным, что за подгруздку материала (истории) на странице будет отвечать ajax. Но конечно же дьявол как говориться, “кроется в мелочах”, и в данном случае мы получаем запрос в котором содержаться ctoken , своеобразный ключ который генерируется из JS, а он в свою очередь обращаться к cookie из которых и собирает данный ключ. Конечно я бы мог запариться, и найти библиотеку для Requests , которая бы смогла обрабатывать мои ‘печеньки’ и вставлять их в JS, но я посчитал что подобные решения просто нерациональны. Зачем изобретать велосипед если есть selenium который создает экземпляр браузера, физически его открывает и работает с всеми его инструментами, к тому же я могу видеть собственными глаза как идет выполнение сценария.

Пример POST запроса

Определение цели сбора

Начал я с сбора истории моих поисковых запросов, из соображения простаты данной операции (как мне тогда казалось). В этом мне помог замечательный плагин для браузера “Katalon Recorder” который обладает инструментом для определения элементов веб интерфейса, что я бы уже непосредственно вставил полученный ‘адрес’ в свой код. На выходи я получил вот это:     driver.find_elements_by_xpath(“//div[@id=’dismissable’]/a/div”)

Это непосредственное содержание Текста истории запроса, find_elements  возвращает Список всех элементов найденных на странице. И тут возникает новая проблема. А сколько их, этих самых элементов ?

Для начал я собрал тестовую сборку кода для определение количества найденных объектов на странице.

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])
В итоге мы получаем замечательный список комментариев, которые так греют душу :3

Собираем минуты

Самое интересное

Винцом проекта естественно будет реализация Главной идеи, я хочу получить суммарное время ВСЕХ просмотренных мною роликов на 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
    # секунды брать не будет =) мне лень 😛
elif len(sp) == 2:
    minut = int(sp[0])
    summ += minut
else:  # Секундные ролики в расчет брать не будем.
    continue

Возможно Можно было БЫ сделать это как-то проще, и изящнее … Нооо я не волшебник, я только учусь :3

К сожалению мой подход к решению данной задачи имеет существенный минус, это Время.. Увы и ах, я еще не овладел такой наукой как мультипроцессинг, по этому скрипт работает крайне долго и потребляет кучу ресурсов, я попробую еще поработать над этим.  Спустя пару часов поиска мтариала мне удалось выяснить что силениум вообще не поддерживает работу с мультипроцессингом. По этому буду думать как реализовать иначе.

Не сдаюсь

Прошло уже около двух-трёх часов, а питончик всё еще пыхтит в попытках обработать всего то 998 объектов *видео. Это меня сильно огорчает, ведь я хотел совершенно не этого, и в связи с невозможностью многопоточного решения я подумал а почему бы не задействовать такую популярную Штуку как beautifulsoup он собственно для этого и существует, опыт работы у меня с ним уже имеется. Хоть он и довольно капризный, однако с ним всё же можно совладать, просто следует быть очень Точным в своих указаниях )

Идея заключается в следующем:

  1. Загрузить всю страницу (желательно до самого первого видео)
  2. Получить HTML код всей страницы.
  3. Вытащить необходимые данные с помощью beautifulsoup
  4. Перемножить
  5. PROFIT

Будем посмотреть =)

Итак спустя пару часов мозгового штурма, решение было найдено. Конструкция поиск нужного значения была не трудной, труднее было вычленить конкретно Цифры из всего этого ‘результата’, чего я только не пробовал, даже регулярные выражения)
Спасла меня по традиции моя память, вспомнил как в одном из видео Олега Молчанова он демонстрировал интересный метод отсеивания полученного значения в beautifulsoup так называемый strip()  который отделил получаемый результат от пробелов и символов переноса строки. В итоге получилась вот такая конструкция:
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
        # секунды брать не будет =) мне лень 😛
    elif len(b) == 2:
        minut = int(b[0])
        summ += minut
    else:  # Секундные ролики в расчет брать не будем.
        continue

Итоги

В результате такой сборки, итерация по 166ти объектам программа справилась всего то за 21 секунду. За час однако удалось обработать 1262 объекта (видео), возможно это предельное количество доступное для загрузки, или хранения. Данный результат был получен из итерации в 1.000 прокруток, я еще попробую увеличить это количество что бы быть уверенным.

Полезные ссылки

Англоязычное руководства по selenium

Русскоязычное руководство по тонкостям webdriver

Русскоязычный сайт с руководством (есть НЕ всё) тут

PDF Книжечка Selenium Python Bindings тут

Эпилог

Заключение

Хочу сказать что работа с Selenium мне не принесла вообще никакого удовольствие, одни муки и баги. Конечно я получил желаемый результат но какой ценой ?… Надеюсь мне никогда больше не предаться использовать данное решение потому что оно требует абсолютного подчинения что бы получить желаемый (корректный) результат. Одной из ключевых неприятностей я могу отметить это зависающие в процессе веб драйвера от хрома. Это происходило повсеместно, даже при том что я обработал исключение с помощью метода driver.close() , однако в процесса подвисало это дерьмо которое приходилось закрывать вручную КАЖДЫЙ раз, потом конечно я доработал конструкцию но она всё же иногда давала осечки. Итак собственно по традиции своего блога каждый СВОЙ пост я заканчиваю посланием для себя будущего.

massege

Если задачу не получается решить одним способом, не стоит зацикливаться на нем, лучше поискать новых подход, и уже если его не существует (что мало вероятно) в таком случае возвращаться к первому подходу пытаясь реализовать его. Бывает так что даже при вдумчивом анализе процесса ты упускаешь общую картину, или абстрактную деталь. Поиск иного пути помогает взглянут на задача с разных сторон обнажая таким образом скрытые детали. Никогда не стоит бояться экспериментировать, только так можно научиться чему-то абсолютно новому.