OCR-парсер: индексы столбцов по заголовку таблицы (#6) #7

Closed
aep wants to merge 2 commits from feat/6-ocr-header-columns into main
Owner

Closes #6

Что сделано

api/src/core/PDFWorker/utils.py: индексы столбцов больше не захардкожены (0 — наименование, 1 — код, 2+ — значения).

Заголовок найден

  • Заголовок — строка с ячейкой «Код» (сравнение после нормализации: регистр, переносы, пунктуация). Из неё строится ColumnLayout:
    • code_idx — ячейка «Код»;
    • label_idx — ячейка с «наименование», иначе ближайшая непустая слева от кода;
    • value_idxs — столбцы правее кода, в заголовке которых есть год или начало «на»/«за». Столбцы с пустым/мусорным заголовком (полоса скана) игнорируются.
  • Повторный заголовок перезаписывает индексы; в БД пишется только первый заголовок таблицы.

Заголовок не первый (встречается в дальнейших строках)

  • Строки до первого заголовка разбираются по раскладке, выведенной из самих этих строк — индексы заголовка могут с ними не совпадать.
  • Заголовок пишется в БД первой строкой таблицы: ordinal = min(row_index разобранных строк) - 1 (сервис сортирует по ordinal, фронтенд вставляет секцию «АКТИВ» после строки с кодом «Код»).

Заголовка нет

  • Раскладка выводится по всем строкам таблицы (пустые ячейки в отдельных строках, например в начале, на результат не влияют):
    • код — столбец с наибольшим числом известных кодов строк (codes_mapping);
    • наименование — столбец левее кода с наибольшим числом текстовых ячеек;
    • значения — столбцы правее кода, где суммы не уступают по числу мусору, либо полностью пустые (прочерк считается пустым).
  • Создаётся заголовок: код «Код», наименование «Наименование показателя», тексты периодов пустые с is_recognized=False.
  • Если столбец кода определить не удалось — таблица пропускается с logger.warning, остальной файл обрабатывается.

Общее

  • column_index значений в БД нормализован: 2, 3, 4… по порядку столбцов (раньше писались сырые индексы OCR, например 2, 4, 5). Контракт 0/1/>1 для fin_stat_service и фронтенда сохранён.
  • Нет ячейки под столбцом — пишется пустое значение с is_recognized=False, столбцы не съезжают.
  • _classify_row и _resolve_table_type работают по раскладке.

Проверка

Автотестов нет (по решению человека, см. R-1). Проверено одноразовым скриптом (вне репозитория) на данных из реального ответа OCR — 23/23:

  • базовый документ: результат совпадает со старым парсером, кроме нормализации column_index;
  • лишняя колонка-полоса; повторный заголовок со сдвинутыми индексами; пропущенная ячейка;
  • нет заголовка: данные как с заголовком, созданный заголовок первым, периоды пустые;
  • нет заголовка + полоса с мусором + пустые первые строки + полностью пустой столбец периода: полоса отброшена, пустой период сохранён;
  • заголовок только в середине, строки до него со своими индексами: разобраны по выведенной раскладке, заголовок записан первым;
  • титульные строки без кодов до заголовка игнорируются; таблица без кодов пропускается с warning; файл целиком без кодов — пустой результат.

Уже сохранённые в БД значения остаются с сырыми column_index — миграции нет; UI сортирует по column_index, поэтому отображение не ломается.

Rulings I made

  • R-1: автотесты не добавлялись (указание человека: «тесты можешь не делать»); проверка — одноразовым скриптом.
  • R-2: таблица, в которой нет ни заголовка, ни определимого столбца кода, пропускается с logger.warning, остальной файл обрабатывается. Если пропущены все таблицы — db_meta пуст и PDFTempPoller помечает job ошибкой, как и раньше.
  • R-3: удалена проверка len(cells) < 3 с ValueError на весь файл — она охраняла жёсткие позиции; теперь строка без ячейки кода просто пропускается.
  • R-4: столбец значений без заголовка — «суммы ≥ мусора» или полностью пустой. Человек подтвердил, что целиком пустой столбец периода возможен, поэтому пустые столбцы сохраняются. Цена ошибки: полностью пустая полоса скана даст лишний пустой столбец значений (данные не теряются).
  • R-5: столбец кода при выводе раскладки определяется по совпадениям с codes_mapping, а не по любому 4-значному числу — чтобы годы в титульных строках не принимались за коды.
  • R-6: у созданного заголовка код и наименование помечены распознанными (константы), тексты периодов — нераспознанными, чтобы в UI было видно, что их нужно заполнить.

Наблюдения

  • O-1: .bounded отсутствует в .gitignore репозитория — исключён локально через .git/info/exclude (вместе с .codegraph/).

Deferred

  • O-2 (deferred): проверка label == "Баланс" в _classify_row не срабатывает — OCR отдаёт БАЛАНС / БАЛАНС , row_kind BALANCE не присваивается никогда.
  • O-3 (deferred): _TABLE_TYPE_MAP не используется, хотя тип таблицы доступен напрямую из table.label.form_name.

🤖 Generated with Claude Code

Closes #6 ## Что сделано `api/src/core/PDFWorker/utils.py`: индексы столбцов больше не захардкожены (0 — наименование, 1 — код, 2+ — значения). ### Заголовок найден - **Заголовок** — строка с ячейкой «Код» (сравнение после нормализации: регистр, переносы, пунктуация). Из неё строится `ColumnLayout`: - `code_idx` — ячейка «Код»; - `label_idx` — ячейка с «наименование», иначе ближайшая непустая слева от кода; - `value_idxs` — столбцы правее кода, в заголовке которых есть год или начало «на»/«за». Столбцы с пустым/мусорным заголовком (полоса скана) игнорируются. - **Повторный заголовок** перезаписывает индексы; в БД пишется только первый заголовок таблицы. ### Заголовок не первый (встречается в дальнейших строках) - Строки до первого заголовка разбираются по раскладке, **выведенной из самих этих строк** — индексы заголовка могут с ними не совпадать. - Заголовок пишется в БД первой строкой таблицы: `ordinal = min(row_index разобранных строк) - 1` (сервис сортирует по `ordinal`, фронтенд вставляет секцию «АКТИВ» после строки с кодом «Код»). ### Заголовка нет - Раскладка выводится по всем строкам таблицы (пустые ячейки в отдельных строках, например в начале, на результат не влияют): - код — столбец с наибольшим числом известных кодов строк (`codes_mapping`); - наименование — столбец левее кода с наибольшим числом текстовых ячеек; - значения — столбцы правее кода, где суммы не уступают по числу мусору, либо полностью пустые (прочерк считается пустым). - Создаётся заголовок: код «Код», наименование «Наименование показателя», тексты периодов пустые с `is_recognized=False`. - Если столбец кода определить не удалось — таблица пропускается с `logger.warning`, остальной файл обрабатывается. ### Общее - **column_index значений в БД нормализован**: 2, 3, 4… по порядку столбцов (раньше писались сырые индексы OCR, например 2, 4, 5). Контракт 0/1/>1 для `fin_stat_service` и фронтенда сохранён. - **Нет ячейки под столбцом** — пишется пустое значение с `is_recognized=False`, столбцы не съезжают. - `_classify_row` и `_resolve_table_type` работают по раскладке. ## Проверка Автотестов нет (по решению человека, см. R-1). Проверено одноразовым скриптом (вне репозитория) на данных из реального ответа OCR — 23/23: - базовый документ: результат совпадает со старым парсером, кроме нормализации column_index; - лишняя колонка-полоса; повторный заголовок со сдвинутыми индексами; пропущенная ячейка; - нет заголовка: данные как с заголовком, созданный заголовок первым, периоды пустые; - нет заголовка + полоса с мусором + пустые первые строки + полностью пустой столбец периода: полоса отброшена, пустой период сохранён; - заголовок только в середине, строки до него со своими индексами: разобраны по выведенной раскладке, заголовок записан первым; - титульные строки без кодов до заголовка игнорируются; таблица без кодов пропускается с warning; файл целиком без кодов — пустой результат. Уже сохранённые в БД значения остаются с сырыми column_index — миграции нет; UI сортирует по column_index, поэтому отображение не ломается. ## Rulings I made - R-1: автотесты не добавлялись (указание человека: «тесты можешь не делать»); проверка — одноразовым скриптом. - R-2: таблица, в которой нет ни заголовка, ни определимого столбца кода, пропускается с `logger.warning`, остальной файл обрабатывается. Если пропущены все таблицы — `db_meta` пуст и `PDFTempPoller` помечает job ошибкой, как и раньше. - R-3: удалена проверка `len(cells) < 3` с `ValueError` на весь файл — она охраняла жёсткие позиции; теперь строка без ячейки кода просто пропускается. - R-4: столбец значений без заголовка — «суммы ≥ мусора» или полностью пустой. Человек подтвердил, что целиком пустой столбец периода возможен, поэтому пустые столбцы сохраняются. Цена ошибки: полностью пустая полоса скана даст лишний пустой столбец значений (данные не теряются). - R-5: столбец кода при выводе раскладки определяется по совпадениям с `codes_mapping`, а не по любому 4-значному числу — чтобы годы в титульных строках не принимались за коды. - R-6: у созданного заголовка код и наименование помечены распознанными (константы), тексты периодов — нераспознанными, чтобы в UI было видно, что их нужно заполнить. ## Наблюдения - O-1: `.bounded` отсутствует в `.gitignore` репозитория — исключён локально через `.git/info/exclude` (вместе с `.codegraph/`). ## Deferred - O-2 (deferred): проверка `label == "Баланс"` в `_classify_row` не срабатывает — OCR отдаёт `БАЛАНС` / `БАЛАНС `, row_kind `BALANCE` не присваивается никогда. - O-3 (deferred): `_TABLE_TYPE_MAP` не используется, хотя тип таблицы доступен напрямую из `table.label.form_name`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 1 commit 2026-09-29 17:39:35 +03:00
Column indices for label, code and values are taken from the header row
(cell «Код») instead of fixed positions 0/1/2+. Every repeated header
overwrites the layout. Value column_index is normalized to 2..n, missing
cells become empty unrecognized values, tables without a header are skipped
with a warning.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aep added 1 commit 2026-09-29 17:49:53 +03:00
Rows before the first header are parsed by a layout inferred from those rows
(the header's indices may not match them); the header is written first.
Without any header the layout is inferred from all rows and a header with
code and name and empty period texts is created. Value columns: amounts
outnumber junk or the column is fully empty.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aep closed this pull request 2026-10-13 20:36:51 +03:00

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DKB_ARMKA#7
No description provided.