01.06.2020 ИФНС опубликовала новый формат выгрузки данных
17.12.2020 Мягко намекнула, что в 2021 будет использоваться только он
01.09.2021 Это свершилось: теперь просто "полная БД ФИАС" перестала обновляться и требуется использовать ГАР БД ФИАС

Частично импортируем ГАР БД ФИАС в MySQL на PHP.

Новость, мягко говоря, не очень, для тех кому нужно получить иерархию улиц и список домов с почтовыми индексами, особенно учитывая, что КЛАДР до сих жив. А не очень из-за того, что файлик данных с 12Гб резко пополнел до 28Гб. Конечно, можно возразить, что скачал один раз и по чуть-чуть обновляйся. Да, можно, если хранить нужные файлы данных целиком и постоянно накатывать на них обновления, но... наличие багов (даже в полной версии) добавит радости.

Небольшой офф

Возможно имеет смысл написать небольшую библиотеку, функционал которой - скачивать только необходимую часть zip архива для распаковки конкретного файла с web сервера, поддерживающего докачку. Весьма актуально, т.к. из этих 28Гб требуется значительно меньшая часть. Если вы знаете, что это уже есть где-то "из коробки" или реализовано отдельной библиотекой - пожалуйста, напишите.

Теперь новая БД содержит иерархическую информацию об адресных объектах в двух вариантах:
- по административно-территориальному устройству (для упорядоченного осуществления функций государственного управления) - на основе этого код ОКАТО
- по муниципальному устройству (для организации местного самоуправления) - соответственно, ОКТМО
Не каждый объект "доступен" в обоих иерархиях. Например, если мы возьмём г. Карабулак, то по административно-территориальному устройству он находится в республике Ингушетия, а по муниципальному устройству расположен городском округе города Карабулак, который в свою очередь в республике Ингушетия и "не участвует" в административно-территориальном устройстве.

Моей целью является получение двух таблиц только с актуальной на момент импорта информацией об иерархической структуре адресов (от региона до, обычно, улицы) и списка жилых домов (не интересуют гаражи, шахты, подвалы...) с почтовыми индексами. NB! При выборе адреса в реальных проектах рекомендую сохранять полный иерархический путь, а не только идентификатор - впоследствии адрес может стать неактуальным (выбрать его нельзя - дома реально нет, а сохранить информацию корректно необходимо).

Таблица gar_addr, ключевое поле id. Иерархию определяют указывающие на него owner_adm и owner_mun. Субъекты РФ (и Байконур) имеют level=1, owner_adm=owner_mun=0. Содержит информацию о названиях адресных объектов (NAME, TYPENAME) и говорящие за себя OKATO, OKTMO, KLADR. OBJECTGUID, ранее в ФИАС именовался AOGUID, является идентификатором адресного объекта (уникальный для актуальных записей; не уникальный, если используются исторические устаревшие записи). OBJECTID аналогичен по значению OBJECTGUID, но уже целочисленный.

Таблица gar_house - список домов. owner_* указывает на gar_addr.id.
Содержит номер дома в 3 полях: основной и 2 дополнительных (и это не я придумал, ADDNUM2 в актуальных данных отсутствует, хотя ранее использовался) и, соответственно, их типы (например: дом, литера, корпус). Описание этих значений в AS_HOUSE_TYPES...XML. ОКАТО и ОКТМО, почтовый индекс.

Импорт и частичное описание структуры.
Всё описанное ниже реализовано в исходниках.

a) Прежде чем начать, проверим zip файл. Убедимся, что он похож на нужный нам и в нём хотя бы есть файлы as_addr_obj... для каждого интересующего нас региона.

Ранее в ФИАС был один файл со всеми регионами, теперь данные о каждом регионе в своей директории.

b) Импортируем файлы AS_ADDR_OBJ_(дата)_(идентификатор).XML, содержащие информацию об адресных объектах.

Все элементы однотипные, выбираем только ISACTUAL=1 и ISACTIVE=1. И получить мы можем только название объекта и поля OBJECTID, OBJECTGUID. Теперь в этом файле нет указания на дочерний объект, нет данных об ОКАТО, ОКТМО - все они находятся в отдельном XML файле.

c) Проверим, что OBJECTID является уникальным. Если нет - надо разбираться, что это вызвало и писать в ИФНС. Ранее в ФИАС такая проблема часто возникала. Проиндексируем таблицу по этому полю - по нему будет определяться адресный объект при обработке последующих XML с данными.

d) Импортируем файлы AS_HOUSES... - дома. Аналогично, интересуют только актуальные записи и дополнительно отсеиваем по типам (дом, здание, строение, корпус). И в этом файле тоже нет данных, какому адресному объекту принадлежит дом.

e) Проиндексируем дома по OBJECTID и убедимся, что все записи уникальны.

f,g) Настало время создать иерархию. Анализируем файлы AS_ADM_HIERARCHY_... и AS_MUN_HIERARCHY_... отбирая только актуальные записи. Пара OBJECTID и PARENTOBJID указывает на OBJECTID объекта.

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

h) Проиндексируем gar_addr по owner_adm и owner_mun

i) И начнём искать ошибки :) Отметим достижимыми все субъекты РФ. И далее будем отмечать достижимыми все записи, родители которых тоже достижимы, до тех пора, пока количество достижимых не изменится. Так делаем по обоим полям - owner_adm, owner_mun. Если owner_adm = owner_mun = 0, то оказалось так, что мы не можем выбрать этот объект - это ошибка. Информация об этом будет сохранена в отчёте, а запись удалена. Отправляется bug-report

Самизнаетекому

ГАР, полная выгрузка
В файле AS_ADDR_OBJ_20210906_2a908987-3309-454e-9364-b75afd551e12.XML
есть объект с ISACTUAL="1" ISACTIVE="1"

<OBJECT ID="1823960" OBJECTID="95254004" OBJECTGUID="e3b0cdce-8a09-4955-8a43-a85ae759cfde" CHANGEID="138210670" NAME="2-й Мартозанова" TYPENAME="пер" LEVEL="8" OPERTYPEID="10" PREVID="0" NEXTID="0" UPDATEDATE="2020-01-15" STARTDATE="2020-01-15" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="1">

однако, его OBJECTID="95254004" вообще не встречается в AS_ADM_HIERARCHY_20210906_221e769c-cfac-4af6-9a20-04cc9c2e1fe5.XML AS_MUN_HIERARCHY_20210906_214fdb76-13c8-49cf-90ef-b5f05c4ee6df.XML

Таких проблем в выгрузке от 07.09.2021 - 107 (из 1405143+107 записей)

j) Теперь проверяем дома. Удаляем и логируем записи без owner_*, т.к. до них мы не сможем добраться. Логично считать ошибкой отсутствие owner в любом из типе устройств - дом же есть, значит до него надо как-то добраться. Разбираться с такими ситуациями придётся вручную (аналогично предыдущему пункту).

В выгрузке от 07.09.2021 - по owner_adm все дома достижимы; по owner_mun - 1639 домов (из 25842972 интересующих) не имеют владельца.

Надеяться, что дом обычно расположен на конкретной улице и owner_adm должен совпадать с owner_mun не получится. Крайне малое количество домов имеют разных владельцев, например один и тот же дом "Х":
Башкортостан, Уфимский р-н, Зубовский с/с, д. "Х"
Башкортостан, Уфимский м.р-н, с.п. Зубовский сельсовет, тер. СНТ Авиатор, ул N1, д. "Х"

В выгрузке от 07.09.2021 - не совпадающих owner_adm и owner_mun всего 2231 объект и есть огромное желание пренебречь одним из столбцов owner_*.

k) Настало время заполнить OKATO, OKTMO и KLADR. Информация о них в файле AS_ADDR_OBJ_PARAMS_... и надо выбрать VALUE из актуальных записей соответствующего TYPEID (6,7,11). Какие данные ещё есть в этом файле указано в AS_PARAM_TYPES_....XML

Всё просто. Из плюсов, что при обработки иерархии мы не знаем к какому объекту относится OBJECTID - здесь же всё однозначно, искать надо только по таблице addr.

l) Аналогично собираем данные OKATO, OKTMO, POSTALCODE для домов. Это уже другой файл - AS_HOUSES_PARAMS_... с аналогичной предыдущему структурой.

m) Удаляем вспомогательные столбцы и индексы

n,o) Выполняем слияние всех таблиц по регионам в одну общую.
p) Создаём нужные индексы
q) Переименовываем временные таблицы в нормальные имена

Чтобы получить этот результат надо обработать:

XML файлы

Описание

Исходные данные, Гб.

AS_ADDR_OBJ_...

Адресные объекты

0.53

AS_HOUSES_...

Дома

18.77

AS_ADM_HIERARCHY_...

Административно-территориальное и муниципальное устройство. Как минимум один набор необходим.

22.18

AS_MUN_HIERARCHY_...

21.68

AS_ADDR_OBJ_PARAMS_...

Параметры адресных объектов, не обязательно если вам не нужны ОКАТО, ОКТМО

3.74

AS_HOUSES_PARAMS_...

Параметры домов (здесь "спрятаны" почтовые индексы)

61.07

У меня этот процесс занял 28 часов 13 минут.

Приложения:
Исходный код, реализующий импорт. (для Windows; для Linux придётся переписать bat на *sh и команды извлечения файла из архива - поискать exec в php файлах).

P.S.

MySQL dump - только адресные объекты (67Мб).
Вероятно, в дальнейшем дамп будет удалён - потеряет свою актуальность; формат, используемый мной, изменится. Обычно информацию по домам и их индексам я группирую по владельцам - что позволяет уменьшить размер БД до 400Мб и использовать её даже на слабых VDS.

P.P.S. locdb.ru давно не обновлялся, но, надеюсь, скоро выложу актуальную базу. "Живых" населённых пунктов стало значительно больше: не только не менее 1000 жителей, также учитываются населённые пункты с большим количеством домов.

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


  1. zartdinov
    14.09.2021 03:12

    Когда то давно хотел на лету парсить конкретные csv файлы из zip-архива чтобы не тратить много памяти. Позже выяснилось, что нужно полностью скачивать весь архив, т.к. важные для распаковки метаданные находятся в конце этого файла (central directory). Но недавно на хабре видел статью, где автор утверждал, что его библиотека на js (https://github.com/greggman/unzipit) умеет это делать (статью похоже удалили). Я немного изучил его реализацию, похоже это действительно возможно, есть несколько путей:
    1) Считывать архив дважды (возможно будет долго, но не придется хранить целиком)
    2) Использовать метаданные файлов (local file header), которые тоже встречаются в архиве (но как понял, на них не стоит рассчитывать).
    3) Если сервер поддерживает HTTP Range requests, то можно сразу обратиться к концу файла и сразу получить нужные метаданные для распаковки.
    В любом случае, я думаю вам стоит попробовать эту библиотеку. Отпишитесь, если найдете решение получше.


    1. Elsajalee Автор
      14.09.2021 03:33

      Спасибо. Решение получше придётся написать самому и, похоже, теоретически сложного-то ничего нет. Возможно, некоторые проблемы могут возникнуть только в больших размерах получаемых данных (тут у меня возник другой вопрос, "а как оптимально скачивать cUrl-ом >20Гб в PHP"). JS (ес-но для NodeJS) меня не устраивает. Вспомнил:

      Простой PHP код для создания ZIP файлов на лету (который я применяю для docx, xlsx...)
      class ZIPMaker {
          private $archiveFiles = 0;
          private $archiveData = '';
          private $archiveHeader = '';
          private $relOffset = 0;
          private function unix2DosTime( $ts = 0 ){
              $timeRR = getdate( $ts ? $ts : time() );
              if ( $timeRR['year'] < 1980) {
                  $timeRR['year'] = 1980;
                  $timeRR['mon'] = $timeRR['mday'] = 1;
                  $timeRR['hours'] = $timeRR['minutes'] = $timeRR['seconds'] = 0;
              }
              return
                  ( ( $timeRR['year'] - 1980 ) << 25 ) |
                  ( $timeRR['mon'] << 21 ) |
                  ( $timeRR['mday'] << 16 ) |
                  ( $timeRR['hours'] << 11 ) |
                  ( $timeRR['minutes'] << 5 ) |
                  ( $timeRR['seconds'] >> 1 );
          }
          function addFile( $name , $data , $ts = 0 ){
              $name = str_replace('\\', '/', $name);
              $HEXTime = pack('V', $this->unix2DosTime($ts));
              $crc = crc32($data);
              $dataGZ = gzcompress($data,9);
              $dataGZ = substr(substr($dataGZ, 0, strlen($dataGZ) - 4), 2); // fix crc bug
              $bin =
                  "\x50\x4b\x01\x02".
                  "\x00\x00".                  // version made by
                  "\x14\x00".                  // version needed to extract
                  "\x00\x00".                  // gen purpose bit flag
                  "\x08\x00".                  // compression method
                  $HEXTime.                    // last mod time & date
                  pack('V', $crc).             // crc32
                  pack('V', strlen($dataGZ)).  // compressed filesize
                  pack('V', strlen($data)).    // uncompressed filesize
                  pack('v', strlen($name)).    // length of filename
                  pack('v', 0).                // extra field length
                  pack('v', 0).                // file comment length
                  pack('v', 0).                // disk number start
                  pack('v', 0).                // internal file attributes
                  pack('V', 32).               // external file attributes
                  pack('V', $this->relOffset). // relative offset of local header
                  $name;
              $this->archiveHeader .= $bin;
              $bin =
                  "\x50\x4b\x03\x04".
                  "\x14\x00".                 // ver needed to extract
                  "\x00\x00".                 // gen purpose bit flag
                  "\x08\x00".                 // compression method
                  $HEXTime.                   // last mod time and date
                  pack('V', $crc).            // crc32
                  pack('V', strlen($dataGZ)). // compressed filesize
                  pack('V', strlen($data)).   // uncompressed filesize
                  pack('v', strlen($name)).   // length of filename
                  pack('v', 0).               // extra field length
                  $name.
                  $dataGZ;
              $this->archiveData .= $bin;
              $this->relOffset += strlen($bin);
              $this->archiveFiles++;
          }
          function bin(){
              return
                  $this->archiveData.
                  $this->archiveHeader.
                  "\x50\x4b\x05\x06\x00\x00\x00\x00".
                  pack('v', $this->archiveFiles).          // total #of entries "on this disk"
                  pack('v', $this->archiveFiles).          // total #of entries overall
                  pack('V', strlen($this->archiveHeader)). // size of central dir
                  pack('V', $this->relOffset).             // offset to start of central dir
                  "\x00\x00";                              // .zip file comment length
          }
          function passthru( $fileName = 'attachment.zip' , $mimeType = 'application/zip' ){
              header('Content-type: '.$mimeType);
              header('Content-length: '.strlen($ctx = $this->bin()));
              header('Content-Disposition: attachment; filename='.$fileName);
              echo $ctx;
          }
      }
      

      И благодаря:

      return
      $this->archiveData.
      $this->archiveHeader.
      "\x50\x4b\x05\x06\x00\x00\x00\x00".
      pack('v', $this->archiveFiles).          // total #of entries "on this disk"
      pack('v', $this->archiveFiles).          // total #of entries overall
      pack('V', strlen($this->archiveHeader)). // size of central dir
      pack('V', $this->relOffset).             // offset to start of central dir
      "\x00\x00";                              // .zip file comment length
      

      Всё относительно очевидно, если сервер отдаёт размер, то считываем и кэшируем заголовок с файлами, а дальше знаем что и откуда получить.


  1. Kwisatz
    14.09.2021 05:40

    Немного не понимаю зачем вы предлагаете MySQL. Во первых если вам понадобится поиск по guid то он, ввиду отсутствия у mysql uuid типа, может быть болезненным по скорости. Во вторых триграм индексы из пг это как раз то что доктор прописал для поиска по таким данным.


    1. Elsajalee Автор
      14.09.2021 06:52

      a) Потому что всё равно. Да и статья-то не про это, я даже постарался ни одной строчки кода в ней не разместить. Тот же код и алгоритм у меня сделает (и делает) PostgreSQL. Я бы сам придрался к php :)

      b) Потому что LAMP.

      Кстати, массовый поиск по UUID при кейсе "выбираем только актуальное" нужен крайне редко (в реальности, только если зачем-то пришлось проверять старые адреса, ес-но хранящие этот UUID, обычно улицы, на возможно изменившийся новый). При менее 1.5млн записей в 280Мб данных не напрягает даже на MySQL.

      Не вижу смысла текстового поиска с учетом опечаток по таким данным, когда выбор по первым двум буквам улицы внутри населенного пункта сокращает результат до десятков, а чаще меньше, вариантов. И выбор самого населенного пункта по 2-3 буквам с учетом количества населения\числа домов - тоже десятки вариантов.


  1. tobolenok
    14.09.2021 09:24

    В 2017 делал парсинг XML ФИАС в Oralce, когда она весила 24 Гб.

    Многопоточное приложение на Delphi.

    Основной поток разбирал по кусочкам XML с буфером около 2 Мб. И скармливал дочерним потокам в текстовый массив, а они в свою очередь делали инсерты.

    Лучшая производительность была при установке потоков = кол-ву виртуальных ядер на клиенте.

    Первые 5 Гб сервер съедал быстро, по 6 Мб/сек на поток, а потом начинал тормозить. Так и не выяснили почему.

    Всего на 24 ГБ уходило около 4 часов. На клиенте прога занимала менее 50 Мб оперативки.


    1. Elsajalee Автор
      14.09.2021 09:31

      Вот только же не было такого размера в 2017. Не доросла она до 24Гб, остановилась на 12Гб. А 19,7 Гб версия ГАР БД ФИАС появилась уже в 2020 году только (о чем до сих пор написано на сайте ИФНС) и за год выросла до 28,9 Гб.

      Аналогичные данные, что здесь (без owner_mun) 31.08.2021 получались на php за 3 часа. Не спешно, в один поток.


      1. tobolenok
        14.09.2021 10:18

        Мы же про разархивированные данные говорим?


        1. Elsajalee Автор
          14.09.2021 12:42

          А как я должен догадаться? Вы же не указали таблицы, а один размер - у архива. Мне что-то кажется, это относительно долго, т.к. AS_ADDROBJ + AS_HOUSE дошел до 44 Гб в 2021. В 2020 году он был 36 Гб и помню, что я в 2,5 часа укладывался. А теперь "ужас" более 128Гб :(


  1. nutz
    14.09.2021 13:05
    -1

    Коллеги, а как используете эту базу? Чисто для подсказок такие объемы выглядят дико... а других применений мне пока не приходит в голову...


    1. Elsajalee Автор
      14.09.2021 14:06

      Объемы-то для подсказок небольшие. Адреса от региона до улицы - 273Мб, и все дома с почтовыми индексами - 400Мб (если не нужны индексы и того меньше). Справляется даже слабенькая VDS.

      Когда заключается договор, адреса (по прописке; или доставки, когда работаешь по куче регионов) следует ввести не абы как менеджеру или клиенту захочется. Что бы не было опечаток. Поэтому только выбор.

      Дико только парсить такие объемы, чтобы получить этот результат :(


      1. freeExec
        15.09.2021 16:21

        И что у вас ни разу не было, что адрес прописки не существует в ФИАС?


        1. Elsajalee Автор
          15.09.2021 16:43
          +1

          Дома ещё иногда встречаются отсутствующие или почтовые индексы неопределенные\неверные у новых домов. Улицы уже давно без проблем. Произвольный "хвост" никто не отменял, но используется всё реже и реже. Сейчас посмотрел по двум базам - в совокупности последние 10К реальных адресов (все 2021 год) - не используется, т.е. улица была полностью определена выбором. А дом хранится без привязки к ФИАС, только при выборе в подсказках, статистики о его существовании у меня нет.

          По прописке встречается - если отсутствует актуальный адрес в ФИАС, то обычно проблемы: например, человек прописан в деревне, дома (или даже целой деревни) уже и на карте-то нет. С пожилыми людьми обычно возникает.


  1. SbWereWolf
    14.09.2021 15:13

    Автор спасибо за статью. Можно код на git hub выложить ?


    1. Elsajalee Автор
      14.09.2021 15:51

      Пока не знаю. Мне придется разделять проект, т.к. я уже использую git и в нём значительно больше того, что выложено.


      1. SbWereWolf
        17.09.2021 16:33

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


  1. beho1der
    23.09.2021 08:53

    А подскажите по домам,как теперь нормально собрать номер дома например: дом 1 корпус 4 строение 3 ? Не понятно с ADDNUM1,ADDNUM2,ADDTYPE1, ADDTYPE2


    1. Elsajalee Автор
      23.09.2021 11:59

      Как и раньше, только теперь обозначение числовое. Я пока ещё не использую ГАР в системах подсказки (и ещё не писал "упаковщик" данных домов, чтобы это снова стало менее 500Мб с постовыми индексами), но заметил, что ADDNUM2\ADDTYPE2 отсутствуют в доступных (т.е. актуальных и до которых можно добраться по иерархии) домах.

      Запись реальная, правда недостижимая (мне ИФНС ещё не ответила на письмо выше, глюк у меня или, как обычно, будет исправлено)
      HOUSENUM="37" ADDNUM1="10" ADDNUM2="10" HOUSETYPE="1" ADDTYPE1="1" ADDTYPE2="2"
      влд. 37 влд. 10 д. 10

      HOUSENUM="4" ADDNUM1="94" HOUSETYPE="10" ADDTYPE1="2"
      корп. 4 д. 94 (В ГАР именно так. Приоритеты, что дом раньше делать самостоятельно.)

      Кстати, сможете назвать хоть один такой реальный такой "сложный" адрес, как в вашем примере?


      1. Am0ralist
        23.09.2021 15:41

        Кстати, сможете назвать хоть один такой реальный такой «сложный» адрес, как в вашем примере?

        Возможно этот имелся ввиду?


        1. Elsajalee Автор
          23.09.2021 15:59

          Это как раз то, из-за чего нецензурно выражаться хочется. С улицами всё хорошо, с домами, похоже, стало хуже. Полная запись этого дома в ГАР сейчас выглядит так:

          HOUSE ID="40479359" OBJECTID="67218762" OBJECTGUID="ed525b52-8467-467f-aa0a-ca802d9432b6" CHANGEID="100222233" HOUSENUM="1-3" ADDNUM1="4" HOUSETYPE="2" ADDTYPE1="2" OPERTYPEID="10" PREVID="0" NEXTID="0" UPDATEDATE="2017-04-05" STARTDATE="1900-01-01" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="1"

          Существенное только: HOUSENUM="1-3" ADDNUM1="4" HOUSETYPE="2" ADDTYPE1="2"

          И адрес получается "д. 1-3 д. 4". (Либо надо ещё раз очень внимательно перечитать документацию...)

          А ещё таких HOUSETYPE="2" ADDTYPE1="2" в актуальных - 368670 домов (1.41%) и, похоже, это всё - проблемы.

          В ФИАС этот же дом HOUSENUM="1-3" , STRUCNUM (строение) = "4" - все логично.

          p.s. Кстати, видите, что на карте Яндекса нет "корпуса".
          p.p.s. Меня пока только одно радует - такие сложности, обычно, в нежилых домах.

          Отправил вопрос в ИФНС, на предыдущий (по поводу недостижимых улиц) мне пока не ответили. Но это нормально, всего-то чуть более 10 дней прошло.


          1. QProgS
            07.10.2021 20:36

            С этими домами всё хорошо, для ADDTYPE1 и ADDTYPE2 существует отдельный справочник: AS_ADDHOUSE_TYPES где 2 это строение:

            <HOUSETYPES>
            <HOUSETYPE ID="1" NAME="Корпус" SHORTNAME="к." />
            <HOUSETYPE ID="2" NAME="Строение" SHORTNAME="стр." />
            <HOUSETYPE ID="3" NAME="Сооружение" SHORTNAME="соор." />
            <HOUSETYPE ID="4" NAME="Литера" SHORTNAME="литера" />
            </HOUSETYPES>


            1. Elsajalee Автор
              07.10.2021 20:39

              Да, спасибо. Уже нашел, оценил логику; написать об этом сюда, правда, забыл. Это HOUSETYPES из AS_ADDHOUSE_TYPES_. HOUSETYPES из AS_HOUSE_TYPES_... другие.