Перейти к основному содержимому

Скачивание отчётов Казначейства (исполнение бюджетов)

Зачем и для кого

Инструкция для аналитиков и разработчиков, которым нужны официальные файлы исполнения бюджета (месячные и годовые формы, долговые книги) с портала Федерального казначейства: это основной публичный вход к кассовому факту и сверке с плановыми срезами ГИИС «Электронный бюджет». На отдельных лендингах (например федеральный бюджет) отчёты часто отдают архивами ZIP, внутри которых лежат XLS / XLSX / PDF и иные вложения. Единого документированного API «все формы» нет: типичный путь — HTML-навигация или обход ссылок на странице раздела; для части показателей удобнее сочетать раздел с каталогом opendata и витринами datamarts.

Входные данные

  • Канонический вход в разделhttps://roskazna.gov.ru/ispolnenie-byudzhetov/ (отчёты, долговые книги, вложения по ссылкам внутри тематических подстраниц).
  • Тематический лендинг с ZIP — например федеральный бюджет: в интерфейсе переключаются ежемесячные, оперативные, ежеквартальные и ежегодные отчёты; в выдаче страницы (SSR) обычно видны карточки активного типа, а сами файлы часто лежат по путям вида /storage/page-files/.../*.zip (плюс вызовы Livewire downloadFile('...') с тем же путём).
  • Код или наименование формы — например 0507011 (часто месячные своды), 0503117 / 0503317 (годовые срезы; см. карточки отчётности и /reporting/0503317); точный перечень и подписи — по актуальному оглавлению на портале на дату выгрузки.
  • Разрезы для сверкифинансовый год, уровень бюджета (федеральный / консолидированный субъекта), при необходимости КБК из заголовков таблицы в файле.
  • Зафиксируйте дату скачивания и URL — меню и прямые ссылки подразделов периодически обновляют (см. карточку источника).

Инструменты

  • Браузер — для поиска нужной формы по смыслу и ручного скачивания вложений (переключение «ежемесячный / квартальный / …» на лендинге может подгружать список через Livewire, без полного обхода в простом GET).
  • Python 3 с пакетом requests; для разбора ссылок в статическом HTML достаточно re и urllib.parse.quote (кириллица в имени файла в URL должна кодироваться по сегментам пути). Для распаковки вложений — стандартный модуль zipfile и каталог выгрузки (pathlib).
  • Табличный редактор (Excel, LibreOffice) — для первичного осмотра XLS/XLSX и проверки кодировок и заголовков.

Шаги

  1. Откройте раздел Исполнение бюджетов и выберите тематический блок (федеральный бюджет, консолидированные бюджеты субъектов, ГВФ и т.д.) — см. навигацию в карточке источника.
  2. Найдите нужный отчётный период и форму по названию или коду в оглавлении подраздела; перейдите на страницу формы и скачайте вложение ZIP и/или прямые XLS / XLSX / PDF (внутри ZIP часто лежит одна таблица или пакет файлов).
  3. Если задача машинная и стабильный прямой URL неизвестен: запросите HTML целевой страницы (или сохраните его из браузера), извлеките ссылки на .zip и прямые .xls / .xlsx / .pdf (в т.ч. из атрибутов href и из строк downloadFile('storage/...zip') на лендинге федерального бюджета), соберите абсолютный URL с кодированием пути (https://roskazna.gov.ru + процент-кодирование каждого сегмента имени), скачайте ZIP и просмотрите ZipFile.namelist() перед распаковкой.
  4. Для регулярной выгрузки рассмотрите каталог открытых данных Казначейства и datamarts — там иная модель доступа, но проще фиксировать паспорт набора и версию файла.
  5. При аналитике сопоставляйте показатели с наборами budget.gov.ru только в одинаковом определении показателя, периоде и методике (см. казначейское исполнение).

Воспроизводимый пример

Ниже — самодостаточный скрипт для лендинга исполнение федерального бюджета. Он:

  1. Загружает HTML и определяет активный тип отчёта в выпадающем фильтре (например «ежемесячный»), который отражён в разметке на момент ответа сервера.
  2. Собирает пути к ZIP из href="/storage/...zip" и из вызовов downloadFile('storage/...zip').
  3. Скачивает один (самый свежий по имени файла) архив, выводит манифест zipfile и распаковывает вложения в каталог на диске.

Имена ссылок и перечень карточек зависят от вёрстки и выбранного фильтра; другие разрезы (квартальные, годовые и т.д.) при необходимости снимайте после переключения фильтра в браузере (повторный GET без эмуляции Livewire может не содержать полного списка).

from __future__ import annotations

import re
import zipfile
from io import BytesIO
from pathlib import Path
from urllib.parse import quote, urljoin

import requests

# Лендинг «Федеральный бюджет» (ежемесячные / оперативные / квартальные / годовые — переключатель в UI).
PAGE_URL = "https://roskazna.gov.ru/ispolnenie-byudzhetov/federalnyj-byudzhet"
BASE_ORIGIN = "https://roskazna.gov.ru"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (compatible; finguide-howto/1.0; "
"+https://github.com/infoculture/finguide)"
),
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
}

# Прямая ссылка в карточке и дублирующий путь в обработчике Livewire на той же странице.
HREF_ZIP_RE = re.compile(
r'href=["\'](/storage/[^"\']+\.zip(?:#|\?[^"\']*)?)["\']',
re.IGNORECASE,
)
DOWNLOAD_FILE_RE = re.compile(
r"downloadFile\(\s*['\"]([^'\"]+\.zip)['\"]\s*\)",
re.IGNORECASE,
)
# Активный пункт фильтра «годовой / квартальный / ежемесячный / оперативный» (как в SSR HTML).
ACTIVE_KIND_RE = re.compile(
r'<a(?=[^>]*wire:click="setFilterType\(\d+\)")(?=[^>]*class="[^"]*dropdown-item active[^"]*")[^>]*>'
r"\s*([^<]+?)\s*</a>",
re.IGNORECASE,
)
# Прямые вложения без ZIP (другие подстраницы исполнения).
HREF_TABLE_RE = re.compile(
r'href=["\']([^"\']+\.(?:xls|xlsx|pdf)(?:\?[^"\']*)?)["\']',
re.IGNORECASE,
)


def normalize_storage_path(raw: str) -> str:
raw = raw.strip().strip("#").split("?", 1)[0]
return raw if raw.startswith("/") else "/" + raw.lstrip("/")


def storage_path_to_absolute_url(storage_path: str) -> str:
"""Кодирует каждый сегмент пути (кириллица и пробелы в имени ZIP на roskazna.gov.ru)."""
path = normalize_storage_path(storage_path)
segments = [s for s in path.split("/") if s]
encoded_path = "/" + "/".join(quote(seg, safe="") for seg in segments)
return f"{BASE_ORIGIN.rstrip('/')}{encoded_path}"


def discover_zip_storage_paths(html: str) -> list[str]:
paths: list[str] = []
seen: set[str] = set()
for m in HREF_ZIP_RE.finditer(html):
p = normalize_storage_path(m.group(1))
if p.lower().endswith(".zip") and p not in seen:
seen.add(p)
paths.append(p)
for m in DOWNLOAD_FILE_RE.finditer(html):
p = normalize_storage_path(m.group(1))
if p.lower().endswith(".zip") and p not in seen:
seen.add(p)
paths.append(p)
return paths


def detect_active_report_kind(html: str) -> str | None:
m = ACTIVE_KIND_RE.search(html)
return m.group(1).strip() if m else None


def pick_newest_zip_path(paths: list[str]) -> str | None:
"""Грубая эвристика: последний по строковому сравнению путь (даты в имени — ДД.ММ.ГГГГ)."""
return max(paths) if paths else None


def list_direct_table_links(page_url: str, html: str) -> list[str]:
out: list[str] = []
seen: set[str] = set()
for m in HREF_TABLE_RE.finditer(html):
url = urljoin(page_url, m.group(1))
if "roskazna.gov.ru" in url and url not in seen:
seen.add(url)
out.append(url)
return out


def print_zip_manifest(data: bytes, limit: int = 50) -> None:
with zipfile.ZipFile(BytesIO(data)) as zf:
infos = zf.infolist()
print(f" В архиве записей: {len(infos)}")
for info in infos[:limit]:
print(f" - {info.filename!r} ({info.file_size} байт)")
if len(infos) > limit:
print(f" … скрыто ещё {len(infos) - limit}")


def extract_zip_safe(data: bytes, dest: Path) -> None:
dest = dest.resolve()
dest.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(BytesIO(data)) as zf:
for info in zf.infolist():
if info.is_dir():
continue
target = (dest / info.filename).resolve()
try:
target.relative_to(dest)
except ValueError as exc:
raise RuntimeError(f"Небезопасное имя в архиве: {info.filename!r}") from exc
target.parent.mkdir(parents=True, exist_ok=True)
with zf.open(info, "r") as src, target.open("wb") as dst:
dst.write(src.read())


def main() -> None:
out_dir = Path("out_roskazna_federal_budget")

session = requests.Session()
resp = session.get(PAGE_URL, headers=HEADERS, timeout=120)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding or resp.encoding or "utf-8"
html = resp.text

kind = detect_active_report_kind(html)
zip_paths = discover_zip_storage_paths(html)
direct_files = list_direct_table_links(PAGE_URL, html)

print("HTTP", resp.status_code, "| кодировка ответа:", resp.encoding)
print("Активный тип отчёта в фильтре (по SSR):", kind or "не найден")
print("Найдено уникальных ZIP (пути /storage/...):", len(zip_paths))
for p in zip_paths:
print(" ", storage_path_to_absolute_url(p))

if not zip_paths:
print("ZIP в HTML не найдены — откройте страницу в браузере, переключите тип отчёта и повторите выгрузку HTML.")
print("Прямые XLS/XLSX/PDF на странице:", len(direct_files))
for u in direct_files[:20]:
print(" ", u)
return

chosen = pick_newest_zip_path(zip_paths)
assert chosen is not None
zip_url = storage_path_to_absolute_url(chosen)
print("Скачиваем для примера:", zip_url)

zresp = session.get(zip_url, headers=HEADERS, timeout=120)
zresp.raise_for_status()
print("ZIP HTTP", zresp.status_code, "bytes:", len(zresp.content))
print_zip_manifest(zresp.content)
extract_zip_safe(zresp.content, out_dir)
print("Вложения распакованы в:", out_dir.resolve())


if __name__ == "__main__":
main()

Проверка результата

  • Убедитесь, что ответ — HTML ожидаемого раздела, а не страница ограничения доступа: при подозрении проверьте User-Agent, статус HTTP и заголовок страницы в браузере.
  • Для ZIP проверьте Content-Type: application/zip, затем манифест (namelist / размеры) и откройте извлечённый XLS/XLSX в табличном редакторе: кодировка имён внутри архива может отличаться от UTF-8 в зависимости от того, как упакован файл на стороне портала.
  • Сверьте код формы, год и уровень бюджета с заголовком опубликованного файла и с методическими пояснениями на портале.
  • Сопоставьте итоговые суммы с ГИИС «Электронный бюджет» только при явном совпадении показателя и периода; расхождения чаще всего из‑за разных срезов (касса / план / консолидация) — см. FAQ в карточке источника.

Ограничения и типовые ошибки

  • Livewire и фильтры: переключение «ежемесячный / оперативный / квартальный / годовой» на лендинге федерального бюджета может подгружать карточки без полного набора в одном GET; для полного списка по каждому типу сохраняйте HTML после выбора фильтра в браузере или используйте инструменты браузерной автоматизации.
  • SSL в локальном Python: при CERTIFICATE_VERIFY_FAILED на macOS обновите корни доверия для установленного интерпретатора (см. документацию дистрибутива Python) или используйте среду с актуальным хранилищем CA; отключать проверку TLS в продакшене не рекомендуется.
  • Нет гарантированно стабильных deep-link на каждую форму: после обновления сайта пути в меню меняются — автоматизацию строите от текущего HTML или от паспортов opendata, версионируя дату скачивания.
  • Смешение разделов: финансовые операции (/finansovye-operacii/ и смежное) — это не те же формы, что исполнение бюджета по КБК в /ispolnenie-byudzhetov/ (см. карточку источника).
  • Не полагайтесь на «голый» скрипт без идентифицируемого User-Agent в продакшене: при HTML вместо файла или пустом списке ссылок проверьте ответ вручную и условия использования на портале.
  • Rate limit и нагрузка: дозируйте запросы; при массовой загрузке предпочтительны официальные наборы из opendata или datamarts, если покрывают ваш срез.

Связанные страницы