Search
Write a publication
Pull to refresh

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

Level of difficultyEasy
Reading time14 min
Views1.2K

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

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

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

Tags:
Hubs:
+8
Comments13

Articles