TL;DR: BOJA опубликовал финансовые декларации 1129 кандидатов в трудночитаемом PDF. Я написал Python-скрипт для извлечения и структурирования данных и веб-приложение для их просмотра. Смотреть → · Код →
27 апреля 2026 года BOJA опубликовал выпуск 79 C1: 402 страницы деклараций об активах, доходах и интересах 1129 кандидатов на выборах в региональный парламент 17 мая. Публичные данные, формально доступные любому гражданину. На практике — PDF.
Причём не просто PDF. Документ, собранный из отсканированных форм и переформатированный, с колонками, которые pdftotext извлекает в неправильном порядке, с колонтитулами BOJA, вклиненными между данными о кандидатах, и целыми разделами, смещёнными на несколько страниц от того места, где они должны быть.
Проблема с pdftotext
pdftotext конвертирует PDF в текст, следуя визуальному порядку колонок на странице. Для таблиц BOJA это катастрофа: одни разделы выходят построчно (все поля строки 1, затем строки 2…), другие — по столбцам (все значения колонки 1 для всех строк, затем все значения колонки 2). Определить, какой порядок, без изучения исходного layout PDF невозможно.
Решение — детектировать паттерн: если текст раздела начинается с заголовка колонки, за которым следуют все её значения до появления следующего заголовка — это column-major. Если значения разных колонок чередуются — row-major. Парсер применяет разные стратегии в каждом случае.
Шум BOJA
Между каждой страницей PDF вставляет фрагменты колонтитула BOJA: номер депозита, URL сайта правительства, номер страницы, слово «BOJA» отдельной строкой. Всё это перемешано с данными кандидатов. Нужно убрать эти фрагменты, не задев похожие реальные данные.
def clean_headers(text):
text = re.sub(r"Depósito Legal:[^\n]+\n\s*\n?\s*https?://[^\n]+\n?", "\n", text)
text = re.sub(r"(?:BOJA\s*\n\s*)?Boletín Oficial de la Junta de Andalucía\s*\n[^\n]+\n\s*página \d+/\d+", "", text)
text = re.sub(r"^\s*BOJA\s*$", "", text, flags=re.MULTILINE)
# ...
return text
Смещённые строки и разделы не на месте
Самая частая проблема: pdftotext помещает содержимое одного раздела в текст другого. Раздел 2.3 (акции и ценные бумаги) может содержать строки, которые на самом деле относятся к 2.4 (транспорт) или 2.6 (долги) — потому что в исходном PDF они располагались рядом и при извлечении смешались.
Эвристика для обнаружения: если в тексте раздела 2.3 встречается заголовок «Descripción» (принадлежащий разделу 2.4), там есть смещённый контент. Его нужно перепарсить с правильным форматом колонок и рекласифицировать каждую строку по ключевым словам: «hipoteca» → долг, «seguro de vida» → страховка, остальное → транспорт.
_seg_kw = re.compile(r"(?i)(segur|póliza)")
_deu_kw = re.compile(r"(?i)(hipoteca|préstamo|crédito|financiaci)")
if re.search(r"(?m)^Descripción\s*$", sec23_text):
displaced_rows = parse_table("2.4", sec23_text)
for row in displaced_rows:
desc = str(row.get("Descripción") or "")
if _seg_kw.search(desc):
seguros.append(row)
elif _deu_kw.search(desc):
deudas.append(row)
else:
vehiculos.append(row)
Ручные исправления
Для некоторых кандидатов layout настолько нерегулярный, что никакая эвристика не справляется. У исполняющего обязанности президента Хунты Хуана Мануэля Морено Бонильи есть два сельских участка в Картаме, кадастровая стоимость которых слита с описанием местонахождения в единый текстовый блок. Парсер это замечает, но разделить правильно не может. Решение — ручной override для этого кандидата.
Результат
Файл candidates.json с 1129 структурированными объектами: рассчитанные финансовые показатели (чистый капитал, суммарные активы, суммарные долги) и полные таблицы с деталями.
Поверх этого JSON — одностраничное веб-приложение без серверных зависимостей и шага сборки: таблица с поиском и фильтрами, сравнение по партиям, флаги предупреждений для примечательных финансовых паттернов и диаграмма рассеяния всех кандидатов, раскрашенных по партиям.
→ Просмотреть декларации · → Репозиторий
Код MIT. Данные публичны (BOJA).