> tomaspica.com
================================================================================
================================================================================
[POST] Когда BOJA не парсится
2026-04-30
--------------------------------------------------------------------------------

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).

================================================================================