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

Возможно для вашего проекта/ресерча иногда требовалось собрать большое количество статей из каких-либо источников в виде веб-сайтов. В эпоху больших языковых моделей полноценный сбор информации с сайтов все еще не самый очевидный сценарий, требующий учета многих мелких деталей, а также понимания принципов работы сайта и взаимодействия с ним. В этом случае единственный оптимальный метод сбора такой информации - это парсинг.
В данной статье мы сфокусируемся на парсинге сайтов российских СМИ, в числе которых Meduza,* как официально запрещенное в РФ и более государственно-подконтрольных RussiaToday и Коммерсанта. Как основные инструменты используем классические библиотеки в Python: requests, BeautifulSoup, Selenium. Несмотря на то, что сейчас во многом можно использовать их более современные аналоги по типу Playwrite, который неплохо работает с имитацией поведения пользователя в браузере, для удобства понимания кода большинством читателей сделаем упор на классику.
*признан иноагентом на территории РФ.
Стандартный путь выбора метода и основных инструментов для парсинга представляет собой несколько этапов:
Владелец сайта предоставляет открытый API.
Application Programming Interface - программный интерфейс, который позволяет передавать информацию между двумя программами. В нашем случае проще это описать так: мы делаем запрос на API, и в этом процессе ваш компьютер обращается к сайту, чтобы получить информацию. Например, чтобы отобразить карту на сайте, связанному с недвижимостью, там может быть подключен API Яндекс Карт. В этом с случае, процесс парсинга становится крайне простым и сводится к запросу и выбору необходимой информации из json-файла.Официального API нет. Нужно искать скрытый.
Не самый тривиальный способ, т.к. возможно если вы только начинаете свой путь в парсинге, то скорее всего пропустите такую возможность. Он работает по такому же принципу, как и официальный, но содержится в бекенд части сайта.
Найти его не так сложно. Пару кликов в панели для разработчиков (вкладкаNetwork
) и внимательность.
Далее мы разберем процесс поиска на конкретном примере.BeautifulSoup: парсим обычную HTML-страницу.
Страницы нашего сайта представляют собой нединамический сайт, который можно разобрать по частям как конструктор, вытащив из кода нужную информацию. Отправляем запрос с помощьюrequests
и парсим наш html. Большой минус заключается в том, что структура сайта может быстро меняться: например, названия классов и css-селекторов, по которым мы выбираем условный заголовок, дату и тд. Но это не самый сложный путь развития.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)
Итак, в результате нам удалось спарсить статьи из трех наиболее известных российских СМИ. Теперь можно и узнать как такие влиятельные медиа-ресурсы представляют и позиционируют выборы в США и другие животрепещущие темы, также используя весь функционал питона и его библиотек.
Исходный код парсеров можно найти в этом репозитории.