Замечания: статья для совсем маленьких и крутым спецам по кодингу будет не интересно, лучше ее пропустить. В коде первым комментарием поставлена ссылка на расположение файла с этим кодом для удобства и простоты. Главная задача была получить результат в виде таблицы SQLite. Качество кода оцениваем как ниже среднего, но с заявкой на максимальную простоту. Код написан достаточно просто и без пояснений, но готовы исправиться, поясниться.
Вводная
Что хотим сделать: взять данные по юридическим лицам (ЮЛ) РФ за 2019 год (идентификаторы ЮЛ: наименование и ИНН(ЮЛ), оборот, расход) и положить в SQLite.
Для строительства велосипедов использованы (но не пострадали):
- Виндовс 7х64 
- Питон 3.8.10 (если нет, берем тут). 
- В качестве IDE использовалась Visual Studio Code, но абсолютно не принципиально, хоть блокнот. 
Подготовительный этап
Открываем командную строку с правами администратора, создаем папку: (cmd) D:\>mkdir 2021_8_16_oborot Заходим в созданную папку: (cmd) D:\>cd 2021_8_16_oborot И создаем виртуальное окружение для проекта: (cmd) D:\2021_8_16_oborot>python -m venv venv При желании можно его активировать в командной строке: (cmd) D:\2021_8_16_oborot>venv\scripts\activate Код проекта можно запускать и в командной строке, но нам удобнее использовать для этого IDE и ее терминал. VSCode, как правило, активирует виртуальное окружение самостоятельно, но почему-то не всегда.
Качаем
Качаем исходный файл. Лучше каким-нибудь даунлодером (например Downloader Master). При попытках качать базы с ФНС и/или Росстата обычным браузером скачка часто обрывается. Исходный файл берем тут. Кладем исходный файл в папку: D:/2021_8_16_oborot/ish Там же берем описание структуры набора данных (.xsd).
Расзиповываем
Посчитали удобнее сразу расзиповать файлы:
# D\2021_8_16_oborot\16.08.2021_unzip_ziped_directory.py
from zipfile import ZipFile
with ZipFile('D:/2021_8_16_oborot/ish/data-20210801-structure-20180801.zip', 'r') as zipObj:
    zipObj.extractall('D:/2021_8_16_oborot/ish_unziped')
Запускаем в терминале IDE (правая клавиша мыши и "Run Python file in Terminal"). Если забыли установить пакет zipfile терминал будет ругаться на его отсутствие. Потому устанавливаем нужный пакет в терминале IDE (точно так же и в командной строке Виндоус, но не забываем активировать виртуальное окружение проекта): (venv) D:\2021_8_16_oborot>pip install zipfile Предварительно смотрим, что терминал запустился автоматически в виртуальном окружении (по непонятным причинами в VScode обычно запускается виртуальное окружение, но не всегда): (venv) D:\2021_8_16_oborot> Если виртуальное окружение не активировалось самостоятельно, то активируем его в ручную, так же как делали в командной строке Виндоус. Получили итог: ошибок нет, файл расзиповался в 12 060 xml файлов.
Любуемся на xml
Если открыть файлы, то там много лишней (для нашей задачи) информации
<?xml version="1.0" encoding="UTF-8"?><Файл ИдФайл="VO_OTKRDAN5_9965_9965_20210729_000f7269-b452-4bb0-85ea-3fa614ec3342" ВерсФорм="4.01" ВерсПрог="1.0" ТипИнф="ОТКРДАННЫЕ5" КолДок="92"><ИдОтпр><ФИООтв Фамилия="_" Имя="_"/></ИдОтпр><Документ ИдДок="b6db52b9-1955-4c94-8dd8-cbc4a3da40d8" ДатаДок="30.07.2021" ДатаСост="31.12.2019"><СведНП НаимОрг="ОБЩЕСТВО СОГРАНИЧЕННОЙОТВЕТСТВЕННОСТЬЮ "ФИБРОТЕК"" ИННЮЛ="7816394401"/><СведДохРасх СумДоход="0.00" СумРасход="0.00"/></Документ><Документ ИдДок="1cd8ab78.....А нам будут нужны только данные по юридическим лицам. В каждом файле xml их от 1 до 100 (примерно):
Название юридического лица (само наименование и организационно-правовая форма) находится в исходных файлах в "СведНП НаимОрг",
ИННЮЛ (инн юридического лица) в "СведНП ИННЮЛ",
Доход в "СведДохРасх СумДоход",
Расход в "СведДохРасх СумРасход"Нам показалось удобнее распарсить нужные данные в файлы с такими же названиями, но .csv расширениями. Ну и положить в папку D:\2021_8_16_oborot\csv Изначально в тестово-велосипедных целях взяли один файл из 12 060, переименовали его в test.xml и написали парсинг и запись данных в .csv файл.
# D:\2021_8_16_oborot\26.08.2021_finish_xml_to_csv.py
import xml.etree.ElementTree as Xet
f = open('17_your_csv_file.csv', 'w', encoding='utf-8') # открываем файл для записи результата обработки
xmlparse = Xet.parse('D:/2021_8_16_oborot/ish_unziped/test1.xml') 
root = xmlparse.getroot() 
for i in root:
   for i2 in i.findall('СведНП'): 
      f.writelines(i2.attrib['НаимОрг'])
      f.write(',')
      f.writelines(i2.attrib['ИННЮЛ'])
      f.write(',')
      
      for i2 in i.findall('СведДохРасх'): 
         f.writelines(i2.attrib['СумДоход'])
         f.write(',')
         f.writelines(i2.attrib['СумРасход'])
         f.write('\n')
    
f.close()Запустили код, убедились, что все ОК. Данные в каждом из файлов выглядят так:
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ФИБРОТЕК",7816394401,0.00,0.00
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ИЗДАТЕЛЬСКИЙ ДОМ "ТВЕРСКАЯ ЖИЗНЬ",6901026686,0.00,0.00
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "МРАВ",7726503712,20800000.00,14102000.00
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АГРОРЕСУРС",6901026703,470000.00,461000.00Данные по каждому ЮЛ в отдельной строчке, данные разделены запятыми. Теперь можно в цикле брать и парсить все 12 060 файлов.
import os
import xml.etree.ElementTree as Xet
def parse_dii():
   path = 'D:/2021_8_16_oborot/ish_unziped/'
   fileList = os.listdir(path)
   for t in fileList:
      with open('D:/2021_8_16_oborot/ish_unziped/' + t, 'r', encoding='utf-8') as ft:
         base = os.path.splitext(t)[0]
         with open(('D:/2021_8_16_oborot/csv' + base + '.csv'), 'w+', encoding='utf-8') as f:
            xmlparse = Xet.parse('D:/2021_8_16_oborot/ish_unziped/' + t)
            root = xmlparse.getroot()
            for i in root:
               for i2 in i.findall('СведНП'):
               f.writelines(i2.attrib['НаимОрг'])
               f.write(',')
               f.writelines(i2.attrib['ИННЮЛ'])
               f.write(',') 
                  for i2 in i.findall('СведДохРасх'):
                     f.writelines(i2.attrib['СумДоход'])
                     f.write(',')
                     f.writelines(i2.attrib['СумРасход'])
                     f.write('\n')
 
if __name__ == '__main__':
    parse_dii()Сами наступаем на грабли, сами правим
После того, как мы записали все csv файлы оказалось, что мы сделали ошибку в путях (пропустили '/' в этой строчке:
with open(('D:/2021_8_16_oborot/csv' + base + '.csv'), 'w+', encoding='utf-8') as f:) И вместо того, чтобы записать все файлы в директорию D:\2021_8_16_oborot\csv\ они записались в общую директорию проекта, что не гуд. Пришлось набросать код, чтобы найти все эти файлы и переместить их куда планировали изначально.
# D:\2021_8_16_oborot\26.08.2021_peremeshalka_csv_to_csv_1.py
import os
import shutil
def parse_dii():
   old_path = 'D:/2021_8_16_oborot'
   new_path = 'D:/2021_8_16_oborot/csv/'
   fileList = os.listdir(old_path)
    
   for t in fileList:      
      if t[0:25] == 'csvVO_OTKRDAN5_9965_9965_':
         t1 = t[3:]
         with open(new_path + t1, 'w+', encoding='utf-8') as f:
            shutil.move(t, new_path+t1) 
            print(new_path + t1)
if __name__ == '__main__':
    parse_dii()Файлы были успешно переименованы и перемещены.
Питоним неудачно
Дальше мы написали Питоний код для создания базы данных, таблицы и записи данных в цикле из всех 12060 csv файлов в таблицу. К сожалению сделать этого не удалось, выходили разные ошибки, которые мы не смогли пока решить. С этой проблемой мы разберемся и сразу дополним статью.
Невзирая ни на что идем дальше
Но так как основная задача была быстро получить финишный результат, то идем дальше к финишу. Поэтому мы пошли так: слили все данные в один csv файл.
# D:\2021_8_16_oborot\05.09.2021_all_csv_to_one_csv.py
import os
import glob
os.chdir("D:/2021_8_16_oborot/csv/")
extension = 'csv'
all_filenames = [i for i in glob.glob('*.{}'.format(extension))]
combined_csv = '\n'.join([open(f, 'r', encoding='utf-8').read().strip() for f in all_filenames ])
with open("combined_csv.csv", 'w') as f:
    f.write(combined_csv)В процессе работы код использовал почти 0,7ГБ памяти, но благополучно записался.
Импорт единого csv в SQLite
Раз уж у нас сегодня день велосипедостроения, то для импорта единого файла используем менеджеры SQLite вместо Питона.
В итоге:
- DB browser (SQLite) не смог импортировать этот файл. Вылетел с записью "Это не ошибка, но в записи № 1 125 507 чой-то не то. 
- Аналогично не импортнул и SQLite Expert 
- Единственный, кто смог импортировать был SQLite Studio (ver.3.3.3). Было записано 1 089 044 строчек в таблицу. 
По идее каждая строчка должна быть отдельным данными по одному ЮЛ. Но как выяснилось в таблице нашлось небольшое количество битых строк, у которых в одну ячейку записались данные нескольких ЮЛ, чего, естественно быть не должно. Разбираемся, что это могло быть. Пока предварительная идея, что в исходных данных или в csv файлах есть битые данные, которые ломают правильное формирование таблицы. Но это нужно проверить, найти ошибку.
В завершение
Планы по исправлению, нахождению причины: продетектить на ошибки (и соответствие схеме) всю исходную директорию, всю директорию с csv файлами, весь "большой" csv файл. Поискать ошибки при записи в таблицу Питоном, вывести ошибки обработки - записи в таблицу.
Замечание. В то же время получить за раз примерно миллион строк с данными по ЮЛ вполне себе неплохо. Если учесть, что максимальное количество действующих ЮЛ доходило по РФ до 4,5 миллиона, а в последнее время было порядка 3 - 3,5 миллиона. То есть у нас есть примерно треть от всех действующих компаний в таблице присутствует. При этом предполагаем, что двойников в таблице нет. Тоже, кстати, надо будет сделать - проверить.
P.S. Код проекта в github пока не положен, в процессе. Будет положен сюда
P.S.2 Изначально, когда мы получили xml-ки, то проверили один из файлов на соответствие схемы. Все было ОК. Но, как мы предполагаем в каких-то из файлов есть ошибки. Когда проверим обновим статью.
А это код проверки только одного (из 12 060) файла на соответствие схемы.
# D:\2021_8_16_oborot\2021_8_16_xsd_validate_xml_file.py
import lxml
from lxml import etree
xml_file = lxml.etree.parse("D:/2021_8_16_oborot/ish_unziped/VO_OTKRDAN5_9965_9965_20210729_000f7269-b452-4bb0-85ea-3fa614ec3342.xml")
xml_validator = lxml.etree.XMLSchema(file="D:/2021_8_16_oborot/structure-20180110.xsd")
is_valid = xml_validator.validate(xml_file)
print(is_valid)P.S.3. Сильно не бейте, это наш первый опус на Хабре.
Комментарии (15)
 - Chuvi09.09.2021 21:07+1- А зачем сначала делать csv и из него SQLite, когда можно из питона сразу в SQLite?  - nasingfaund Автор09.09.2021 21:16+1- Да, конечно, можно, но в учебно-образовательных целях так удобнее и нагляднее. 
 
 - uaggster10.09.2021 06:17+1- Это задача из разряда, когда можно и нужно обойтись без программирования. Достаточно воспользоваться правильным средством. - Берем MSSQLSERVER, можно даже бесплатный express. 
- Создаем БД. 
- В БД создаем Filetable с внешним доступом и обычную таблицу, в которую поместим результат. 
- Копируем в Filetable xml-и с данными прямо проводником Windows. С распаковкой из zip-a. 2019 MSSQLSERVER умеет распаковывать zip, но зачем? 
- Выполняем запрос (тут мог бы быть текст запроса, если бы вы привели структуру исходного xml. Впрочем, аналогичные задачи показывают, что это примерно 10 строк в среднем) в SSMS. 
 - Собственно, всё. 2 миллиона записей будут импортироваться, навскидку, по такой схеме, минуты 3, ЕМНИП. Ну, хорошо, 5, хотя скорее всего - одну. - Для реальных задач нужно пользоваться острозаточенным инструментом. - :-)  - nasingfaund Автор10.09.2021 08:37- Каждую хорошую учебную задачу можно и нужно уметь решать разными путями. В данном случае программирование и Питон были обязательными для использования, обучения. 
 
 - WinLin210.09.2021 06:47- В именах файлов и каталов даты желательно использовать дату в формате yyyy-mm-dd для последующей сортировки и удобного хранения. Лучше не использовать месяц из одной цифры или импортозамещенное наименование с 'dd.mm.yyyy'. 
 - aatelnov10.09.2021 08:23- Было записано 1 089 044 строчек в таблицу. - Примерно половина из имеющихся данных (их там 2 174 433)  - nasingfaund Автор10.09.2021 08:30- Было записано 1 089 044 строчек в таблицу.- Примерно половина из имеющихся данных (их там 2 174 433)- Ну вот видите, какое прекрасное учебное задание разобраться, почему не удалось все записать, где кроется ошибка, как ее решить правильно.  - nasingfaund Автор10.10.2021 17:09- Сорри, все норм, тоже 2 174 433 получил. Сейчас проверю на двойников. 
 
  - nasingfaund Автор10.10.2021 17:07- Как считали, дайте, плиз код, пока у меня получается 2 174 423. 10 строчек не достает, ищу. На двойники не проверяли? 
 
 - danilovmy10.09.2021 09:23- Привет. Задание Можно доделать: - Консоль sqlite3 импортирует CSV. - Стоит не клеить CSV а по-файлово выгружать в базу и отслеживать вывод ошибок. Изучение отслеживания вывода предупреждений. - Можно сделать через Fabric - изучение модуля автоматизации процессов. - Ну и конечно же можно сделать через питоновские читалки xls/xlsx и научиться работать, например, с датафреймами пандас. - Норм пример для обучения, если у вас есть потом фидбек от ментора.  - nasingfaund Автор10.09.2021 10:28- Благодарствую за предложения по развитию учебно-велосипедного проекта. Подумаем, что и как можно сделать. Пока основная цель - решить все текущие ошибки, потренироваться с парсингом других открытых и общедоступных баз ФНС, Росстата. Сделать MVP с GUI и поиском. 
 
 
           
 
nasingfaund Автор
Ссылка на гитхаб битая, рабочая вот. Сейчас займемся выкладкой кода в гитхаб.
dabrahabra
Подскажите, это реальная задача (фриланс или внутренние нужды) или просто синтетическое задание?
nasingfaund Автор
Вообще это попытка потренироваться-написать для себя локальный поисковик по юридическим лицам РФ, просто не удовлетворяет то, что есть бесплатного-платного по этому направлению по России. Не удовлетворяет соотношение цена-качество, количество ошибок, их неумение решать востребованные бизнес-задачи. Но это глобальная так сказать задача. А пока получилась первая часть тренировки на "зайчиках". Был опыт публикации похожего поисковика онлайнового по Казахстану (все ЮЛ 0,3 млн) и России (не все ЮЛ, примерно 2,5млн). Но код писали другие люди.