<jupyter_start><jupyter_text>Семинар 4 Всем привет и добро пожаловать на наш семинар по сбору новостей. Сегодня мы ставим себе следующие цели:1. Собрать все заголовки статей на определенную тему, отфильтровав по дате с сайта делового издания "Коммерсант"2. Узнать про хитрости, которые могут помочь в сборе данных3. Провести самостоятельную практику с похожими заданиямиДля этого мы воспользуемся уже знакомыми Вам библиотеками requests и BeautifulSoup, а также познакомимся с новой – Selenium.*Материал подготовила Анастасия Максимовская, хитрости позаимствованы у Филиппа Ульянкина. Вопросы по материалам можно писать мне в телеграм @anastasiyamaxx*  Забираем заголовки с Коммерсанта<jupyter_code>import requests 
from bs4 import BeautifulSoup<jupyter_output><empty_output><jupyter_text>Итак, начнем с простого – проверим, соберется ли у нас информация с главной страницы Коммерсанта или нужно искать специальные примочки.<jupyter_code>url = 'https://www.kommersant.ru/'
response = requests.get(url)
response<jupyter_output><empty_output><jupyter_text>`` выглядит хорошо. Но имейте в виду – это не всегда значит, что мы получили нужную информацию. Например, когда я пишу этот семинар, главная страница выглядит так:Однако, если бы нам вылетел баннер (например, какое-нибудь предложение о скидке) или запрос в духе "уточните Ваше местоположение", или капча, то некоторый нужный текст с главной страницы в собранный html мог бы не попасть. Для этого можно либо глазами посмотреть на `response.content` или попробовать найти нужный элемент с помощью методов `.find()` (находит первый элемент, отвечающий условиям в скобочках) или `.find_all()` (находит все нужные элементы) из библиотеки `bs4`. Сделаем деревце, по которому можно искать нужные элементы с помощью `bs4`:<jupyter_code>tree = BeautifulSoup(response.content, 'html.parser')<jupyter_output><empty_output><jupyter_text>Найдем главную новость в тексте. Для этого я перехожу на сайт Коммерсанта, навожу мышкой на заголовок "Роспотребнадзор сократил до суток максимальный срок выполнения исследования на коронавирус", щелкаю правой кнопкой мыши и нажимаю "Просмотреть код элемента". Вижу что-то такое:Попробуем найти этот элемент в нашем дереве!<jupyter_code>tree.find_all('a', {'class': 'top_news_main__link link'})<jupyter_output><empty_output><jupyter_text>Достанем только текст:<jupyter_code>tree.find('a', {'class': 'top_news_main__link link'}).text.strip()<jupyter_output><empty_output><jupyter_text>Однако, если Вы впервые заходите на сайт или откроете окно в режиме инкогните, то увидите, что при первом визите на сайт вылетает такой баннер:Также это можно заметить, полистав содержимое `tree`. Конкретно в этом примере нам это не мешает вытащить заголовок. Однако, иногда такие всплывающие окна мешют собрать html с нужной информацией со страницы. Что же делать? Нам на помощь придет библиотека selenium – специальный инструмент для автоматизации действий браузера.  Добавляем селениум :) Библиотека `selenium` – набор инструментов для интерактивной работы в браузере средствами Python. Вообще Selenium ‒ это целый проект, в котором есть разные инструменты. Мы рассмотрим один из самых распространенных ‒ Selenium WebDriver, модуль, который позволяется Python встраиваться в браузер и работать в нем как пользователь: кликать на ссылки и кнопки, заполнять формы, выбирать опции в меню и прочее.<jupyter_code># через восклицательный знак обращемся к командной строке (на маке называется terminal)
# pip – менеджер пакетов для питона, который позволяет нам поставить библиотеку
!pip install selenium
!pip install webdriver-manager<jupyter_output>Requirement already satisfied: selenium in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (3.141.0)
Requirement already satisfied: urllib3 in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (from selenium) (1.26.4)
Requirement already satisfied: webdriver-manager in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (3.4.2)
Requirement already satisfied: requests in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (from webdriver-manager) (2.25.1)
Requirement already satisfied: configparser in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (from webdriver-manager) (5.0.2)
Requirement already satisfied: crayons in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (from webdriver-manager) (0.4.0)
Requirement already satisfied: idna<3,>=2.5 in /Users/a18509896/opt/anaconda3/lib/python3.8/site-packages (from requests->webdriver-manager) (2.10)
Requirement already satisfied: chardet<5,>=3.0.2 in /Users/a18509896/opt/anaconda3/lib[...]<jupyter_text>Для того, чтобы воспользоваться библиотекой, нужно загрузить вебдрайвер для Вашего браузера. Подробнее можно почитать [в пункте 1.5 документации про установку](https://selenium-python.readthedocs.io/installation.html). План действий такой: качате драйвер – прописываете путь в переменной PATH – используете.Но мы воспользуемся лайфхаком, чтобы не мучиться долго с установкой. Это библиотека `webdriver-manager`, которая скачает вебдрайвер за Вас. Подробнее в [документации](https://pypi.org/project/webdriver-manager/) (там же можно посмотреть код для других браузеров).<jupyter_code>from selenium.webdriver.common.keys import Keys
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())<jupyter_output>====== WebDriver manager ======
Current google-chrome version is 94.0.4606
Get LATEST driver version for 94.0.4606
Get LATEST driver version for 94.0.4606
Trying to download new driver from https://chromedriver.storage.googleapis.com/94.0.4606.61/chromedriver_mac64.zip
Driver has been saved in cache [/Users/a18509896/.wdm/drivers/chromedriver/mac64/94.0.4606.61]<jupyter_text>На Вашем компьютере откроется пустая страничка. Давайте перейдем на сайт Коммерсанта.<jupyter_code>driver.get('https://www.kommersant.ru/')<jupyter_output><empty_output><jupyter_text>Откройте эту страничку – теперь она не пустая :) Следующим шагом, нам надо понять, как кликнуть на баннер так, чтобы он закрылся. Для этого нужно определить пусть к кнопке. Как и раньше, наводим мышку и кликаем "Просмотреть код". Теперь нужно сделать 2 действия кодом:1. Помочь драйверу найти элемент2. Кликнуть на негоЕсть несколько способов указать пусть к элементу, они описаны [здесь](https://selenium-python.readthedocs.io/locating-elements.html) (попросите Вашего семинариста вкратце прокомментировать каждый). Принципиальной разницы нам сейчас нет, предлагаю воспользоваться методом `driver.find_element_by_css_selector()`. Правой кнопокой мыши щелкните по коду нужной кнопки (принять или отклонить), выберите copy – copy selector. Сохраним селектор в переменную, найдем нужный элемент и кликнем. Иногда работает не с первого раза.<jupyter_code>selector = "body > div.subscription-popup-v2.subscription-popup-v2_push-popup > div.subscription-popup-v2__inner-container > div.subscription-popup-v2__controls > div.subscription-popup-v2__reject"
ss = driver.find_elements_by_css_selector(selector)[0]
ss
ss.click()<jupyter_output><empty_output><jupyter_text>Обновим страничку на всякий случай:<jupyter_code>driver.refresh()<jupyter_output><empty_output><jupyter_text>Давайте найдем главный заголовок еще одним способом. Сначала найдем элемент, помня имя класса (см. скрины выше), потом достанем его html код.<jupyter_code>main_news = driver.find_element_by_class_name("top_news_main__name")
main_news
main_news.get_attribute('innerHTML')
small_tree = BeautifulSoup(main_news.get_attribute('innerHTML'), 'html.parser')
small_tree
small_tree.text.strip()<jupyter_output><empty_output><jupyter_text>Ура, получили заголовок главной новости. Если он отличается от того, что на скрине, то нестрашно – новостные сайты быстро меняют статьи на главных страницах. Остальное можно достать по аналогии :)Перейдем к более интересному – соберем все новости на определенную тему и срок. Предлагаю попробовать собрать все новости, содержащие фразу "центральный банк" за период с 24 августа 2021 по текущий день. То есть, переводя это на программистский, нам нужно проделать следующие действия:1. Найти окно поиска, кликнуть2. Ввести в него ключевую фразу, нажать кнопку поиска3. Нажать кнопку расширенный поиск4. Найти кнопку, где изменяем дату начала поиска, выставить нужную нам5. Собрать информациюДавайте начнем :) В прошлый раз мы воспользовались поиском с помощью селектора `.find_element_by_css_selector()`. Теперь добавим немного разнообразия и сделаем поиском через XPath. Получить ее можно по старой схеме: наводим мышь на окно поиска – кликаем посмотреть код – правой кнопкой кликаем по мыши на выделенном коде – выбираем copy – copy xpath.    По шагам:1. наводим мышь на окно поиска – кликаем посмотреть код 2. правой кнопкой мыши кликаем на выделенном коде – выбираем copy – copy xpath<jupyter_code>"Гарик "Бульдог" Харламов"
"Гарик \"Бульдог\" Харламов"
'Гарик "Бульдог" Харламов'
# найденный по инструкции выше xpath к лупе
xpath_query = '//*[@id="js-navsearch-submit"]'
# находим окно поиска
search = driver.find_element_by_xpath(xpath_query)
# кликаем на него
search.click()
# найденный по инструкции выше xpath к окну поиска
xpath_query = '//*[@id="js-navsearch-query"]'
# находим окно поиска
search = driver.find_element_by_xpath(xpath_query)
# кликаем на него
search.click()
search_term = "ц<PASSWORD>"
# печатаем фразу для поиска в окне для поиска
search.send_keys(search_term)
# нажимаем кнопку enter
search.send_keys(Keys.RETURN)<jupyter_output><empty_output><jupyter_text>Если Вы посмотрите, что происходит в окне браузера, которым управляет селениум, то увидите, что окно поменялось и мы теперь в разделе поиска :) Нажимаем на кнопку расширенный поиск и выбираем дату. Дальше мы все уже знаем. Откройте в соседней с ноутбуком вкладке сайт коммерсанта и доставайте оттуда нужные селекторы / xpath (неважно).<jupyter_code># находим селектор для кнопки расширенный поиск и нажимаем ее
selector2 = "body > main > div > div > section > div.grid-col.grid-col-s3 > form > div.ui-field_pack > label"

ext_search = driver.find_element_by_css_selector(selector2)
ext_search.click()
# находим селектор для поля ввода даты
selector3 = "body > main > div > div > section > div.grid-col.grid-col-s3 > form > div.ui-collapse.js-toggle-collapse.js-toggle-item.ui-collapse--show > section.simple_page_section.simple_page_section--form.js-search-settings > div.grid.ui-collapse.js-toggle-collapse.js-toggle-item.ui-collapse--show > div:nth-child(1) > div > input"

date = driver.find_element_by_css_selector(selector3)<jupyter_output><empty_output><jupyter_text>Обратите внимание на картинку ниже – дата начала поиска по дефолту вбита в окошко, надо ее удалить.<jupyter_code># удаляем введеный по дефолту текст в ячейке
date.send_keys(Keys.SHIFT, Keys.END, Keys.BACK_SPACE)
# вводим нужную дату и надижимаем enter
date_start = "24.08.2021"
date.send_keys(date_start)
date.send_keys(Keys.RETURN)<jupyter_output><empty_output><jupyter_text>Ура, получили нужную выдачу! Попробуем перейти на следующую страничку.<jupyter_code># путь к кнопке следующая страница
xpath3 = "/html/body/main/div/div/section/div[1]/div[3]/a"
second_page = driver.find_element_by_xpath(xpath3)
second_page.click()<jupyter_output><empty_output><jupyter_text>Посмотрим на адрес нашей странички:<jupyter_code>driver.current_url
# driver.page_source<jupyter_output><empty_output><jupyter_text>Обратите внимание на параметр `page=2`. Если мы будем менять номера, то будем перемещаться по всем страницам с заголовками, удовлетворяющим нашим условиям. Осталось написать функцию, которая будет доставать нужную информацию с одной странички, и запустить ее циклом для всех. Начнем с того, как задать url. Обратите внимание на обратный слэш, это так называемый line continuation character. Он означает, что код продолжится на следующей строке. Также обратите внимание на букву f перед продолжением url-адреса на 3 строчке – она позваоляет мне подставить значение переменной `{page_num}` в середину строки.<jupyter_code>page_num = 1
url = 'https://www.kommersant.ru/search/results?search_query='\
    '%D1%86%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9+%D0%B1'\
    '%D0%B0%D0%BD%D0%BA&sort_type=1&search_full=1&time_range=2&'\
    f'dateStart=2021-08-24&dateEnd=2021-10-15&page={page_num}'
url<jupyter_output><empty_output><jupyter_text>Как обычно забираем HTML-разметку и делаем деревце бьютифул супом.<jupyter_code>response2 = requests.get(url)
response2
tree_search = BeautifulSoup(response2.content, 'html.parser')<jupyter_output><empty_output><jupyter_text>Уже знакомый по лекциям механизм поиска элемента по html разметке.<jupyter_code># находим заголовки
headers = tree_search.find_all('h2', {'class': 'uho__name rubric_lenta__item_name'})
len(headers)
headers[0]<jupyter_output><empty_output><jupyter_text>Если достать из тега текст, то можно заметить, что есть пробелы / переходы на новые строки в начале и конце. Метод `.strip()` избавится от них.<jupyter_code>headers[0].text
headers[0].text.strip()
# находим подзаголовки
subheaders = tree_search.find_all('h3', \
                {'class': 'uho__subtitle rubric_lenta__item_subtitle'})
len(subheaders)<jupyter_output><empty_output><jupyter_text>Подзаголовки есть не у всех новостей!<jupyter_code>subheaders[0]
subheaders[0].text
subheaders[0].text.strip()
# напишем функцию, которая будет выдавать список из словарей 
# в каждом словаре заголовок и описание
def get_page_info(page_num):
    url = 'https://www.kommersant.ru/search/results?search_query='\
        '%D1%86%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9+%D0%B1'\
        '%D0%B0%D0%BD%D0%BA&sort_type=1&search_full=1&time_range=2&'\
        f'dateStart=2021-08-24&dateEnd=2021-10-15&page={page_num}'
    
    response = requests.get(url)
    tree_search = BeautifulSoup(response.content, 'html.parser')
    headers = tree_search.find_all('h2', \
                                   {'class': 'uho__name rubric_lenta__item_name'})
    subheaders = tree_search.find_all('h3', \
                            {'class': 'uho__subtitle rubric_lenta__item_subtitle'})
    result = []
    for i in range(len(headers)):
        header = headers[i]
        try:
            subheader = subheaders[i]
            d = {'article_header': header.text.strip(),
                 'article_subheader': subheader.text.strip()}
        except:
            d = {'article_header': header.text.strip(),
                 'article_subheader': ''}
        result.append(d)
    return result
all_data = []
for n in range(1, 14):
    all_data.extend(get_page_info(n))
len(all_data)<jupyter_output><empty_output><jupyter_text>Пока не очень знакомая библиотека сделает табличку из списка и позволит сохранить ее в файл.<jupyter_code>import pandas as pd
df = pd.DataFrame(all_data)
df.head()
# сохранить в csv формат
# index=False сделает так, чтобы колонка с индексами не вогла в итоговый файл
df.to_csv('all_data.csv', index=False)
# сохранить в xlsx формат
df.to_excel('all_data.xlsx', index=False)
# не забываем закрыть браузер драйвером после завершения работы :)
driver.close()<jupyter_output><empty_output><jupyter_text>Что делать, если сервер разозлился? * Вы решили собрать себе немного данных * Сервер не в восторге от ковровой бомбардировки автоматическими запросами * Error 403, 404, 504, $\ldots$ * Капча, требования зарегистрироваться* Заботливые сообщения, что с вашего устройства обнаружен подозрительный трафик   а) быть терпеливым * Слишком частые запросы раздражают сервер* Ставьте между ними временные задержки<jupyter_code>import time
time.sleep(3) # и пусть весь мир подождёт 3 секунды<jupyter_output><empty_output><jupyter_text>б) быть похожим на человекаЗапрос нормального человека через браузер выглядит так:      С ним на сервер попадает куча информации! Запрос от питона выглядит так:   Заметили разницу?  Очевидно, что нашему скромному запросу не тягаться с таким обилием мета-информации, которое передается при запросе из обычного браузера. К счастью, никто нам не мешает притвориться человечными и пустить пыль в глаза сервера при помощи генерации фейкового юзер-агента. Библиотек, которые справляются с такой задачей, существует очень и очень много, лично мне больше всего нравится [fake-useragent.](https://pypi.org/project/fake-useragent/) При вызове метода из различных кусочков будет генерироваться рандомное сочетание операционной системы, спецификаций и версии браузера, которые можно передавать в запрос:<jupyter_code>from fake_useragent import UserAgent
UserAgent().chrome<jupyter_output><empty_output><jupyter_text>Например, https://knowyourmeme.com/ не захочет пускать к себе python и выдаст ошибку 403. Она выдается сервером, если он доступен и способен обрабатывать запросы, но по некоторым личным причинам отказывается это делать.<jupyter_code>url = 'https://knowyourmeme.com/'

response = requests.get(url)
response<jupyter_output><empty_output><jupyter_text>А если сгенерировать User-Agent, вопросов у сервера не возникнет.<jupyter_code>response = requests.get(url, headers={'User-Agent': UserAgent().chrome})
response<jupyter_output><empty_output><jupyter_text>__Другой пример:__ если захотите спарсить ЦИАН, он начнет вам выдавать капчу. Один из вариантов обхода: менять ip через тор. Однако на практически каждый запрос из-под тора, ЦИАН будет выдавать капчу. Если добавить в запрос `User_Agent`, то капча будет вылезать намного реже.   в) общаться через посредников  Посмотрим на свой ip-адрес без прокси.<jupyter_code>r = requests.get('https://httpbin.org/ip')
print(r.json())<jupyter_output>{'origin': '192.168.3.11, 192.168.3.11'}<jupyter_text>А теперь попробуем посмотреть, что будет если подключить прокси.<jupyter_code>proxies = {
    'http': '172.16.17.32:47592',
    'https': '172.16.17.32:47592'
}

r = requests.get('https://httpbin.org/ip', proxies=proxies)

print(r.json())<jupyter_output>{'origin': '172.16.17.32, 172.16.17.32'}<jupyter_text>Запрос работал немного подольше, ip адрес сменился. Большая часть проксей, которые вы найдёте работают криво. Иногда запрос идёт очень долго и выгоднее сбросить его и попробовать другую проксю. Это можно настроить опцией `timeout`.  Например, так если сервер не будет отвечать секунду, код упадёт.<jupyter_code>import requests
requests.get('http://www.google.com', timeout=1)<jupyter_output><empty_output><jupyter_text>У requests есть довольно много разных интересных примочек. Посмотреть на них можно в [гайде из документации.](https://requests.readthedocs.io/en/master/user/advanced/)__Где можно попытаться раздобыть списки прокси:__ * https://qna.habr.com/q/591069* https://getfreeproxylists.blogspot.com/* Большая часть бесплатных прокси обычно не работает. Пишите парсер, который будет собирать списки из проксей и пытаться применить их.   г) уходить глубже  Можно попытаться обходить злые сервера через тор. Есть аж несколько способов, но мы про это говорить не будем. Лучше подробно почитать в нашей статье на Хабре. Ссылка на неё в конце тетрадки. Ещё в самом начале была. А ещё в середине [наверняка есть.](https://habr.com/ru/company/ods/blog/346632/)  Совместить всё? 1. Начните с малого 2. Если продолжает банить, накидывайте новые примочки3. Каждая новая примочка бьёт по скорости 4. [Разные примочки для requests](http://docs.python-requests.org/en/v0.10.6/user/advanced/)  Хитрости для парсинга :)  Хитрость 1:  Не стесняйтесь пользоваться `try-except`Эта конструкция позволяет питону в случае ошибки сделать что-нибудь другое либо проигнорировать её. Например, мы хотим найти логарифм от всех чисел из списка:<jupyter_code>from math import log 

a = [1,2,3,-1,-5,10,3]

for item in a:
    print(log(item))<jupyter_output>0.0
0.6931471805599453
1.0986122886681098<jupyter_text>У нас не выходит, так как логарифм от отрицательных чисел не берётся. Чтобы код не падал при возникновении ошибки, мы можем его немного изменить:<jupyter_code>from math import log 

a = [1,2,3,-1,-5,10,3]

for item in a:
    try:
        print(log(item))  # попробуй взять логарифм
    except:
        print('я не смог') # если не вышло, сознайся и работай дальше<jupyter_output>0.0
0.6931471805599453
1.0986122886681098
я не смог
я не смог
2.302585092994046
1.0986122886681098<jupyter_text>__Как это использовать при парсинге?__  Интернет создаёт человек. У многих людей руки очень кривые. Предположим, что мы на ночь поставили парсер скачивать цены, он отработал час и упал из-за того, что на како-нибудь одной странице были криво проставлены теги, либо вылезло какое-то редкое поле, либо вылезли какие-то артефакты от старой версии сайта, которые не были учтены в нашем парсере. Гораздо лучше, чтобы код проигнорировал эту ошибку и продолжил работать дальше.   Хитрость 2:  pd.read_htmlЕсли на странице, которую вы спарсили, среди тэгов `` и `` прячется таблица, чаще всего можно забрать её себе без написания цикла, который будет перебирать все стобцы и строки. Поможет в этом `pd.read_html`. Например, вот так можно забрать себе [табличку с сайта ЦБ](https://cbr.ru/currency_base/daily/)<jupyter_code>import pandas as pd

df = pd.read_html('https://cbr.ru/currency_base/daily/', header=-1)[0]
df.head()<jupyter_output><empty_output><jupyter_text>Команда пытается собрать в массив все таблички c веб-страницы. Если хочется, можно сначала через bs4 найти нужную таблицу, а потом уже распарсить её:<jupyter_code>resp = requests.get('https://cbr.ru/currency_base/daily/')
tree = BeautifulSoup(resp.content, 'html.parser')

# нашли табличку
table = tree.find_all('table', {'class' : 'data'})[0]

# распарсили её
df = pd.read_html(str(table), header=-1)[0]
df.head()<jupyter_output><empty_output><jupyter_text>Хитрость 3:  используйте пакет tqdm> Код уже работает час. Я вообще без понятия когда он закончит работу. Было бы круто узнать, сколько ещё ждать... Если в вашей голове возникла такая мысль, пакет `tqdm` ваш лучший друг. Установите его: ```pip install tqdm```<jupyter_code>from tqdm import tqdm_notebook

a = list(range(30))

# 30 раз будем спать по секунде
for i in tqdm_notebook(a):
    time.sleep(1)<jupyter_output><ipython-input-75-b83b2df6bc34>:6: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0
Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
  for i in tqdm_notebook(a):<jupyter_text>Мы обмотали тот вектор, по которому идёт цикл в `tqdm_notebook`. Это даёт нам красивую зелёную строку, которая показывает насколько сильно мы продвинулись по коду. Обматывайте свои самые большие и долгие циклы в `tqdm_notebook` и всегда понимайте сколько осталось до конца.   Хитрость 4:  распаралеливаниеЕсли сервер не очень настроен вас банить, можно распаралелить свои запросы к нему. Самый простой способ сделать это — библиотека `joblib`.<jupyter_code>from joblib import Parallel, delayed
from tqdm import tqdm_notebook

def simple_function(x):
    return x**2


nj = -1 # паралель на все ядра 
result = Parallel(n_jobs=nj)(
                delayed(simple_function)(item)          # какую функцию применяем 
                for item in tqdm_notebook(range(10)))   # к каким объектам применям

# tqdm_notebook в последней строчке будет создавать зелёный бегунок с прогрессом<jupyter_output><empty_output><jupyter_text>На самом деле это не самый эффективный способ паралелить в python. Он жрёт много памяти и работает медленнее, чем [стандартный multiprocessing.](https://docs.python.org/3/library/multiprocessing.html) Но зато две строчки, КАРЛ! Две строчки!   Хитрость 5:  selenium без браузераСелениум можно настроить так, чтобы физически браузер не открывался.<jupyter_code>from selenium import webdriver
from selenium.webdriver.firefox.options import Options

options = Options()
options.headless = True 
driver = webdriver.Firefox(options=options)

ref = 'http://google.com'
driver.get(ref)

driver.close()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08870528037579163, 'fraction_numerical': 0.024577444113576312, 'mean_word_length': 5.697752808988764, 'pattern_counts': {'":': 0, '<': 142, '<?xml version=': 0, '>': 168, 'https://': 21, 'lorem ipsum': 0, 'www.': 5, 'xml': 0}, 'pii_count': 6, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '36', 'dir': 'jupyter-structured-clean-dedup', 'id': '56647', 'license': 'MIT', 'n_tokens_mistral': 9852, 'n_tokens_neox': 10677, 'n_words': 2346, 'path': 'seminars/sem4/sem04_news_parsing-inclass.ipynb', 'repo_name': 'hse-python-jour/hse_intro_to_data_journalism', 'size': '23856'}