Парсинг российских СМИ

от автора

Разбираем на примере Russia Today, Коммерсант и Meduza*

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

В данной статье мы сфокусируемся на парсинге сайтов российских СМИ, в числе которых Meduza,* как официально запрещенное в РФ и более государственно-подконтрольных RussiaToday и Коммерсанта. Как основные инструменты используем классические библиотеки в Python: requests, BeautifulSoup, Selenium. Несмотря на то, что сейчас во многом можно использовать их более современные аналоги по типу Playwrite, который неплохо работает с имитацией поведения пользователя в браузере, для удобства понимания кода большинством читателей сделаем упор на классику.

*признан иноагентом на территории РФ.

Стандартный путь выбора метода и основных инструментов для парсинга представляет собой несколько этапов:

  1. Владелец сайта предоставляет открытый API.

    Application Programming Interface — программный интерфейс, который позволяет передавать информацию между двумя программами. В нашем случае проще это описать так: мы делаем запрос на API, и в этом процессе ваш компьютер обращается к сайту, чтобы получить информацию. Например, чтобы отобразить карту на сайте, связанному с недвижимостью, там может быть подключен API Яндекс Карт. В этом с случае, процесс парсинга становится крайне простым и сводится к запросу и выбору необходимой информации из json-файла.

  2. Официального API нет. Нужно искать скрытый.

    Не самый тривиальный способ, т.к. возможно если вы только начинаете свой путь в парсинге, то скорее всего пропустите такую возможность. Он работает по такому же принципу, как и официальный, но содержится в бекенд части сайта.
    Найти его не так сложно. Пару кликов в панели для разработчиков (вкладка Network) и внимательность.
    Далее мы разберем процесс поиска на конкретном примере.

  3. BeautifulSoup: парсим обычную HTML-страницу.

    Страницы нашего сайта представляют собой нединамический сайт, который можно разобрать по частям как конструктор, вытащив из кода нужную информацию. Отправляем запрос с помощью requests и парсим наш html. Большой минус заключается в том, что структура сайта может быстро меняться: например, названия классов и css-селекторов, по которым мы выбираем условный заголовок, дату и тд. Но это не самый сложный путь развития.

  4. Selenium: динамический сайт с JS-элементами.

    В современном интернете большинство сайтов именно такие. Содержимое страницы генерируется автоматически с JavaScript кодом. При отправки запроса на получение html мы уже сталкиваемся с тем, что защита сайта распознает робота и не подгружает нам страницу. В этом случае, необходимо имитировать поведение пользователя, и самая классическая библиотека, помогающая это реализовать — Selenium. При помощи Chrome-драйвера библиотека подгружает страницу сайта и с помощью различных опций по устранению возможности быть распознанным сайтом как робот в итоге получаем html страницу, которую далее парсим с BeautifulSoup.


Начнем с кейса Медузы*.

Медуза* не предоставляет официальный API, поэтому начинаем поиск скрытого.
Заходим на сайт и вбиваем в поиск ключевые слова нужных статей. Традиционно начинаем исследование с клика левой кнопки мыши и нажатию на 'inspect'. Получаем исходный код HTML‑страницы.

В верхней панели ищем вкладку Network , здесь открывается весь бекенд сайта и его механизм работы.

Теперь нам доступен список запросов к сайту (колонка под Name), которые обязательно выполняются при загрузки сайта. Среди них по названию можно примерно понять, что он делает и найти нужный нам API запрос. Обычно это что-то типо search, news с возвращаемым json файлом в response.

Внимательно поищем желанный, скрытый API, находим get-запрос new_search,смотрим на параметры и response, который он возвращает. В нем видим json-ответ, это то, что нам нужно.

Создаем класс MeduzaParser в Python, он будет содержать все необходимые методы для парсинга новостей.

Инициализируем наши аргументы в методе init.

class MeduzaParser:     def __init__(self, phrase_search: str):         self.phrase_search = phrase_search         self.headers = {'User-Agent': random.choice(user_agents)}         self.api = 'https://meduza.io/api/w5/new_search'         self.proxies = {                     'http': proxy,                     'https': proxy,         }         self.request_body = {'term': self.phrase_search,                              'per_page': 15,                              'locale': 'ru'} 

В self.headers для уменьшения вероятности быть заблокированным спустя несколько запросов прописываем User-Agent, который поможет маскировать название нашего браузера при отправки запроса. Предварительно загружаем список случайных юзер-агентов, которые можно найти например в этом репозитории. Из них рандомно выбирается один случайный, и так для каждого запроса. В self.request_body определяем параметры запроса: ключевая фраза/слово по которому будем искать, число статей при одном запросе и язык.

Медуза является запрещенной в России организацией, поэтому для отправки запроса немаловажным аспектом является использование прокси-сервера. С российского API-адреса вы не получите ответ после отправки запроса. Оптимальный вариант — использование пула прокси-серверов с их динамической сменой, чтобы снизить риск блокировки.

Далее необходимо добавить метод для отправки запроса на API через библиотеку requests. Так как максимальное количество статей в запросе — 15, необходима итерация по страницам, поэтому на вход принимаем аргумент page.
С блоком try - except оставляем пять попыток получить ответ в виде json объекта c основными ключами _count — количество статей в запросе, collection — ссылки на все статьи и documents — метаданные по статьям.

def send_request(self, page: int) -> Optional[tuple]:     '''Get the json response from the Meduza     hidden API.     '''     params = {         **self.request_body,         'page': page     }     for attempt in range(5):         try:             response = requests.get(self.api, params=params,                                      proxies=self.proxies, headers=self.headers,                                      verify=False).json()             print('Got a response')             return response['documents'], response['_count']                      except Exception as e:             print(f"Can't get a response for api request - {e}")             continue                  return None 

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

{'datetime': 1730966709,  'layout': 'simple',  'mobile_layout': 'simple',  'source': {'trust': 0},  'title': 'Зеленский рассказал, что созвонился с\xa0Трампом',  'url': 'news/2024/11/07/zelenskiy-rasskazal-chto-sozvonilsya-s-trampom',  'version': 3} 

И так ответ, содержащий метаданные о всех файлах в статье:

Скрытый текст
{'datetime': 1730712566,  'image': {'base_urls': {'elarge_url': '/image/attachments/images/010/580/173/elarge/jErsR06_OK513LOROdBzmA.jpg',                          'is1to2': '/image/attachment_overrides/images/010/580/173/ov/ujms6916u68Ql64Av-qOgQ.jpg',                          'is1to3': '/image/attachments/images/010/580/173/wh_810_540/jErsR06_OK513LOROdBzmA.jpg',                          'is1to4': '/image/attachments/images/010/580/173/wh_810_540/jErsR06_OK513LOROdBzmA.jpg',                          'isMobile': '/impro/yMxV8ai5e8gtkOeFaQl8_Oo16DkSTO50yiB9sBoO2_Y/resizing_type:fit/width:782/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL2xhcmdl/L2pFcnNSMDZfT0s1/MTNMT1JPZEJ6bUEu/anBn.jpg',                          'wh_300_200_url': '/image/attachments/images/010/580/173/wh_300_200/jErsR06_OK513LOROdBzmA.jpg',                          'wh_405_270_url': '/image/attachments/images/010/580/173/wh_405_270/jErsR06_OK513LOROdBzmA.jpg'},            'cc': 'default',            'credit': 'Chip Somodevilla / Getty Images',            'display': 'default',            'elarge_url': '/image/attachments/images/010/580/173/elarge/jErsR06_OK513LOROdBzmA.jpg',            'gradients': {'bg_rgb': '0,0,0', 'text_rgb': '255,255,255'},            'height': 890,            'is1to1': '/image/attachment_overrides/images/010/580/173/ov/eYU01BuHD8uQXl7fyONPRA.jpg',            'is1to2': '/image/attachment_overrides/images/010/580/173/ov/ujms6916u68Ql64Av-qOgQ.jpg',            'is1to3': '/image/attachments/images/010/580/173/wh_810_540/jErsR06_OK513LOROdBzmA.jpg',            'is1to4': '/image/attachments/images/010/580/173/wh_810_540/jErsR06_OK513LOROdBzmA.jpg',            'isMobile': '/impro/yMxV8ai5e8gtkOeFaQl8_Oo16DkSTO50yiB9sBoO2_Y/resizing_type:fit/width:782/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL2xhcmdl/L2pFcnNSMDZfT0s1/MTNMT1JPZEJ6bUEu/anBn.jpg',            'mobile_ratio': 1.5,            'optimised_urls': {'elarge_url': '/impro/CGnSJrpx94CGYSVB0j-fJs9sWbbgasIZu8vWxkuXQ9g/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL2VsYXJn/ZS9qRXJzUjA2X09L/NTEzTE9ST2RCem1B/LmpwZw.webp',                               'is1to2': '/impro/JRRhxXheqdVXhOGWMqCUjaTnN38eRZktbG3PWBzbrHo/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudF9v/dmVycmlkZXMvaW1h/Z2VzLzAxMC81ODAv/MTczL292L3VqbXM2/OTE2dTY4UWw2NEF2/LXFPZ1EuanBn.webp',                               'is1to3': '/impro/FnOI0FOccrlOdodgEMg16-8XwitHiGEQXGfWx-9KpUg/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL3doXzgx/MF81NDAvakVyc1Iw/Nl9PSzUxM0xPUk9k/QnptQS5qcGc.webp',                               'is1to4': '/impro/FnOI0FOccrlOdodgEMg16-8XwitHiGEQXGfWx-9KpUg/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL3doXzgx/MF81NDAvakVyc1Iw/Nl9PSzUxM0xPUk9k/QnptQS5qcGc.webp',                               'isMobile': '/impro/A3SRAMBVlzsLmMAArkNbFtHqIqGx8pB8DuyG7GJzvjc/resizing_type:fit/width:782/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL2xhcmdl/L2pFcnNSMDZfT0s1/MTNMT1JPZEJ6bUEu/anBn.webp',                               'wh_300_200_url': '/impro/6c6c9Re0Lbi_sSLgqEHC5oQBbnLE_9QlzT6Afoq3Og0/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL3doXzMw/MF8yMDAvakVyc1Iw/Nl9PSzUxM0xPUk9k/QnptQS5qcGc.webp',                               'wh_405_270_url': '/impro/FBtTpPfA2tzXzdFqpOW1mUF9pVHS7EMzUHFG1nB_01I/resizing_type:fit/width:0/height:0/enlarge:1/quality:80/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAxMC81/ODAvMTczL3doXzQw/NV8yNzAvakVyc1Iw/Nl9PSzUxM0xPUk9k/QnptQS5qcGc.webp'},            'show': True,            'wh_1245_500_url': '/image/attachments/images/010/580/173/wh_1245_500/jErsR06_OK513LOROdBzmA.jpg',            'wh_300_200_url': '/image/attachments/images/010/580/173/wh_300_200/jErsR06_OK513LOROdBzmA.jpg',            'wh_405_270_url': '/image/attachments/images/010/580/173/wh_405_270/jErsR06_OK513LOROdBzmA.jpg',            'wh_810_540_url': '/image/attachments/images/010/580/173/wh_810_540/jErsR06_OK513LOROdBzmA.jpg',            'width': 1335},  'layout': 'rich',  'mobile_layout': 'rich',  'mobile_theme': '255,255,255',  'second_title': 'Выпуск рассылки «Сигнал» на\xa0«Медузе»',  'tag': {'name': 'истории', 'path': 'articles'},  'title': 'Дональд Трамп требует честных выборов\xa0— а\xa0его обвиняют в\xa0'           'подрыве демократии. Так возможны\xa0ли фальсификации в\xa0США?',  'url': 'feature/2024/11/04/donald-tramp-trebuet-chestnyh-vyborov-a-ego-obvinyayut-v-podryve-demokratii-tak-vozmozhny-li-falsifikatsii-v-ssha',  'version': 3}

Теперь необходимо пропарсить json и вытащить информацию о названии, дате, типе статьи и саму ссылку на статью.

Получить информацию о дате будет довольно трудоемким процессом, поэтому ее, как и основной текст страницы мы получим с помощью парсинга html-страницы.

Создаем новый метод parse_article, который на вход примет ссылку на статью и данные в виде словаря.

Сначала выбираем ключ ‘tag’ из словаря, он представлен не в каждом блоке информации о статье, поэтому оборачиваем в try-except. По наблюдению можно понять, что тег отсутствует в рублике ‘news’ поэтому дадим значение самостоятельно.
Вынимаем заголовок и чистим от символа-знака пробела.

def parse_article(self, link: str, data: dict) -> Optional[dict]:     '''Get article metadata and main text.     Using BeautifulSoup to parse html page.     '''     try:         tag = data['tag']     except KeyError:         tag = 'новости'              title = data['title'].replace('\xa0', ' ')      url = 'https://meduza.io/' + link     html = self.get_page(url)     if not html:         return None              soup = BeautifulSoup(html, 'html.parser')              date = None     date_tag = soup.find('time',                           attrs={'data-testid': 'timestamp'})     if date_tag:         date_str = date_tag.text         if 'назад' not in date_str:             date = str(datetime.strptime(date_str.split(', ')[1],                                               '%d %B %Y').date())     text = None     text_tag = soup.find('div', class_=[         'GeneralMaterial-module-article',          'SlidesMaterial-module-slides'     ])     if text_tag:         text = text_tag.text.replace('\xa0', ' ')       article_data = {         'title': title,         'tag': tag,         'date': date,          'link': url,         'text': text     }                       return article_data 

Теперь нужно получить дату и текст статьи. В ход идет BeautifulSoup. Создадим отдельный метод get_page для получения страницы и передадим в объект soup.

 def get_page(self, link: str) -> Optional[str]:         '''Get the html page of article         from url request.         '''         try:             html = requests.get(link, proxies=self.proxies,                                                  headers=self.headers).text             return html                      except Exception as e:             print(f"Can't get a response from page")             return None 

Ищем дату на странице по тегу time и атрибуту data-testid. В процессе написания парсера объект супа не сразу находил нужный тег, поэтому в этом случае необходима тщательная обработка значения. Если дата находится, то в некоторых статьях медузы, а точнее в подкастах дата представлена не в формате ‘dd-mm-YYYY’, а строкой ‘n месяцев назад’, точной даты не получить, поэтому в таких типах статей вместо даты оставим пропущенное значение.

Находим текст статьи по одному из классов, из-за разных рубрик статей, текст может быть либо в 'GeneralMaterial-module-article' либо в 'SlidesMaterial-module-slides'.

... # продолжение метода parse_article  url = 'https://meduza.io/' + link     html = self.get_page(url)     if not html:         return None              soup = BeautifulSoup(html, 'html.parser')              date = None     date_tag = soup.find('time',                           attrs={'data-testid': 'timestamp'})     if date_tag:         date_str = date_tag.text         if 'назад' not in date_str:             date = str(datetime.strptime(date_str.split(', ')[1],                                               '%d %B %Y').date())     text = None     text_tag = soup.find('div', class_=[         'GeneralMaterial-module-article',          'SlidesMaterial-module-slides'     ])     if text_tag:         text = text_tag.text.replace('\xa0', ' ')       article_data = {         'title': title,         'tag': tag,         'date': date,          'link': url,         'text': text     }                       return article_data 

Сохраняем полученные данные в словарь article_data и возвращаем его.

В методе page_iterate будет содержаться весь алгоритм работы парсера. Его задача — итерироваться по страницам и получить данные запроса по каждой из них, для каждой статьи запроса получить данные по ней и сохранить в лист articles_data.

Создаем бесконечный цикл while True, где с каждой итерацией число страниц будет возрастать на один и цикл будет обрываться только в момент получения ответа от запроса, где статей уже нет. То есть в этот момент парсер собрал все статьи и их больше нет.

def page_iterate(self) -> list:   '''Get all articles as a DataFrame with    the use of page iteration.   '''      articles_data = []   articles_total = 0   page = 0      while True:       data = self.send_request(page)        if data is None:           print(f'Got None for this request on page {page}')           page += 1           continue        article_data, articles_num = data                  if  articles_num == 0:           print('No more articles found')           break              for link, metadata in article_data.items():           parsed_data = self.parse_article(link, metadata)           if parsed_data:               articles_data.append(parsed_data)                  articles_total += articles_num       page += 1                  print(f'Parsed {articles_total} articles')              return pd.DataFrame(articles_data) 

По итогу сохраняем полученный результат в DataFrame. Создаем объект класса, вызываем финальный метод page_iterate и получаем такой результат.

CPU times: user 21.9 s, sys: 1.89 s, total: 23.8 s Wall time: 3min 27s 

По запросу ‘выборы в сша’ у нас собралось 486 статей за 3.5 минуты.


В случае RussiaToday, открыв вкладку разработчика в поисках нужного запроса единственным похожим на что-то, содержащее информацию о статьях является запрос search?. Но возвращает он не сырой json, a html страницу, что не совсем удобно для нас. Тут можно принять факт отсутствия удобного формата и вооружиться BeautifulSoup, но если присмотреться внимательнее, в payload можно увидеть параметр API запроса ‘format’, который как раз и принимает аргументом ‘json’. Это наше решение.

По аналогии с парсером медузы создадим class RussiaTodayParser с теми же атрибутами метода init, за исключением параметров запроса и прокси-сервера, тут он не понадобится. Здесь к нему добавятся такие параметры как df и dt, отвечающие за даты с которой по какое необходимо получить статьи, и format, где указываем json.

self.params = {             'q': query,             'df': date_from,             'dt': date_to,             'pageSize': 100,             'format': 'json'         } 
 def get_metadata(self, article: dict) -> dict:     '''Get metadata of article'''     link = f'{self.base_url}{article['href']}'     text = self.get_article_text(link)          category = article['category'] if 'category' in article else None     date = str(datetime.fromtimestamp(             int(article['date'])).date())          article_data = {         'id': article['id'],         'link': link,         'date': date,         'type': article['type'],         'category': category,         'title': article['title'],         'summary': article['summary'],         'text': text      }      return article_data 

Текст получаем в отдельном методе, где он находится по классу на HTML-странице. Не забываем перевести дату из формата timestamp. Последний метод iterate_pages, позволяющий собрать весь механизм работы парсера можно увидеть в гитхаб репозитории, его механизм также похож на тот, что мы видели в предыдущем парсере.


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

В классе KommersantParser создаем метод get_driver, где определим базовые опции ChromeDriver.
При инициализации драйвера чтобы минимизировать ошибки и вероятность обнаружения робота используем дополненую версию ChromeDriver — undetected chrome-driver , который автоматически предотвращает быть пойманным такими системами защиты как: Cloudflare, Distil Networks.

Опция '--no-sandbox' поможет снизить вероятность ошибок или сбоев, вызванных песочницей — места, где браузер проходит проверки безопасности операционной системы. --headless=new отключит автоматический вызов окна браузера при каждой попытке получения страницы драйвером.

 def get_driver(self):     '''Getting ChromeDriver to imitate      user behaviour in browser.     '''     options = uc.ChromeOptions()     options.add_argument("--no-sandbox")     options.add_argument("--headless=new")     options.add_argument(f'--user-agent={random.choice(user_agents)}')     driver = uc.Chrome(version_main=138, options=options)     return driver 

В первую очередь, нужно собрать ссылки со всех страниц, для этого необходимо получение драйвером всех доступных страниц запроса.
Создаем метод get_links, перед тем, как дать возможность получить страницу, пропишем функцию delete_all_cookies(), она поможет избежать ошибки “no such window: target window already closed”, которая связанна с наличием файлов куки от прошлого запроса. Т.к. мы неоднократно отправляем запросы, то не очищенные куки с прошлого являются помехой.

Прописываем функцию wait для прогрузки всего блока статей и ищем его по css-селектору.

def get_links(self, url: str) -> Optional[list]:     '''Getting articles links on website      page.     '''     links = []     driver = self.get_driver()          try:         driver.delete_all_cookies()          driver.get(url)         wait(driver, 10).until(             EC.visibility_of_element_located((By.CSS_SELECTOR,                                                'article.uho')))         articles = driver.find_elements(             By.CSS_SELECTOR, 'article.uho')              except TimeoutException:         return None          for article in articles:         link = article.find_element(             By.CSS_SELECTOR, 'a.uho__link').get_attribute('href')         links.append(link)          driver.close()        return links 

Далее из каждого блока html-кода статьи вынимаем ссылку.

Теперь при помощи BeautifulSoup прописываем метод парсинга статей исходя из полученных ссылок, здесь ничего нового:

def select_part(self, soup, css_selector: str) -> Optional[str]:     page_object = soup.select_one(         css_selector).text.strip()     return page_object if page_object else ''           def get_metadata(self, article_link: str) -> dict:     '''Getting the metadata and article      text.     '''     html = requests.get(article_link).text     soup = BeautifulSoup(html, 'html.parser')      title = self.select_part(soup,                               'h1.doc_header__name')     date = self.select_part(soup,                               'time.doc_header__publish_time')     text = self.select_part(soup,                               'div.doc__body')          article_data = {         'title': title,         'date': date,         'link': article_link,         'text': text     }          return article_data 

У коммерсанта максимальное количество страниц с ссылками на статьи, которые мы можем получить это — 100. Чтобы не потерять наши статьи, сначала разобъем наш временной промежуток на более мелкие с дельтой равной месяцу. И для каждого месяца будем делать отдельный запрос.
В этом же цикле у нас получается второй вложенный, где уже идет итерация по страницам. На выходе получаем список из всех ссылок на статьи.

def iterate_pages(self) -> list:     '''Iteration through the date      range and pages     '''     article_links = []     dates = pd.date_range(         self.df, self.dt, freq='31D')     dates = dates.strftime('%Y-%m-%d').tolist()          for i in tqdm(range(1, len(dates))):         print(dates[i])         params = {             **self.params,             'datestart': dates[i-1],             'dateend': dates[i]         }          for page in range(1, 101):             request_payload = {                 **params,                 'page': page             }             url = self.base_url + urllib.parse.urlencode(                 request_payload)             links = self.get_links(url)                          if links:                 article_links.extend(links)             else:                 break                      print(f'Found {len(article_links)} article links.')               return article_links 

И наконец полученные всех данных о статьях при помощи последнего метода get_articles:

def get_articles(self) -> pd.DataFrame:     '''Getting the final results     with articles in DataFrame     '''     articles_data = []     article_links = self.iterate_pages()     for link in article_links:         data = self.get_metadata(link)         if data:             articles_data.append(data)              return pd.DataFrame(articles_data) 

Итак, в результате нам удалось спарсить статьи из трех наиболее известных российских СМИ. Теперь можно и узнать как такие влиятельные медиа-ресурсы представляют и позиционируют выборы в США и другие животрепещущие темы, также используя весь функционал питона и его библиотек.

Исходный код парсеров можно найти в этом репозитории.


ссылка на оригинал статьи https://habr.com/ru/articles/930188/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *