Как я парсил базу данных ГАР на Go

Как я парсил базу данных ГАР на Go


Go

В начале октября этого года начальство приказало реализовать сервис хранения и получения адресов из базы данных ГАР/ФИАС. Для чего это может быть нужно и

что это вообще такое?

ГАР – это Государственный Адресный Реестр. Его поддерживает и регулярно обновляет Федеральная Налоговая Служба РФ. По сути, это огромный справочник объектов налогообложения, то есть, всё, с чего ФНС собирает налоги – участки, дома, квартиры и так далее. Налогом облагается вся недвижимость и земля – соответственно, для нас, айтишников, эта база данных – самый полный и доступный адресный справочник из всех существующих в открытом доступе.

Как это можно использовать?

В моем случае, первостепенная потребность в такой штуке – подсказка-автозаполнение при вводе адреса прописки в одном из разрабатываемых нами сервисов в ИТМО. Вы начинаете вводить город, улицу, а сервис автоматически предлагает вам уже полностью введенные адреса, вплоть до квартиры. По-любому где-то встречали подобное) С большой долей вероятности, работает эта фича именно на базе ГАР.

Помимо этого, такой сервис нужен для унификации адресов во всех системах университета. По факту, все адреса, хранимые в университете – набор строк, который меняется в жизни, но не меняется в самой БД. Огромная часть людей из ИСУ (инф. система универа) живут на улицах, которых уже может не существовать. Эти же улицы фигурируют во всяких документах, которые приходится исправлять в ручном режиме. С таким сервисом все эти изменения можно легко автоматизировать.

Виновник торжества и источник сие ценных данных – https://fias.nalog.ru/Frontend. Каждые 3-4 дня ФНС выкладывает два архива – полную версию архива и архив изменений.

Первое – 48гб сжатых текстовых данных о всех адресах, разложенных на XML-файлы, сгруппированных по папкам по каждому региону. То есть, папка “53” принадлежит только Новгородской области, а “78” – городу Санкт-Петербург. В распакованном виде данные занимают более 360ГБ (если верить моему архиватору).

Второе – архив изменений. Обычно, он весит немного – в сжатом виде занимает пару десятков МБ, в распакованном – в районе двухсот (на примере актуальных архивов от 7 октября 2025). Он выглядит точно так же, как и полный, только самих данных в нем гораздо меньше. Имеет ту же структуру.

Структура

Как я и упоминал, все данные расфасованы по папкам региона (от 1 до 99). Внутри находятся следующие приколы:

gar_1

Файлы AS_ADM_HIERARCHY* и AS_MUN_HIERARCHY* хранят идентификаторы адресного объекта и его родителя. Благодаря идентификаторам мы можем восстановить соотношения между объектами в разных типах территориального деления.

Например, в HIERARCHY хранятся пары записей c ОBJECTID и PARENTОBJID, объект и его родитель.

gar_2

Адресные данные хранятся в файлах _AS_ADDR_OBJ*, AS_HOUSES*. В файлах AS_ADDR_OBJ лежат компоненты адреса от региона до улицы. LEVEL указывает, к какому уровню относится часть адреса. Сельское или городское поселение — 4, населенный пункт — 6, улица — 8

Также, помимо файлов в папках, в корне архива лежат файлы со справочными значениями, на которые ссылаются остальные данные. Узнал я о них, конечно, не сразу…)

Думаю, что вводное погружение в структуру можно закончить, часть инфы я любезно позаимствовал спиздил из статьи на Хабре. В своем повествовании хотелось бы рассказать, как я на практике одолел эту дуру и смог за ночь вгрузить в постгрес всё (кроме истории изменений), что лежало в полном архиве от 07.10.25.

Суть затеи

Целью досуга стала таблица, которая по крупицам собирает частички адреса в единую строку, предварительно прописав ей пару пендалей в виде индексов и tsvector (чтобы не втыкала и искала адрес быстро). Назвал я её full_address. В итоге она выглядит так: gar_3

Приятного в ней мало, но по сути, это то, что планируется отдавать наружу. Тут и строка адреса целиком, и его отдельные элементы (субъект, город, улица, дом, квартира).

На каждую сущность в архиве я выделил по отдельной таблице – точь-в-точь, как данные представлены в XML-файлах. По-началу планировалось сделать stage-схему с unlogged-таблицами, загрузить в них все данные, и потом скопировать в основную схему со всеми ключами, индексами и т.д, что требовало в два раза больше и без того огромного свободного места в кластере. Когда я запустил переливку на ночь впервые, на утро я обнаружил, что выжрал всё свободное место, доступное в dev-кластере постгреса. Мда)

Как я это переливал?

Первым делом архив надо скачать. Как оказалось (удивительно, блять), загрузить файл весом 48ГБ в поду кубернетеса оказалось… не так уж просто)

По плану крон-джоба раз в два дня ходила бы на сайт ФНС и дергала волшебную ручку – https://fias.nalog.ru/WebServices/Public/GetLastDownloadFileInfo

Она возвращает следующую информацию:

{
  "VersionId": 20251205,
  "TextVersion": "БД ФИАС от 05.12.2025",
  "FiasCompleteDbfUrl": "",
  "FiasCompleteXmlUrl": "",
  "FiasDeltaDbfUrl": "",
  "FiasDeltaXmlUrl": "",
  "Kladr4ArjUrl": "https://fias-file.nalog.ru/downloads/2025.12.05/base.arj",
  "Kladr47ZUrl": "https://fias-file.nalog.ru/downloads/2025.12.05/base.7z",
  "GarXMLFullURL": "https://fias-file.nalog.ru/downloads/2025.12.05/gar_xml.zip",
  "GarXMLDeltaURL": "https://fias-file.nalog.ru/downloads/2025.12.05/gar_delta_xml.zip",
  "ExpDate": "2025-12-05T00:00:00",
  "Date": "05.12.2025"
}

Отсюда мне нужно несколько полей – VersionId, GarXMLFullURL, GarXMLDeltaURL. По ИД версии можно отслеживать, что уже загружено, что в процессе, а что ещё нет. Соответственно, если сервис понимает, что в базе ничего нет – нужно скачивать полный архив, а если есть – то только обновления (т.е. Delta).

Первая итерация – всё подряд

Полный файл у меня получилось скачать удаленно (т.е. сразу на dev) 0 раз. Оно и не удивительно – я просто выжирал всю доступную оперативку. Поэтому, на первое время от идеи загрузки полного архива автоматизированно пришлось отказаться :(

Пришлось качать зип напрямую к себе на компик – и как назло, интернет приказал не газовать и скорость загрузки была что-то около 5-6 Мб/с. Я даже не мог приступить к какой-либо разработке)

Где-то спустя два-три дня архив я таки заполучил. Данные переливал локально.

Запакованный файл открывался и лихим забегом через семафор (10 горутин) читалось всё, что попадется. Никакого порядка: ни по регионам, ни по сущностям – всё подряд. Некоторые таблицы набирали сотни миллионов строк, и что-то в них заселектить стало нереальным. До копирования из stage-схемы в основную я так и не добрался.

Первый забег не увенчался успехом и я подумал, что надо смотреть в сторону индексов и внешних ключей. На этом фоне ко мне пришло прозрение о существовании в архиве справочников и логической модели данных. Я полностью переписал схему базы. Так же в силу ограниченного места решил всё писать сразу в основную схему.

Тут родился некий порядок – сначала загружались справочные данные (например, типы домов, квартир и т.д.), и только после этого основные данные адресов. Отчасти, это сработало, так как за очередную ночь у меня получилось загрузить всё. Однако, не без ошибок.

Оказывается, блять, в данных есть куча пропусков в виде несуществующих внешних ключей. Например, дом с ИД типа 0, когда в справочнике типов домов записи начинаются с ИД = 1, а с нулем строки вообще нет (при этом, в некоторых справочниках хранится запись '0, Не определено', а в других нет. Почему? Хз). Из-за того, что я использую CopyFrom, а не Insert (из-за надобности загрузки пачками), у меня нет ни механизма ON CONFLICT, ни понимания, какая строка проблемная и пропустить её, обработать и т.д. Просто проёбывался весь батч, а на тот момент это было 10 или 20 тысяч записей.

Вишенкой на торте стала невозможность посчитать таблицу full_address. На тот момент в базе находилось около 220 ГБ данных, с успешным подсчетом таблицы (как выяснилось позднее) к ним добавится ещё ~70ГБ. В одной таблице. Я состарюсь быстрее, чем выполнится этот инсерт.

Вторая итерация – по регионам

В тот момент от начальства поступило следующее предложение – считать full_address для каждого региона на этапе вставки, чтобы разом не пихать 70 ГБ. Звучит логично.

В алгоритм переливки пришлось учитывать регион – это заставило меня сильно отрефакторить сервис, завести структуры сущностей, с которыми сразу связаны названия таблиц и файлов в архиве. Сущности определялись не тупым strings.Contains(s, subStr), а через регулярку. Это также позволило декомпозировать переливку на этапы – по регионам. Рефактор дал возможность догружать как отдельные сущности, так и отдельные регионы.

Как я справился с пропусками в данных? Изучив схему и обозначив справочники, на которые опираются основные данные, я загружаю их в первую очередь (их совсем немного), и все эти значения выгружаю в гошную память. Не забыл вставить и значение -1 на случай пропуска, чтобы не было ошибки FK violation.

Сначала по невнимательности эти значения я положил в массив, а не словарь, из-за чего в памяти валялся справочник на 19 миллионов значений, и каждый батч одной из сущностей (10-20к записей) проверялся на наличие одного из этих значений. Я впервые услышал, как работает система охлаждения Mac mini на M4 Pro на полную мощность. Нихера он не тихий иногда) gar_4

Переписав этот кусок на словарь, стало получше. В целом, потом от тех 19 миллионов появилась возможность отказаться, т.к. они нужны для истории изменений, которая на данный момент не в приоритете. Как я и говорил, при желании это всё легко отдельно догрузить.

Почти победа. Однако, большинство регионов по непонятным причинам не посчитались в full_address, из-за чего в базе лежали только Адыгея, Архангельск и Удмуртия) Переливал я это всё в сумме добротные двое суток, и всё равно нормально не вышло.

Третья итерация – партиции

Почти отчаявшись, я понимаю, что нужно попробовать партиционирование таблиц. Примерно в тот же момент мне пришла стратегическая мысль – зачем мне всегда хранить данные всех сущностей, если нужна только full_address? Зачем я отказался от unlogged-таблиц и жду сутками результатов загрузки?

Не без помощи ИИ я переписал миграции в базе. Теперь вся движуха находится в stage-схеме – 1610 партиционированных unlogged-таблиц на каждую сущность на каждый регион. Также, были созданы партиции full_address-таблицы для каждого региона. Каждый регион подсчитывается в свою партицию и эта партиция переносится в одну из немногих в основной схеме таблицу с индексами и ключами – full_address. После окончания переливки каждый регион подчищает все свои партиции – таким образом удается занимать НЕ СЛИШКОМ ДОХУЯ места. Более того, я въебал размер батча в 100 и 200к строк. Ну а че)

Параллельно обрабатывались 8 регионов по 10 сущностей в каждом. Итого: 80 горутин. Ну а че)

И это сработало. Проснувшись утром, я увидел заветное “proccessed all” в логах. И ни единой ошибки. Итоговый размер базы упал с 290 ГБ (как было в прошлом без full_address) до 70 ГБ. Незадолго до этого я оформил необходимые REST-ручки для получения адресов внешими сервисами / пользователями.

Два месяца меня мучала эта хуйня. Пришлось даже расчехлить AnyDesk, чтобы оперативно тестить различные подходы удалённо с ноутбука (архив то ещё поди скачай).

Не знаю, как-то воодушевило меня то, что я смог найти успешные решение и подход. Интересная задача, ничего подобного до этого я ещё не делал.

Предстоит эту дуру перенести с дева на прод – благо существует Yandex Data Transfer. Но предвкушаю ещё какие-нибудь приколы в будущем… посмотрим)

© 2025 Валера Алюшин