Извлекаем почтовые индексы из ФИАС
Проблема с выгрузкой
Проблема с выгрузкой в том, что почтовые индексы есть у домов, но почтовых индексов нет у собственно населённых пунктов, но у населённых пунктов конечно есть дома, и они связаны между собой через таблицу ADM_HIERARCHY
по административному делению.
Собственно задача состоит в том что бы сгруппировать почтовые индексы всех домов и взять минимальный индекс как индекс собственно населённого пункта (индекс населённого пункта обычно заканчивается нолями, у подчинённых почтовых отделений индексы заканчиваются цифрами от 1 до 9).
Но прежде чем начать работать с данными, надо к этим данным получить доступ, разместить эти данные так где к ним можно будет выполнять запросы. Например разместить данные в СУБД.
План работ
Выгрузка почтовых индексов для всех населённых пунктов, состоит из нескольких этапов:
Выгрузить данные с сайта ФНС
Подготовим СУБД для импорта данных ФИАС
Подготовка PHP скриптов для импорта
Импортировать данные в БД
Сформировать выгрузку
Требования к обеспечению
1Tb file storage
PostgreSQL || MySQL
PHP
Composer
Unzip
Для работы PHP скриптов понадобятся следующие расширения:
apt-get install php
apt-get install php-xml
# если в качестве СУБД выбираем PostgreSQL
apt-get install php-pgsql
# если в качестве СУБД выбираем MySQL
apt-get install php-mysql
Ниже будет приведён пример для Ubuntu
24 и СУБД PostgreSQL
. Вся информация по состоянию на 03 января 2025 года.
Скрипты могут работать и с СУБД MySQL, но в MySQL внутри БД нет схемы, поэтому из скриптов надо удалить (закомментировать) строки:
$schema = constant('SCHEMA');
$connection->exec("SET search_path TO {$schema}");
Скачиваем архив с ФИАС с сайта налоговой (ФНС)
Выбрать место для размещения файлов
Определяемся с тем где будем размещать архив ФИАС и другие рабочие файлы (1Tb file storage).
Если у нас виртуалка и нам не хватает места, то или расширяем раздел, или, что лучше для временных файлов, добавляем новый диск, который после получения результата можно будет безболезненно удалить.
Инструкция для линукс систем: Как добавить или расширить диск в Linux.
Допустим новый диск мы подключили к пути mnt/pg-storage/
.
Скачать архив данным ФИАС
Теперь надо получить ссылку на скачивание архива, находим страницу скачивания, для этого:
Забиваем в интернет поисковик «ФНС ФИАС скачать»
Федеральная информационная адресная система
Разработчикам
Копируем ссылку на архив из колонки «ПОЛНАЯ ВЕРСИЯ, XML»
Допустим ссылкой будет gar_xml.zip
Скачиваем архив туда где у нас достаточно свободного места.
wget -P /mnt/pg-storage/download https://fias-file.nalog.ru/downloads/2024.10.29/gar_xml.zip
Развернём архив с помощью unzip, допустим в директорию /mnt/pg-storage/download/gar_xml
Удалить не нужные файлы
Удаляем не нужные файлы
cd /mnt/pg-storage/download/gar_xml
find ./ -name "AS_ADDR_OBJ_DIVISION_*.XML" -exec rm {} \;
find ./ -name "AS_APARTMENTS_*.XML" -exec rm {} \;
find ./ -name "AS_APARTMENTS_PARAMS_*.XML" -exec rm {} \;
find ./ -name "AS_CARPLACES_*.XML" -exec rm {} \;
find ./ -name "AS_CARPLACES_PARAMS_*.XML" -exec rm {} \;
find ./ -name "AS_CHANGE_HISTORY_*.XML" -exec rm {} \;
find ./ -name "AS_HOUSES_*.XML" -exec rm {} \;
find ./ -name "AS_MUN_HIERARCHY_*.XML" -exec rm {} \;
find ./ -name "AS_NORMATIVE_DOCS_*.XML" -exec rm {} \;
find ./ -name "AS_REESTR_OBJECTS_*.XML" -exec rm {} \;
find ./ -name "AS_ROOMS_*.XML" -exec rm {} \;
find ./ -name "AS_STEADS_*.XML" -exec rm {} \;
find ./ -name "AS_STEADS_PARAMS_*.XML" -exec rm {} \;
Подготовим СУБД для импорта данных ФИАС
mkdir -p /mnt/pg-storage/data
Создадим табличное пространство для БД ФИАС.
CREATE TABLESPACE fias_data OWNER postgres LOCATION '/mnt/pg-storage/data';
Создадим БД для ФИАС в соответствующем табличном пространстве.
create database fias with owner postgres tablespace fias_data;
Создадим схему для ФИАС в соответствующей БД
create schema gar;
Подготовка скриптов для импорта
Настраиваем PHP
Узнаём путь к файлу с настройками PHP
php --ini
Откроем ini-файл с настройками PHP в текстовом редакторе (c Ubuntu в комплекте идёт nano
).
Для быстрой работы скриптов надо включить OPCache и JIT (PHP 8.4).
Находим раздел [opcache]
.
opcache.jit=1255
opcache.jit_buffer_size=128M
opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=0
opcache.save_comments=0
;zend_extension = xdebug
xdebug.mode=off
Если установлено какое либо расширение для отладки PHP кода, то отключаем его.
После того как скрипты отработают не забудьте восстановить исходные значения.
Настраиваем опции PHP скриптов
mkdir script && cd script
git clone https://github.com/SbWereWolf/extract-fias-params.git .
composer install
cp config.env.example config.env
nano config.env
Большинство значений можно оставить как есть, кроме реквизитов подключения к СУБД
Устанавливаем соответствующие значения для:
LOGIN
имя для подключения к СУБДPASSWORD
парольDSN
строка подключения PHP скрипта к СУБДSCHEMA
имя схемы, если используем PostgreSQLDO_IMPORT_WITH_CHECK
при импорте всегда FALSEBATCH_SIZE
количество записей для записи в БД за раз (100000)XML_FILES_PATH
путь к файлам для импорта в БД
Настраиваем собственно PHP скрипты
Распределяем файлы для импорта между скриптами, в настоящем варианте файлы разделены между тремя скриптами:
data-import-01.php
data-import-02.php
data-import-03.php
В одном скрипте надо импортировать данные справочников (в работе скриптов справочники не участвуют, но для самостоятельного исследования данных будут полезны)
За импорт справочников отвечает второй аргумент конструктора класса ImportOptions ($referencePatterns
), это значения:
[
AddHouseTypes::class =>
'AS_ADDHOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
AddressObjectTypes::class =>
'AS_ADDR_OBJ_TYPES_20*.{x,X}{m,M}{l,L}',
ApartmentTypes::class =>
'AS_APARTMENT_TYPES_20*.{x,X}{m,M}{l,L}',
HouseTypes::class =>
'AS_HOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
NormativeDocumentsKinds::class =>
'AS_NORMATIVE_DOCS_KINDS_20*.{x,X}{m,M}{l,L}',
NormativeDocumentsTypes::class =>
'AS_NORMATIVE_DOCS_TYPES_20*.{x,X}{m,M}{l,L}',
ObjectLevels::class =>
'AS_OBJECT_LEVELS_20*.{x,X}{m,M}{l,L}',
OperationTypes::class =>
'AS_OPERATION_TYPES_20*.{x,X}{m,M}{l,L}',
ParamTypes::class =>
'AS_PARAM_TYPES_20*.{x,X}{m,M}{l,L}',
RoomTypes::class =>
'AS_ROOM_TYPES_20*.{x,X}{m,M}{l,L}',
],
Соответственно справочники надо импортировать только один раз, значит в одном из скриптов через этот аргумент надо передать список классов парсеров, во всех других скриптах импорта второй аргумент ($referencePatterns
) должен быть пустым массивом — []
, иначе получим дублирование значений в справочниках.
За то, из каких директорий скрипт будет брать файлы для импорта, отвечает третий аргумент ($regionDataDirectoryPattern
шаблон для поиска директорий):
'{01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33}',
Всего директорий около ста, от 01 до 99, соответственно все эти директории надо распределить между скриптами.
Если сервер будет загружен только импортом данных ФИАС, то оптимально будет запустить скриптов на один меньше чем ядер у сервера, то есть для 4-х ядерного — 3 скрипта, для 8-ми — 7 скриптов.
Выполняем импорт данных
Как выполнить импорт полной базы ФИАС описано в статье Импорт полной базы ФИАС за 9 часов, How To.
В настоящем варианте для извлечения индексов городов достаточно четырёх таблиц, скрипты уже соответствующим образом подготовлены.
Импорт состоит из следующих этапов
Создание таблиц в БД
Импорт данных
Создание индексов
Создание таблиц в БД
Создание таблиц в БД это быстрый процесс, его необязательно выполнять как фоновую задачу.
php ./create-indexes.php
В БД будут созданы таблицы.
Импорт данных
Перед импортом данных лучше отключить автовакуум.
ALTER SYSTEM SET AUTOVACUUM TO FALSE;
Импорт данных это долгий процесс, что бы при закрытии терминала импорт не был прекращен, лучше запустить импорт как фоновую задачу:
nohup php data-import-01.php &> import01.log &
Вывод скрипта будет перенаправлен в файл import01.log
, логи можно будет отслеживать командой tail
tail import01.log
Так же каждый скрипт пишет свои логи в директорию logs
.
Запускаем остальные скрипты
nohup php data-import-02.php &> import02.log &
nohup php data-import-03.php &> import03.log &
Когда скрипты закончат свою работу, тогда в логах появиться запись о том что импорт завершён. И в принципе по нагрузке на систему будут понятно (команда top
в помощь).
Импорт может растянуться на несколько часов (6–12 часов). Запустите и займитесь другими делами.
Создание индексов
Индексы создаются не долго, можно обойти без запуска в фоновом режиме:
php ./create-indexes.php
То какие индексы будут созданы для каждой таблицы определяется в соответствующем шаблоне по пути template/%Имя таблицы%/create-index.php
, изменяйте на свой вкус.
После импорта данных, когда индексы созданы, нужно включить сбор статистики для планировщика запросов, и обновить эту статистику.
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.00001;
ALTER SYSTEM SET autovacuum_vacuum_insert_scale_factor = 0.00001;
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.00001;
ALTER SYSTEM SET AUTOVACUUM TO TRUE;
VACUUM FULL VERBOSE ANALYZE;
Сформировать выгрузку
В чём собственно проблема с выгрузкой ?
Проблема с выгрузкой в том, что почтовые индексы есть у домов, но почтовых индексов нет у собственно населённых пунктов, но у населённых пунктов конечно есть дома, и они связаны между собой через таблицу ADM_HIERARCHY
по административному делению.
Собственно задача состоит в том что бы сгруппировать почтовые индексы всех домов и взять минимальный индекс как индекс собственно населённого пункта (индекс населённого пункта обычно заканчивается нолями, у подчинённых почтовых отделений индексы заканчиваются цифрами от 1 до 9).
Почтовые индексы записаны не в таблицу домов, почтовые индексы это дополнительная информация и она записана в таблицу HOUSES_PARAMS
. Поэтому собственно файлы с данными домов HOUSES
мы удалили как не нужные.
Промежуточная таблица с индексами отдельных зданий
Создадим таблицу для почтовых индексов всех домов и заполним её данными
create table zip_code
(
region bigint,
objectid bigint,
value text
)
PARTITION BY LIST (region);
;
create unique index zip_code_region_objectid
on zip_code (region, objectid);
insert into zip_code
select hp.region,
hp.objectid,
hp.value
from houses_params hp
where hp.typeid = 5
and current_date::date between hp.startdate and hp.enddate
order by
hp.region,
hp.objectid
;
В этом запросе можно текущую дату (current_date::date
) поменять на произвольную.
База ФИАС ведётся как кадастр и включает в себя историю изменений, поэтому при запросах надо уточнять на какую дату вам нужны данные:
WHERE current_date::date between hp.startdate and hp.enddate
Кроме того в таблицах есть колонки IsActive
и IsActual
. С этими колонками не совсем понятно чему верить. К гадалке не ходи когда то они все разъезжаются, при промышленной эксплуатации конечно надо ловить такие моменты и как то толковать. Я ФИАСом занимаюсь эпизодически, с подобными проблемами не сталкивался, но можете рассказать в комментариях как оно бывало в вашей практике.
Интересующий нас параметр Почтовый индекс
, имеет идентификатор 5:
hp.typeid = 5
Таблица ZIP_CODE
является промежуточной, можно обойтись без неё, но на этапе разработки постоянно формировать промежуточную выборку это лишнее время на ожидание запроса, мои запросы её используют.
Итоговая таблица с индексами населённых пунктов
Теперь надо создать таблицу для данных выгрузки по почтовым индексам.
create table gar.addr_obj_zip_code
(
region bigint,
zip_code text,
typename text,
name text,
level bigint,
objectid bigint
);
create unique index addr_obj_zip_code_region_objectid_ux
on gar.addr_obj_zip_code (region, objectid);
Данных в ней будет не так много, можно обойтись без партиционирования.
Логика работы запроса на группировку значений
Что бы от дома с почтовым индексом перейти к населённому пункту и передать ему почтовый индекс надо построить иерархию, от дочерних элементов — домов, к родительским — улица, районам и далее до субъекта федерации.
Для каждого дома будет поднята вся «родня» до самого корня, далее для каждого уровня иерархии начиная с 6 будет выбрано минимальное значение индекса.
Запрос с использованием Recursive Common Table Expression
Строим иерархию с помощью CTE.
Нам надо соединить адресный объект (условную улицу) с почтовым индексом дома, у этих сущностей разный набор полей.
Для первой строки набора данных, те поля которых нет у почтового индекса (название адресного объекта, тип адресного объекта, уровень в иерархии) подставляем как пустые.
null as typename,
null as name,
0::bigint as level
Следующую строку в набор данных добавляем по связке идентификатора родительского объекта предыдущей строки с идентификатором объекта следующей.
ah.parentobjid = next.objectid
В следующую строку переносим из предыдущей строки только почтовый индекс ah.value
.
Остальные данные в следующей строке подтягиваются для адресного объекта следующей строки:
уровень в иерархии
a.level
наименование
a.name
название «уровня иерархии»
a.typename
уровень в иерархии
a.level
Для вставки в результирующую таблицу берём только адресные объекты с уровнем от 6 и выше (до 1, потому что LEVEL=»6» это NAME=«Населенный пункт», уровень ниже LEVEL=»7» это NAME=«Элемент планировочной структуры» — микрорайон)
Скрипт работает не долго, на четырёх ядерном процессоре всего 15 минут на всю Россию (я думаю что запрос выполнялся даже на одном ядре, и если как то настроить многопоточность, то будет ещё быстрее).
WITH RECURSIVE ah AS (
SELECT
prev.region,
prev.parentobjid,
prev.objectid,
z.value,
null as typename,
null as name,
0::bigint as level
FROM adm_hierarchy prev
join zip_code z on
prev.region = z.region
and prev.objectid = z.objectid
and prev.isactive = 1
UNION ALL
SELECT
next.region,
next.parentobjid,
next.objectid,
ah.value,
a.typename,
a.name,
a.level
FROM adm_hierarchy next
JOIN ah ON
next.region = ah.region
and next.objectid = ah.parentobjid
and next.isactive = 1
join addressobjects a on
next.region = a.region
and next.objectid = a.objectid
and a.isactive = 1
)
insert
into addr_obj_zip_code
(
region,
zip_code,
TYPENAME,
NAME,
LEVEL,
objectid
)
SELECT
ah.region,
min(ah.value) zip,
ah.typename,
ah.name,
ah.level,
ah.objectid
FROM ah
where ah.level between 1 and 6
group by ah.region, ah.typename, ah.name, ah.objectid, ah.level
;
Саму выгрузку с помощью IDE легко сохранить в любом формате (CSV конечно), как это сделать из командной строки я слава богу пока не знаю :) Можете подсказать в комментариях, кому нибудь пригодиться.
Дополнительная информация по адресным объектам
В ФИАС есть дополнительная информация для адресных объектов, такая как:
ОКАТО (
typeid
=6)ОКТМО (
typeid
=7)КЛАДР (
typeid
=10)
Эту информацию добавить в выгрузку значительно легче.
Добавляем колонку:
alter table addr_obj_zip_code add okato text ;
Запрос на обновление выгрузки:
update addr_obj_zip_code target
set okato = params.value
from
addressobjects source
join addr_obj_params params on
params.region = source.region
and params.objectid = source.objectid
and params.typeid=6
where
target.region = source.region
and target.objectid = source.objectid
and source.isactive=1
and current_date::date between params.startdate and params.enddate
С остальными дополнительными параметрами аналогично, добавляем колонку, подставляем соответствующий typeid
(7
— ОКТМО, 10
— КЛАДР).
Делитесь в комментариях своим опытом работы с ФИАС!