О проекте “Росатом Новостной Парсинг” / “Rosatom News Parsing” v1.2

Зачем журналисту программирование? Ответ простой: чтобы не тратить часы на ручной сбор архивов, а за секунды получать общую картину. Этот проект — ответвление от проекта по анализу данных и основан на анализе новостного контена ГК “Росатом”. На сайте Rosatom.ru новости доступны начиная с января 2024 года. Однако, согласно данным web.archive, на официальном сайте выходят с 2010-2011 года, что перекликается с перезапуском в 2011 году проекта самой крупнотиражной отраслевой газеты — Страна Росатом.

1. Сбор данных (Python)

Исключителльно для парсинга данных и работы с HTML-структурой сайта был использован Python (библиотеки requests и BeautifulSoup), а для анализа и визуализации — R. Держа в голове, что библиотека rvest была вдохновленна не менее известной Beautiful Soup, именно последняя стала основой парсинга html страниц.

1.1. Настройка инструментов

Загрузка библиотек и основы для парсинга

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re

BASE_URL = 'https://rosatom.ru'
NEWS_LIST_URL = 'https://rosatom.ru/journalist/news/'
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}

1.2. Логика извлечения данных

Самая кропотливая часть — функция парсинга parse_rosatom_news. Внутри нее спрятана функция fetch_page, которая забирает содержимое страницы. Основной цикл проходит по списку новостей на каждой отдельной странице. Здесь мы ищкм теги div с названиями классов whiteBG и purpleBG — именно в них завернуты публикации на сайте. Запаситесь терпением на +-5 минут)

def parse_rosatom_news(pages_for_parse=1):
    def fetch_page(url):
        response = requests.get(url, headers=HEADERS)
        pause = random.uniform(4, 6)
        time.sleep(pause)
        return BeautifulSoup(response.text, 'html.parser')

    news_data = []

    for page_num in range(1, pages_for_parse + 1): 
        current_url = f"{NEWS_LIST_URL}?PAGEN_1={page_num}"

        soup = fetch_page(current_url)
        articles = soup.find_all('div', class_=['whiteBG', 'purpleBG'])
        for article in articles:
            link_tag = article.find('a', href=True)
            link = link_tag['href']
            final_link = BASE_URL + link if link.startswith('/') else link

# Для каждой новости мы вытаскиваем ссылку, заголовок, рубрику и разделяем дату со временем
            date_container = article.find('span', class_='right')
            full_date_text = date_container.get_text(separator='|').strip()
            parts = full_date_text.split('|')

            if len(parts) >= 1:
                date_val = parts[0].strip()
            if len(parts) >= 2:
                time_val = parts[1].strip()

            type_tag = article.find('span', class_='block-info__section')
            news_type = type_tag.text.strip()

            title_tag = article.find('span', class_='title')
            title = title_tag.text.strip()

            news_data.append({
                'Date': date_val,
                'Time': time_val,
                'Type': news_type,
                'Title': title,
                'Link': final_link})

# После обработки каждой страницы делаем паузу
        pause = random.uniform(7, 9)
        time.sleep(pause)

    return news_data

# Указываем кол-во страниц для парсинга
data = parse_rosatom_news(pages_for_parse=20)

# В конце упаковываем все в `pandas` и проверяем, сколько строк удалось собрать
df = pd.DataFrame(data)
df = df[['Date', 'Time', 'Type', 'Title', 'Link']]
df.to_csv('rosatom_news_archive.csv', index=False, encoding='utf-8')

print(f"Собрано новостей: {len(df)}")
## Собрано новостей: 640

1.3. Дальнейшие версии

Впоследствии планируется переход по final_link для полноценного парсинга и создания полномасштабного датасета текстов самих статей.

2. Анализ данных (R)

На этом моменте переходим в среду R. Что содержится в нашем датасете:

Date — Дата добавления новости на сайт;

Time — Время добавления новости на сайт (24х часовой формат);

Type — Новостная рубрика;

Title — Заголовок новостного текста;

Link — Сслыка на страницу отдельной новости.

library(tidyverse)

rosatom_news <- read.csv('rosatom_news_archive.csv')
tibble(head(rosatom_news, 3))
## # A tibble: 3 × 5
##   Date            Time  Type        Title                                  Link 
##   <chr>           <chr> <chr>       <chr>                                  <chr>
## 1 19 декабря 2025 17:00 Новость     «Росатом» и Комиссия по атомной энерг… http…
## 2 19 декабря 2025 14:00 Новость дня Кольская АЭС досрочно выполнила задан… http…
## 3 17 декабря 2025 13:00 Новость дня Состоялась церемония награждения лаур… http…

2.1. Токинизация и работа с датафреймом

Пока нет версии 1.3. (с парсингом текста самих статей), мы поработвем с текстами заголовков, но принцип самой токенизации и визуализиции не отличается.

library(tidytext)
library(ggplot2)
library(dplyr)

# Убираем мусор (цифры, ковычки), особое внимание к "-", лучшего варианта сохранения пока не нашел
rosatom_news$Title_Clean <- rosatom_news$Title |>
  str_replace_all("([а-яА-ЯёЁa-zA-Z])-([а-яА-ЯёЁa-zA-Z])", "\\1_\\2") |> 
  str_remove_all("[^а-яА-ЯёЁa-zA-Z0-9\\s_]") |>
  str_trim()
  
# Разбиваем заголовки на слова (важно не потерять отраслевые сокращения типа "АЭС", "Эль-Дабаа")
tokens <- rosatom_news |>
  unnest_tokens(word, Title_Clean, to_lower = FALSE)

2.2. Умный фильтр

Еще одна задача — не потерять льраслевые сокращения (которых приличное множество) при удалении предлогов. Мы оставляем слово, если его длина больше 2х символов ИЛИ если это ,более чем 1-буквенная аббревиатура (все буквы заглавные).

tokens_filtered <- tokens |>
  mutate(word = str_replace_all(word, "_", "-")) |> # Вернули "-"
  filter(nchar(word) > 2 | # Убрали союзы, предлоги и другие короткие формы
      (nchar(word) > 1 & str_detect(word, "^[[:upper:]А-ЯЁ]{2,}$")) | str_detect(word, "-")) # Оставили все капсы от 2х букв

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

tokens_final <- tokens_filtered |>
  mutate(word = if_else(str_detect(word, "^[[:upper:]А-ЯЁ\\-]+$"), 
                        word,          # Если капс от 1 букв и выше — оставляем как есть
                        tolower(word)  # Остальное — в нижний регистр
  )) |>
  # Убираем часть слов вручную 
  filter(!word %in% c("для", "все", "что", "уже", "при")) 

2.3. Создание простой визуализации по частотности встречаемых слов

Для начала посмотрим частотный анализ слов: что чаще всего звучало в корпоративном издании с июня 2024 по декабрь 2025.

tokens_final |>
  count(word, sort = TRUE) |>
  head(30) |>
  ggplot(aes(x = reorder(word, n), y = n)) +
  geom_col(fill = "#005596") + # Корпоративный синий цвет)
  coord_flip() +
  labs(title = "Топ-30 слов за 2024-25", x = "Слово", y = "Кол-во упоминаний") +
  theme_minimal()

Само собой, на первых строчках внутренние SEO упоминания, главный экспортный продукт госкорпорации и PR активность генерального директора.

2.2. Создание простой визуализации распределения рубрик

Посмотрик под какими рубриками чаще всего выкладывается контент.

ggplot(rosatom_news, aes(x = Type, fill = Type)) +
  geom_bar() +
  labs(title = "Какие рубрики популярны?", x = "Рубрика", y = "Кол-во новостей") +
  theme_minimal()

Больше свего выделятся рубрика “Важная новость” и “Новость”, первая была введена на сайте лишь 3 сентября 2025 года, и имеет пока только 3 публикации. Все остальные же разделы живет на сайте примерно с декабря 2018 года (согласно web.archive). Также можно заметить, что некоторые рубрики удалены, например, “Событие”.

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

На дисерт осталось самое инетерсное — сколько часов перерабатывают сотрудники Rosatom (2025) (как будто отложенной отправки пока не изобрели). Время публикации новости возьмем как за последнюю минуту прибывания в офисе, а за рабочее время примим 9:00 - 18:00, то есть с 540 до 1080 минут от начала суток.

overtime_work <- rosatom_news |>
  mutate(h = as.numeric(str_extract(Time, "^[0-9]{2}")), m = as.numeric(str_extract(Time, "[0-9]{2}$")), # Вытаскиваем часы и минуты
    total_min = h * 60 + m, status = if_else(total_min >= 540 & total_min <= 1080, "Рабочее время", "Переработка"),
    over_min = case_when(total_min > 1080 ~ total_min - 1080, # Если после 18:00, но до полуночи
      total_min < 540 ~ total_min + 360, # Если после полуночи, но до 9:00, то это 6 часов вчерашней переработки
      TRUE ~ 0)) # В рабочее время переработка = 0

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

overtime_work |>
  group_by(h) |>
  summarise(total_overtime_h = sum(over_min) / 60) |> # Суммируем минуты и переводим в часы
  ggplot(aes(x = h, y = total_overtime_h)) +
  geom_col(aes(fill = total_overtime_h > 0)) + # Только переработки
  labs(title = "Где спрятаны переработки?", x = "Время публикации", y = "Накопленные часы переработки") +
  theme_minimal() 

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

До встречи на v.1.3!

Библиография

Rosatom. 2025. “Official Website.”