Зачем журналисту программирование? Ответ простой: чтобы не тратить часы на ручной сбор архивов, а за секунды получать общую картину. Этот проект — ответвление от проекта по анализу данных и основан на анализе новостного контена ГК “Росатом”. На сайте Rosatom.ru новости доступны начиная с января 2024 года. Однако, согласно данным web.archive, на официальном сайте выходят с 2010-2011 года, что перекликается с перезапуском в 2011 году проекта самой крупнотиражной отраслевой газеты — Страна Росатом.
Исключителльно для парсинга данных и работы с HTML-структурой сайта
был использован Python (библиотеки
requests и BeautifulSoup), а для анализа и
визуализации — R. Держа в голове, что библиотека
rvest была вдохновленна не менее известной
Beautiful Soup, именно последняя стала основой парсинга
html страниц.
Загрузка библиотек и основы для парсинга
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'}
Самая кропотливая часть — функция парсинга
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
Впоследствии планируется переход по final_link для
полноценного парсинга и создания полномасштабного датасета текстов самих
статей.
На этом моменте переходим в среду 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…
Пока нет версии 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х символов ИЛИ если это ,более чем 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("для", "все", "что", "уже", "при"))
Для начала посмотрим частотный анализ слов: что чаще всего звучало в корпоративном издании с июня 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 активность генерального директора.
Посмотрик под какими рубриками чаще всего выкладывается контент.
ggplot(rosatom_news, aes(x = Type, fill = Type)) +
geom_bar() +
labs(title = "Какие рубрики популярны?", x = "Рубрика", y = "Кол-во новостей") +
theme_minimal()
Больше свего выделятся рубрика “Важная новость” и “Новость”, первая была
введена на сайте лишь 3 сентября 2025 года, и имеет пока только 3
публикации. Все остальные же разделы живет на сайте примерно с декабря
2018 года (согласно web.archive).
Также можно заметить, что некоторые рубрики удалены, например,
“Событие”.
На дисерт осталось самое инетерсное — сколько часов перерабатывают сотрудники 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!