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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Разбираем на примере 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/


Интересные статьи

Интересные статьи

Прочитал тут недавно, что специалисты из Си-Ай-Эй создали (или создают) цифровых двойников мировых лидеров в целях прогнозирования их поведения в различиных геополитических реалиях
В повседневной жизни мы слышим целый мир записанных звуков — от сигнала будильника до музыки из плейлиста и голосовых сообщений. Но технология записи звука существует всего 150 лет. До середины XIX ...
Для каких целей? На момент возникновения идеи было еще не совсем понятно. Но предпосылки развития идеи были
Хочу поделиться опытом автоматизации экспорта заказов из Aliexpress в несколько CRM. Приведенные примеры написаны на PHP, но библиотеки для работы с Aliexpress есть и для...
Статья о том, как упорядочить найм1. Информируем о вакансии2. Ведём до найма3. Автоматизируем скучное4. Оформляем и выводим на работу5. Отчитываемся по итогам6. Помогаем с адаптацией...