feat: bulk-дампы открытых данных ФНС по юрлицам + lookup по ИНН #3429

Merged
lekss361 merged 2 commits from feat/fns-legal-entities into main 2026-09-08 22:29:11 +00:00
Owner

Что и зачем

Легальный обход блокера ЕГРН: сведений о правообладателях-физлицах в открытом доступе не существует (218-ФЗ ст. 62, выписка 1 640 ₽ юрлицу), а по юрлицам данные открыты и типовые условия ФНС разрешают переработку и перераспространение со ссылкой на источник.

Наборы 7707329152-{revexp,sshr2019,debtam,snr}.

URL дампа не хардкодится — дата в имени файла меняется при каждой публикации (data-20260825-structure-20180110.zip), ссылка резолвится со страницы каталога; загруженная версия трекается в fns_dataset_versions, чтобы не перекачивать одно и то же. Обход XML внутри ZIP потоковый (lxml.etree.iterparse с очисткой), архив не распаковывается ни в память, ни на диск.

Два дефекта, найденные прогоном на реальном дампе

До этого парсер извлекал ноль записей при 17 зелёных тестах — фикстуры были написаны по выдуманной форме XML. Прогон на настоящем revexp (data-20260825, 104 МБ, 2118 XML) показал:

1. ИНН и показатели лежат на РАЗНЫХ соседних элементах внутри <Документ>:

<Документ ИдДок="..." ДатаДок="25.08.2026" ДатаСост="31.12.2025">
  <СведНП НаимОрг="ООО ..." ИННЮЛ="4205406898"/>
  <СведДохРасх СумДоход="341864000.00" СумРасход="282224000.00"/>
</Документ>

Харвестер требовал ИНН и число на одном элементе — совпадений не было ни одного. Теперь носители ИНН ищутся по поддереву, а значения без собственного ИНН привязываются к организации только когда носитель в документе один (иначе непонятно, чьи это цифры, и мы их не выдумываем).

2. Безусловный elem.clear() на каждом элементе вычищал <СведНП>/<СведДохРасх> до закрытия <Документ>end-события детей приходят раньше родительского. Чистка теперь только на границе записи.

Попутно: отчётный период берётся из ДатаСост документа, а не из подставленного значения; служебные атрибуты (ИдДок, ДатаДок, ВерсФорм, КолДок) фактами не считаются.

Проверка

Прогон на реальном дампе: 40 002 факта на первых 20 001 организаций, серии СумДоход/СумРасход, период 31.12.2025, нечисловых ИНН — ноль. Резолвер URL проверен на живой странице каталога.

ruff check — чисто; полный сьют pytest5 642 passed, 37 skipped (два новых регресса: боевая форма документа ФНС и «служебные id не становятся фактами»).

Скоуп

Только загрузка и lookup по ИНН. Потребителя нет и он не изобретается: estimator.py не тронут, ручек API не добавлено. Подключение к скорингу застройщика/УК — отдельная задача.

ЕГРЮЛ в этих наборах нет — ни адреса, ни ОКВЭД, ни учредителей, только ИНН + наименование + показатели набора.

Миграции 296 (таблицы) и 297 (сид расписания, enabled=false, interval_days 30 — включение отдельным осознанным шагом после ручного dry-run, дампы тяжёлые).

## Что и зачем Легальный обход блокера ЕГРН: сведений о правообладателях-физлицах в открытом доступе не существует (218-ФЗ ст. 62, выписка 1 640 ₽ юрлицу), а по **юрлицам** данные открыты и типовые условия ФНС разрешают переработку и перераспространение со ссылкой на источник. Наборы `7707329152-{revexp,sshr2019,debtam,snr}`. **URL дампа не хардкодится** — дата в имени файла меняется при каждой публикации (`data-20260825-structure-20180110.zip`), ссылка резолвится со страницы каталога; загруженная версия трекается в `fns_dataset_versions`, чтобы не перекачивать одно и то же. Обход XML внутри ZIP потоковый (`lxml.etree.iterparse` с очисткой), архив не распаковывается ни в память, ни на диск. ## Два дефекта, найденные прогоном на реальном дампе До этого парсер извлекал **ноль записей** при 17 зелёных тестах — фикстуры были написаны по выдуманной форме XML. Прогон на настоящем `revexp` (`data-20260825`, 104 МБ, 2118 XML) показал: **1. ИНН и показатели лежат на РАЗНЫХ соседних элементах** внутри `<Документ>`: ```xml <Документ ИдДок="..." ДатаДок="25.08.2026" ДатаСост="31.12.2025"> <СведНП НаимОрг="ООО ..." ИННЮЛ="4205406898"/> <СведДохРасх СумДоход="341864000.00" СумРасход="282224000.00"/> </Документ> ``` Харвестер требовал ИНН и число на одном элементе — совпадений не было ни одного. Теперь носители ИНН ищутся по поддереву, а значения без собственного ИНН привязываются к организации только когда носитель в документе один (иначе непонятно, чьи это цифры, и мы их не выдумываем). **2. Безусловный `elem.clear()` на каждом элементе** вычищал `<СведНП>`/`<СведДохРасх>` **до** закрытия `<Документ>` — `end`-события детей приходят раньше родительского. Чистка теперь только на границе записи. Попутно: отчётный период берётся из `ДатаСост` документа, а не из подставленного значения; служебные атрибуты (`ИдДок`, `ДатаДок`, `ВерсФорм`, `КолДок`) фактами не считаются. ## Проверка Прогон на реальном дампе: **40 002 факта на первых 20 001 организаций**, серии `СумДоход`/`СумРасход`, период 31.12.2025, нечисловых ИНН — ноль. Резолвер URL проверен на живой странице каталога. `ruff check` — чисто; полный сьют `pytest` — **5 642 passed, 37 skipped** (два новых регресса: боевая форма документа ФНС и «служебные id не становятся фактами»). ## Скоуп Только **загрузка и lookup по ИНН**. Потребителя нет и он не изобретается: `estimator.py` не тронут, ручек API не добавлено. Подключение к скорингу застройщика/УК — отдельная задача. **ЕГРЮЛ в этих наборах нет** — ни адреса, ни ОКВЭД, ни учредителей, только ИНН + наименование + показатели набора. Миграции **296** (таблицы) и **297** (сид расписания, `enabled=false`, `interval_days` 30 — включение отдельным осознанным шагом после ручного dry-run, дампы тяжёлые).
lekss361 added 1 commit 2026-09-08 21:40:46 +00:00
feat: bulk-дампы открытых данных ФНС по юрлицам + lookup по ИНН
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m19s
1b1859b7d3
Легальный обход блокера ЕГРН: сведений о правообладателях-физлицах в открытом
доступе не существует (218-ФЗ ст. 62), а по ЮЛ данные открыты и типовые условия
ФНС разрешают переработку и перераспространение со ссылкой на источник.

Наборы 7707329152-{revexp,sshr2019,debtam,snr}. URL дампа НЕ хардкодится —
дата в имени файла меняется при каждой публикации, ссылка резолвится со
страницы каталога; загруженная версия трекается в fns_dataset_versions, чтобы
не перекачивать одно и то же. Обход XML внутри ZIP потоковый (lxml iterparse
с очисткой), архив не распаковывается ни в память, ни на диск.

ДВА ДЕФЕКТА, найденные прогоном на реальном дампе (revexp, data-20260825,
104 МБ, 2118 XML) — до этого парсер извлекал НОЛЬ записей при 17 зелёных
тестах, потому что фикстуры были написаны по выдуманной форме:

1. ИНН и показатели лежат на РАЗНЫХ соседних элементах внутри <Документ>:
   ИНН на <СведНП ИННЮЛ="...">, суммы на <СведДохРасх СумДоход="...">.
   Харвестер требовал их на одном элементе — совпадений не было ни одного.
   Теперь носители ИНН ищутся по поддереву, а значения без собственного ИНН
   привязываются к организации только когда носитель в документе один.
2. Безусловный elem.clear() на каждом элементе вычищал детей ДО закрытия
   <Документ> (end-события детей приходят раньше родительского). Чистка
   теперь только на границе записи.

Отчётный период берётся из ДатаСост документа, а не из подставленного
значения. Служебные атрибуты (ИдДок, ДатаДок, ВерсФорм, КолДок) фактами
не считаются.

Проверено: 40 002 факта на первых 20 001 организаций, серии СумДоход/СумРасход,
период 31.12.2025, нечисловых ИНН — ноль.

СКОУП: только загрузка и lookup по ИНН. Потребителя нет и он не изобретается —
estimator.py не тронут, ручек API не добавлено. Подключение к скорингу
застройщика/УК — отдельная задача.

Миграции 296 (таблицы) и 297 (сид расписания, enabled=false, interval_days 30).
bot-backend added 1 commit 2026-09-08 22:20:20 +00:00
merge main в feat/fns-legal-entities
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m9s
60c355b9a5
# Conflicts:
#	tradein-mvp/backend/app/services/product_handlers.py
lekss361 merged commit e0bef636e6 into main 2026-09-08 22:29:11 +00:00
lekss361 deleted branch feat/fns-legal-entities 2026-09-08 22:29:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3429
No description provided.