Pull to refresh

Парсинг 0.5Tb xml за несколько часов. Поиск организаций в открытых данных реестра субъектов МСП ФНС

Reading time9 min
Views13K
По роду деятельности (автоматизация процессов и разработка архитектуры информационных систем) часто приходится сталкиваться с необходимостью написать скрипт и получить результат «здесь и сейчас» для неожиданно «прилетевшей» задачи в ситуации, когда нет возможности оперативно привлечь внешних разработчиков.

Решению одной из таких задач будет посвящен обзор. В какой-то момент появилась необходимость проанализировать на основе открытых данных “Единого реестра субъектов малого и среднего предпринимательства” Федеральной налоговой службы (далее Реестр МСП) динамику по месяцам количества организаций определенного вида деятельности, а именно, сельхозпредприятий. Подходы, которые использовались при ее решении, надеюсь будут полезны тем, кто ищет варианты обработки больших структурированных массивов данных XML, но распространенные средства обработки такие как SelectFromXML, он-лайн XML обработчики по каким-то причинам не подходят. Либо ограничен функционал, либо возникают проблемы при работе с кириллической кодировкой, либо не обеспечивается необходимая производительность, либо ограничены ресурсы «железа». Программисты и профессионалы надеюсь не буду слишком строги к стилю кодирования и выбору способов реализации, а критика и советы в комментариях приветствуются.

Итак задача:

На февраль 2018 года реестр МСП содержит 18 zip-архивов размером 3-4Gb. Каждый архив содержит около 5-6 тыс. файлов, содержащих сведения о примерно 6 миллионах организаций, общим объемом около 40Gb. Из этого массива требуется отобрать только те, которые относятся к сельхозпредприятиям и проанализировать динамику количества этих предприятия по месяцам.

Исходные файлы ФНС размещены по ссылке

Файлы описания организаций содержат следующую структуру:

<Файл ИдФайл="VO_RRMSPSV_0000_9965_20170110_01b07970-41d2-4d1e-bb80-0abee395d333" ВерсФорм="4.01" ТипИнф="РЕЕСТРМСП" КолДок="900">
<ИдОтпр>
<ФИООтв Фамилия="-" Имя="-"/>
</ИдОтпр>
<Документ ИдДок="4e28d9a9-c004-0f72-a27d-7d677620df81" ДатаСост="10.01.2017" ДатаВклМСП="01.08.2016" ВидСубМСП="2" КатСубМСП="1" ПризНовМСП="2">
<ИПВклМСП ИННФЛ="636204531704">
<ФИОИП Фамилия="МАРЫШЕВ" Имя="ВЯЧЕСЛАВ" Отчество="ВЛАДИМИРОВИЧ"/>
</ИПВклМСП>
<СведМН КодРегион="63">
<Регион Тип="ОБЛАСТЬ" Наим="САМАРСКАЯ"/>
<Район Тип="РАЙОН" Наим="БЕЗЕНЧУКСКИЙ"/>
<НаселПункт Тип="УЛИЦА" Наим="СОВЕТСКАЯ"/>
</СведМН>
<СвОКВЭД>
<СвОКВЭДОсн КодОКВЭД="42.21" НаимОКВЭД="Строительство инженерных коммуникаций для водоснабжения и водоотведения, газоснабжения" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="52.21.2" НаимОКВЭД="Деятельность вспомогательная, связанная с автомобильным транспортом" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="74.30" НаимОКВЭД="Деятельность по письменному и устному переводу" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="63.91" НаимОКВЭД="Деятельность информационных агентств" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="95.23" НаимОКВЭД="Ремонт обуви и прочих изделий из кожи" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="42.21" НаимОКВЭД="Строительство инженерных коммуникаций для водоснабжения и водоотведения, газоснабжения" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="62.09" НаимОКВЭД="Деятельность, связанная с использованием вычислительной техники и информационных технологий, прочая" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="25.72" НаимОКВЭД="Производство замков и петель" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="47.54" НаимОКВЭД="Торговля розничная бытовыми электротоварами в специализированных магазинах" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="42.22.1" НаимОКВЭД="Строительство междугородних линий электропередачи и связи" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="47.99" НаимОКВЭД="Торговля розничная прочая вне магазинов, палаток, рынков" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="82.19" НаимОКВЭД="Деятельность по фотокопированию и подготовке документов и прочая специализированная вспомогательная деятельность по обеспечению деятельности офиса" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="49.32" НаимОКВЭД="Деятельность такси" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="42.22.2" НаимОКВЭД="Строительство местных линий электропередачи и связи" ВерсОКВЭД="2014"/>
</СвОКВЭД>
</Документ>
<Документ ИдДок="7a14e521-68a3-9514-7540-04cb03799ac4" ДатаСост="10.01.2017" ДатаВклМСП="10.09.2016" ВидСубМСП="2" КатСубМСП="1" ПризНовМСП="1">
<ИПВклМСП ИННФЛ="636204538611">
<ФИОИП Фамилия="РУЧКАНОВА" Имя="ЛЮДМИЛА" Отчество="АЛЕКСЕЕВНА"/>
</ИПВклМСП>
<СведМН КодРегион="63">
<Регион Тип="ОБЛАСТЬ" Наим="САМАРСКАЯ"/>
<Район Тип="РАЙОН" Наим="БЕЗЕНЧУКСКИЙ"/>
<НаселПункт Тип="УЛИЦА" Наим="МОЛОДЕЖНАЯ"/>
</СведМН>
<СвОКВЭД>
<СвОКВЭДОсн КодОКВЭД="47.11" НаимОКВЭД="Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="47.25.12" НаимОКВЭД="Торговля розничная пивом в специализированных магазинах" ВерсОКВЭД="2014"/>
</СвОКВЭД>
</Документ>
<Документ ИдДок="ad8636bb-78c3-763c-52d2-4fe5a93e9a8f" ДатаСост="10.01.2017" ДатаВклМСП="10.09.2016" ВидСубМСП="2" КатСубМСП="1" ПризНовМСП="1">
<ИПВклМСП ИННФЛ="636204540794">
<ФИОИП Фамилия="МИЧУРОВА" Имя="ТАТЬЯНА" Отчество="АЛЕКСАНДРОВНА"/>
</ИПВклМСП>
<СведМН КодРегион="63">
<Регион Тип="ОБЛАСТЬ" Наим="САМАРСКАЯ"/>
<Город Тип="ГОРОД" Наим="САМАРА"/>
<НаселПункт Тип="УЛИЦА" Наим="ВЛАДИМИРСКАЯ"/>
</СведМН>
<СвОКВЭД>
<СвОКВЭДОсн КодОКВЭД="47.41" НаимОКВЭД="Торговля розничная компьютерами, периферийными устройствами к ним и программным обеспечением в специализированных магазинах" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="49.20.9" НаимОКВЭД="Перевозка прочих грузов" ВерсОКВЭД="2014"/>
<СвОКВЭДДоп КодОКВЭД="47.78" НаимОКВЭД="Торговля розничная прочая в специализированных магазинах" ВерсОКВЭД="2014"/>
</СвОКВЭД>
</Документ>

Обработка будет выполняться в оболочке bash на виртуальной Linux машине с 2-я ядрами, 8 Gb оперативной памяти и 100Gb дискового пространства:

%Cpu0  :  6.1 us,  2.0 sy,  0.0 ni, 91.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  : 54.1 us, 11.2 sy,  0.0 ni,  6.1 id, 28.6 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8258760 total,    64684 free,  5645284 used,  2548792 buff/cache
KiB Swap:  2129916 total,  1157076 free,   972840 used.  2271428 avail Mem

Скрипт должен обеспечить скачивание zip-архивов с сайта ФНС, переименование файлов для удобства последующей обработки, распаковку, обработку парсером (используется xmlstarlet) для поиска организаций, соответствующих заданных в скрипте критериям, очистку диска от временных файлов (в процессе обработки исходные файлы занимают десятки Gb), сохранение в формате, удобном для последующего использования в системах анализа данных и импорта в программы для работы с электронными таблицами (в нашем случае будет использоваться формат csv).

Скачивание и переименование выполним с использованием wget. Чтобы скрипт понимал, какие архивы с РМСП ему обрабатывать, создадим файл, под условным названием «полетное задание», где укажем, какие файлы обрабатывать и как именовать полученный результат.

Конфигурационный файл имеет следующую структуру:
Ссылка на файл, название результирующего файла, отметка о необходимости обработки '*' (для случаев, если возникает необходимость загрузить не весь набор файлов).

rmspfiles.txt

http://data.nalog.ru/opendata/7707329152-rsmp/data-08262016-structure-08012016.zip;20160826;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-09102016-structure-08012016.zip;20160910;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-10102016-structure-08012016.zip;20161010;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-11252016-structure-08012016.zip;20161125;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-12122016-structure-08012016.zip;20161212;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-01112017-structure-08012016.zip;20170111;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-02102017-structure-08012016.zip;20170212;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-03102017-structure-08012016.zip;20170310;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-04102017-structure-08012016.zip;20170410;*
http://data.nalog.ru/opendata/7707329152-rsmp/data-05102017-structure-08012016.zip;20170510
http://data.nalog.ru/opendata/7707329152-rsmp/data-11062017-structure-08012016.zip;20170611
http://data.nalog.ru/opendata/7707329152-rsmp/data-07102017-structure-08012016.zip;20170710
http://data.nalog.ru/opendata/7707329152-rsmp/data-08102017-structure-08012016.zip;20170810
http://data.nalog.ru/opendata/7707329152-rsmp/data-09112017-structure-08012016.zip;20170911
http://data.nalog.ru/opendata/7707329152-rsmp/data-10102017-structure-08012016.zip;20171010
http://data.nalog.ru/opendata/7707329152-rsmp/data-11102017-structure-08012016.zip;20171110
http://data.nalog.ru/opendata/7707329152-rsmp/data-12112017-structure-08012016.zip;20171211
http://data.nalog.ru/opendata/7707329152-rsmp/data-01112018-structure-08012016.zip;20180111

По завершению скачивания с переименованием запускается цикл по полученным архивам: unzip, поиск в XML нужных записей, запись результата в csv файлы. Перед обработкой следующего архива очищается место на диске от исходных файлов.

Не смотря на простоту задачи, скрипт пройдя этапы отладки и совершенствования получился достаточно замысловатым.

Итак, что получилось в итоге:

Загрузчик файлов:

#!/bin/bash

# **************** batch downloader from rmsp v 1.0. 2018-02-15 ***********************
start=`date +%s`
dt=`date`
logFn='output_wget.log'

printf "********************************************************************************************\n" | tee tmp_output.log
echo "*                            ${dt} wget                                       *" | tee -a tmp_output.log
printf "*********************************************************************************************\n\n" | tee -a tmp_output.log

# download loop считываем файлы по ссылкам из “полетного задания”,  переименовываем и сохраняем в папке zip2
IFS=';' 

while read line; do    
	read -r -a array <<< "$line"
	echo "${array[0]} | ${array[1]} "
#    	wget ${array[0]} -O ./zip2/${array[1]}.zip | tee -a tmp_output.log  2>&1 

# get filesize of external - этот параметр пишется в лог для оценки производительности обработчика 

	FILESIZE=$(wget --spider ${array[0]}  2>&1 | awk '/Length/ {print $2}')

# - c - continue,    3>&1  - размер файла

	wget -c ${array[0]} -O ./zip2/${array[1]}.zip 3>&1 | tee -a  tmp_output.log 
	end=`date +%s`; runtime=$((end-start)); dt=`date '+%Y-%m-%d %H:%M:%S'`
	printf "%s %4d sec %10d %s [ %s" ] ${dt} $runtime $FILESIZE ${array[0]} ${array[1]} | tee -a tmp_output.log
done < rmspfiles.txt

echo "" | tee -a tmp_output.log  //записываем в файл для последующей отладки результаты работы 

cat tmp_output.log $logFn  > tmp_output2.log;  mv tmp_output2.log $logFn

2. Парсер

#!/bin/bash
# 2018-02-16 Версия 1.1 Добавлены столбцы в итоговый файл
# 2018-02-19 Добавлены кавычки для предотвращение переноса строки в номерах лицензий в excell
# 2018-02-19 Добавлен sed для замены /n -> ;  @@;  -> \n
# удалены для лицензий кавычки 
# задаем разделитель колонок для итоговых файлов (в нашем случае табуляция)
sp='	' 

# Задаем параметры обработчика, пути для исходных и результирующих файлов, названия файлов для журналов обработки. 


path_src="./src"
path_zip="./zip2"
path_res="./res"
t1="p1.log"
t2="p2.log"
t3="parsz.log"
 
fnExt=""$1


start=`date +%s`
dt=`date '+%Y-%m-%d %H:%M:%S'`

# Результат выводим в лог

echo "**** | parsz | ${dt} unzip from: $path_zip/$fnExt.zip to $path_src/$fnExt" 
# | tee $t1

# -q  quiet mode (-qq => quieter)
# -o  overwrite files WITHOUT prompting    
# -j  junk paths. The archive's directory structure is not recreated; all files are deposited in the extraction directory (by default, the current one).


unzip -j -q -o $path_zip/$fnExt.zip -d $path_src/$fnExt/

end=`date +%s`
runtime=$((end-start))

MOREF1=`ls  "$path_src/$fnExt/" | wc -l`

echo "     ${dt}, $runtime sec [${MOREF1}] | files from: $path_src/$fnExt/ to $path_res/$fnExt.csv" | tee -a $t1

echo "ИНН$spНаименование МСП\
$spКатегория МСП\
$spВид МСП\
$spВид Деятельности (Основной ОКВЭД)\
$spРегионНаим\
$spРайонТип\
$spРайонНаим\
$spгородТип\
$spгородНаим\
$spНаселПунктТип\
$spНаселПунктНаим\
$spДатаСост\
$spДатаВключения\
$spНомерЛицензии\
$spФайлИмя@@\
" > $path_res/res-$fnExt.csv


/usr/bin/find $path_src/$fnExt/ -name "*.xml" | xargs -n1 xmlstarlet sel -T -f -t -m "//Документ/ОргВклМСП[contains(@НаимОрг,'СЕЛЬСКОХОЗЯЙСТВЕНН')]" \
-v "@ИННЮЛ" -o "$sp" \
-v "@НаимОрг" -o "$sp" \
--if "../@КатСубМСП=1" -o "Микро" --else --if "../@КатСубМСП=2" -o "Малые" --else -o "Средние" --break --break -o "$sp"  \
--if "../@ВидСубМСП = 1" -o "Организация" --else -o "ИП" --break  -o "$sp[" \
-v "../СвОКВЭД/СвОКВЭДОсн/@КодОКВЭД"  -o "]" \
-v "../СвОКВЭД/СвОКВЭДОсн/@НаимОКВЭД" -o "$sp" \
-v "../СведМН/Регион/@Наим" -o "$sp" \
-v "../СведМН/Район/@Тип" -o "$sp" \
-v "../СведМН/Район/@Наим" -o "$sp" \
-v "../СведМН/Город/@Тип" -o "$sp" \
-v "../СведМН/Город/@Наим" -o "$sp" \
-v "../СведМН/НаселПункт/@Тип" -o "$sp" \
-v "../СведМН/НаселПункт/@Наим" -o "$sp" \
-v "../@ДатаСост" -o "$sp" \
-v "../@ДатаВклМСП" -o "$sp" \
-v "../СвЛиценз/@НомЛиценз" -o "$sp" \  
-o "$fnExt@@" \
-n >> $path_res/res-$fnExt.csv

end=`date +%s`
runtime=$((end-start))

dt=`date '+%Y-%m-%d %H:%M:%S'`

echo "     ${dt}, $runtime sec :parsing" | tee -a $t1

# Удаляем переносы строк в значениях за исключением последних в строках

sed -e ':a;N;$!ba;s/\n/;/g' $path_res/res-$fnExt.csv > $path_res/sed_tmp.csv
sed -e 's/@@;/\n/g' $path_res/sed_tmp.csv > $path_res/res-$fnExt.csv


end=`date +%s`
runtime=$((end-start))

dt=`date '+%Y-%m-%d %H:%M:%S'`
echo "     ${dt}, $runtime sec :sed " | tee -a $t1
cat $t1 $t3  > $t2;  mv $t2 $t3
# удаляем исходные XML файлы

rm -rf $path_src/$fnExt/*
echo "Удаляем исходные XML файлы rm -rf $path_src/$fnExt/*"

rm $t1

Весь массив данных из 18 файлов общим объемом в сотни Gb обрабатывается около 6 часов.
Процесс обработки записывается в файлы для последующей отладки и оптимизации скрипта.

После импорта в MS Excel получаем следующий результат:

Tags:
Hubs:
Total votes 26: ↑20 and ↓6+14
Comments21

Articles