Это первая статья из цикла о построении CDC-пайплайна в домашней лаборатории.
Полный путь: Telegram → PostgreSQL → Debezium → Kafka → HDFS → DWH.
Но любой пайплайн начинается с данных — и эта статья про их получение.

Проблема

Хочу видеть свои расходы в нормальной аналитике, но банки не дают API для выгрузки операций. Можно руками вбивать каждую покупку в Excel — но это путь в никуда. Можно подключить агрегаторы типа Дзен-мани — но они требуют доступ к онлайн-банку, а я параноик.

Решение: скриншоты операций → распознавание → PostgreSQL → дальше CDC pipeline.

Кто я

SRE/DevOps инженер в банке. На работе поддерживаю CDC-репликацию — 7 баз данных, сотни миллионов записей, Debezium, Kafka, PostgreSQL.

Проблема: понимаю как работает, но не всегда понимаю почему. Решение: построить весь стек с нуля в домашней лабе. Заодно решу бытовую задачу — учёт личных расходов.

Зачем я это пишу

Честно: в первую очередь это мои заметки. На работе всё уже настроено до меня. Хочу понять, как оно устроено изнутри — поэтому строю с нуля дома.

Во вторую очередь — портфолио. Если буду менять работу, эти статьи можно приложить к резюме.

И в третью — мотивация не бросить. Если хотя бы десять человек прочитают и кому-то пригодится — значит не зря потратил выходные.

Что получилось

Telegram-бот, которому отправляешь скриншот — получаешь структурированные данные в PostgreSQL:

Схема: от скриншота до DWH. Жёлтая зона — эта статья.

Реальный результат в боте:

Отправил скриншот → получил 3 транзакции в БД за секунды

Почему не OCR

Первая идея была очевидной — Tesseract OCR. Бесплатно, локально, без API.

# Код был простой

from PIL import Image

import pytesseract

def extract_text_from_image(image_path):

    img = Image.open(image_path)

    text = pytesseract.image_to_string(img, lang='rus')

    return text

Проблемы на практике:

  • UI-элементы распознаются как текст («Что показывать», «Период», «Карта»)

  • Иконки превращаются в мусор (@, ©, №)

  • Структура теряется — непонятно где одна операция, где другая

  • Нужен отдельный парсер для каждого банка

Claude Vision смотрит на картинку и понимает контекст — различает UI от данных, группирует операции, определяет категории. Из 16 распознанных транзакций 10 были полностью корректны сразу (62.5%). Остальные легко исправляются через верификацию — и это создаёт UPDATE-события для CDC, что нам и нужно.

Стек

Компонент

Зачем

python-telegram-bot

Telegram бот с диалогами для верификации

anthropic

Официальный SDK Claude API

psycopg2-binary

PostgreSQL драйвер

Pillow

Определение формата изображения

python-dotenv

Секреты в .env файле

Архитектура кода

budget-parser/

├── src/

│   ├── telegram_bot.py      # Интерфейс: принимает фото, показывает результат

│   ├── claude_parser.py     # Ядро: отправляет в Claude, получает JSON

│   ├── db_writer.py         # PostgreSQL: INSERT/UPDATE/DELETE для CDC

│   └── date_parser.py       # Утилита: русские даты → ISO формат

├── .env                     # Секреты (в .gitignore)

└── requirements.txt

Разберём ключевые модули.


Модуль 1: Парсинг русских дат

Проблема

Банки показывают даты без года: «3 октября», «15 сентября, чт». Когда мы парсим скриншот в январе и видим «15 декабря» — это какой год?

Решение

MONTHS_RU = {

    "января": 1, "февраля": 2, "марта": 3,

    "апреля": 4, "мая": 5, "июня": 6,

    "июля": 7, "августа": 8, "сентября": 9,

    "октября": 10, "ноября": 11, "декабря": 12

}

def parse_russian_date(date_str, current_year=None):

    # "2 октября, чт" → "2 октября"

    clean_str = date_str.lower().strip()

    if ',' in clean_str:

        clean_str = clean_str.split(',')[0]

    # Извлекаем число и месяц

    match = re.search(r'(\d+)\s+(\w+)', clean_str)

    day = int(match.group(1))

    month = MONTHS_RU[match.group(2)]

    # Умное определение года

    now = datetime.now()

    year = now.year

    if month > now.month:

        year -= 1  # Декабрь в январе = прошлый год

    return f"{year}-{month:02d}-{day:02d}"

Результат

Сейчас: January 2026 (месяц 1)

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

3 октября            → 2025-10-03  # октябрь > январь → прошлый год

5 января             → 2026-01-05  # январь = январь → текущий год

2 октября, чт        → 2025-10-02  # день недели отбрасывается

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


Модуль 2: Claude Vision API

Подготовка изображения

Claude API требует base64 + правильный media_type:

def image_to_base64(image_path):

    with open(image_path, 'rb') as file:

        return base64.b64encode(file.read()).decode('utf-8')

def get_image_media_type(image_path):

    with Image.open(image_path) as img:

        format_map = {'png': 'image/png', 'jpeg': 'image/jpeg', 'webp': 'image/webp'}

        return format_map.get(img.format.lower(), 'image/jpeg')

Промпт — самое важное

Промпт прошёл 5+ итераций. Каждая строчка — результат конкретной ошибки:

prompt = f"""

Проанализируй этот скриншот банковских операций.

КОНТЕКСТ ВРЕМЕНИ:

Скриншот сделан: {file_date.strftime('%d.%m.%Y')} ({file_date.year} год)

КРИТИЧЕСКИ ВАЖНО ДЛЯ ДАТ:

1. Если на скриншоте ЕСТЬ дата (день и месяц) — используй год из контекста

2. Если даты НЕТ совсем — верни date: null, НЕ выдумывай!

ВАЖНЫЕ ПРАВИЛА:

1. ИГНОРИРУЙ уведомления "Нашли счёт от..." — это НЕ операции

2. ИГНОРИРУЙ итоги за день (сумма рядом с датой БЕЗ описания)

3. Если есть кнопка "Оплатить" рядом — это НЕ операция

4. Переводы с именами людей: + это доход, - это расход

5. Две операции с одинаковой суммой (+/-) с одним человеком — нормально

Верни результат СТРОГО в формате JSON:

{{

  "transactions": [

    {{

      "date": "YYYY-MM-DD",

      "merchant": "название контрагента",

      "amount": число (отрицательное для расходов),

      "type": "Расход" или "Доход",

      "category": "категория или null"

    }}

  ]

}}

КРИТИЧЕСКИ ВАЖНО:

- Только валидный JSON

- Никакого markdown (``json``)

- Никакого текста до или после

"""

Почему каждое правило появилось

Правило в промпте

Какая была ошибка

«ИГНОРИРУЙ уведомления»

Claude включал «Нашли счёт от Самарские ЖКХ» как операцию

«НЕ выдумывай дату»

Claude придумывал даты для операций без даты на скриншоте

«Никакого markdown»

Иногда Claude оборачивал JSON в ``json...``

«КОНТЕКСТ ВРЕМЕНИ»

Без даты файла Claude не знал, какой сейчас год

Защита от markdown

Даже с инструкцией Claude иногда добавляет markdown:

response_text = message.content[0].text

if response_text.strip().startswith('```'):

    response_text = response_text.replace('``json', '').replace('``', '').strip()

data = json.loads(response_text)

Модуль 3: PostgreSQL и CDC

Этот модуль особенно важен — он создаёт операции, которые Debezium захватит и отправит в Kafka.

Структура таблицы

CREATE TABLE finance.parsed_transactions (

    id SERIAL PRIMARY KEY,

    transaction_date DATE,

    merchant VARCHAR(255),

    amount NUMERIC(12,2),

    transaction_type VARCHAR(20),  -- 'income' / 'expense'

    category VARCHAR(100),

    is_verified BOOLEAN DEFAULT FALSE,

    verified_at TIMESTAMP,

    original_amount NUMERIC(12,2),    -- для истории изменений

    original_merchant VARCHAR(255),   -- для истории изменений

    source_image_hash VARCHAR(64),

    parsed_at TIMESTAMP DEFAULT NOW()

);

Дедупликация

Если парсить один скриншот дважды — не должно быть дублей:

def is_duplicate(self, transaction):

    cur.execute("""

        SELECT id FROM finance.parsed_transactions

        WHERE transaction_date = %s

          AND merchant = %s

          AND amount = %s

    """, (date, merchant, amount))

    return cur.fetchone() is not None

Дубликат = совпадение по трём полям: дата + контрагент + сумма.

CDC-операции

Каждый метод создаёт операцию для Debezium:

Метод

SQL

CDC событие

insert_transaction()

INSERT

op: "c" (create)

verify_transaction()

UPDATE

op: "u" (update)

delete_transaction()

DELETE

op: "d" (delete)

Особенно интересен UPDATE — он сохраняет историю изменений:

def verify_transaction(self, tx_id, new_amount=None, new_merchant=None):

    updates = ["is_verified = TRUE", "verified_at = NOW()"]

    if new_amount is not None and float(new_amount) != float(current_amount):

        updates.append("original_amount = amount")  # Сохраняем старое

        updates.append("amount = %s")

        params.append(new_amount)

# UPDATE выполняется → Debezium захватывает before/after

Debezium захватит before и after — в Kafka будет видно, что именно изменилось. Это важно для DWH: можно строить SCD Type 2 или просто логировать историю правок.


Модуль 4: Telegram-бот

Основные команды

Команда

Что делает

/start

Приветствие и список команд

/recent

Последние транзакции

/verify

Верификация (исправление ошибок AI)

/stats

Статистика

Обработка фото

При получении скриншота бот:

  1. Скачивает фото максимального разрешения

  2. Отправляет в Claude Vision

  3. Парсит JSON-ответ

  4. Проверяет на дубликаты

  5. Сохраняет в PostgreSQL

Верификация — UPDATE для CDC

Список непроверенных транзакций

Можно подтвердить, изменить сумму/контрагента, или удалить

Когда пользователь исправляет данные через бота:

/verify → выбор транзакции → "Изменить сумму" → ввод правильной суммы

    → db.verify_transaction(tx_id, new_amount=correct_amount)

        → UPDATE в PostgreSQL

            → Debezium захватывает изменение

                → В Kafka появляется событие с before/after

Ошибки нейросети — это не баг, а фича для тестирования CDC. Каждое исправление генерирует UPDATE, каждое удаление мусора — DELETE. В следующих статьях увидим, как эти события проходят через весь пайплайн.


Грабли и выводы

1. OCR не работает для банковских скриншотов

Слишком много UI-элементов, разные форматы у разных банков. Claude Vision понимает контекст — это решает проблему.

2. Промпт требует итераций

Первая версия работала на 60%. Каждая ошибка Claude → новое правило в промпте. После 5 итераций — приемлемая точность + верификация для остального.

3. Даты без года — нужна логика

Банки не показывают год. Решение: если месяц операции > текущего месяца → прошлый год.

4. Claude иногда игнорирует инструкции

«Никакого markdown» не гарантирует отсутствие markdown. Нужна защита в коде.

5. Дедупликация обязательна

Без неё один скриншот = дублирование данных при каждом запуске.


Стоимость

Claude Sonnet 4: $3 за 1M входных токенов, $15 за 1M выходных.

Один скриншот: ~1500 токенов на изображение, ~500 токенов промпт, ~300 токенов ответ.

Итого: ~$0.01 за скриншот. За месяц при 100 операциях — около $1.

⚠️ Для читателей из России: Claude API требует VPN или VPS за рубежом. В одной из следующих статей покажу настройку WireGuard-туннеля.


Что дальше: план цикла

#

Статья

О чём

1

Эта статья

Получение данных: Telegram + Claude Vision

2

PostgreSQL для CDC

Logical replication, слоты, настройка WAL

3

Debezium + Kafka

Захват изменений, топики, коннекторы

4

HDFS + Hive

Хранение и SQL-доступ к истории

5

Consumer + скрипты проверки данных

Что должно вычитывать, корректно класть в HDFS и проверть консистентность данных

6

Мониторинг

Grafana-дашборды для всего пайплайна

Подписывайтесь, чтобы не пропустить.


Вопросы и feedback — в комментариях. Особенно интересно, какие банки у вас и какие форматы скриншотов.

Комментарии (17)


  1. bak
    17.01.2026 11:22

    Есть способ намного проще - парсить SMS от банка.


    1. osj
      17.01.2026 11:22

      Есть еще проще, разрешить получать чеки в банковском приложении, там получат чеки из OFD, распарсят, разложат по категориям и в интерфейсе отобразят.


    1. Filvs
      17.01.2026 11:22

      Доброго. У меня не все операции, к примеру, выливаются в СМС. Часть идёт в пуши. При этом банков всего два. Вопрос в том - не проще ли попробовать через банковскую выписку


      1. bak
        17.01.2026 11:22

        Пуши вроде тоже можно паристь. Выписку? Единоразово проще наверное, если надо пайплайн то кажется что смс / пуши проще парсить, выписку еще же надо сгенерировать как-то, не руками же каждый раз выгружать.


  1. aborouhin
    17.01.2026 11:22

    Ну ОК, API банки не дают (Open Banking в какой-то непонятной стадии внедрения и для пет-проекта точно не вариант). Но выписки-то выгружает любой банк, и уж точно проще их обрабатывать, чем скриншоты.


    1. PaulNoks Автор
      17.01.2026 11:22

      Согласен, для чистого учёта расходов — проще. Но цель цикла статей — построить CDC-пайплайн с нуля и разобраться как он работает. Скриншоты дают разнообразные события: INSERT при парсинге, UPDATE при верификации, DELETE при удалении мусора. Выписка дала бы только INSERT'ы


  1. proDan0
    17.01.2026 11:22

    Там проблема в том что многие банки скриншоты блочат на уровне системы, так что решение не универсальное. У себя (андроид) через АДБ стримлю экран и делаю скриншоты, которые потом загоняю в нейросеть. Стоило бы автоматизировать, но мотивации пока не придумал ;)


    1. osj
      17.01.2026 11:22

      Ведь есть еще личный кабинет на сайте банка...


  1. Tizar1121
    17.01.2026 11:22

    Как сделано у меня: Пуш-уведомления об операциях от банка -> парсинг в Macrodroid -> расширение для гугл таблиц Apps Script -> занесение в файл в гугл таблицы.


    1. PaulNoks Автор
      17.01.2026 11:22

      Рабочее решение. У меня была похожая идея, потом реализовал с сохранением в excel, но тут хотелось именно PostgreSQL → Debezium → Kafka — чтобы разобраться в стеке, который использую на работе


  1. TolK84
    17.01.2026 11:22

    Незнаю как сейчас в РФ, но в Казахстане скрины банк аппов не сделать без особых выкрутасов.


    1. riky
      17.01.2026 11:22

      Тоже помню раньше такое было, сейчас попробовал у тбанка скриншоты запрещены только на странице где реквизиты карты, причем только после нажатия "показать реквизиты". Остальное все скриншотится. Я думаю юзеры часто скидывают скрин после оплаты.


      1. TolK84
        17.01.2026 11:22

        Да раньше также выборочно было, теперь любая страница черный квадрат малевича. И отключить запрет нельзя.


  1. Usurer
    17.01.2026 11:22

    Мне кажется, что без разбивки на категории такой учёт не очень полезен, в смысле, что в той или иной форме он уже есть в банковском приложении. А к какой категории относится чек на 123 рубля от ООО Ромашка знает только пользователь. Ну или покупки на маркетплейсе, где всегда одна и та же организация в чеке. В итоге всё равно надо вспоминать и руками прописывать категорию ко многим транзакциям. Я понимаю, что задача решается во многом учебная, но тем не менее.


    1. babysas
      17.01.2026 11:22

      Категории, вообще самая непростая штука. Если пытаться из этого пользу извлекать.

      т. к. многие покупки идут "одним чеком" и бнз разбивки теряют много ценности.

      я когда-то начал вести учёт, чтобы понимать диапазон расходов с расбивкой на категории т.к. именно по отдельным категориям можно легко ужиматься

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

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


  1. krabdb
    17.01.2026 11:22

    Понятно, что это pet-проект, но... почти все банки, которые стоит использовать, дают делать экспорт транз в csv прямо в аппе. К чему эти скачки со скриншотами...


  1. shurutov
    17.01.2026 11:22

    Telegram → PostgreSQL → Debezium → Kafka → HDFS → DWH

    Это именно у вас. А в других организациях более по-другому устроено. А ещё реализация DWH у каждого своя. Мне, например, интересны общие принципы построения, соответственно, для своего проекта я беру то, чего на работе нет. Брать работу на дом - <тут должна быть картинка из "Операция Ы" про "поллитра? Вдребезги? Да я тебя!">

    Хочу видеть свои расходы в нормальной аналитике

    В банковском сообщении - только сумма и продаван. Т.е. только и исключительно деньги, а это синтетический учёт, если мне память по части терминологии не изменяет. По крайней мере, на тот момент, когда нас пичкали знаниями по бухгалтерии...
    Лично для меня нормальная аналитика - это, помимо денег, товары, группы товаров и их (товаров) количество, т.е. к синтетическому учёту добавляется аналитический. Из банковских документов аналитику не вытащить.