{"id":14291,"url":"\/distributions\/14291\/click?bit=1&hash=257d5375fbb462be671b713a7a4184bd5d4f9c6ce46e0d204104db0e88eadadd","hash":"257d5375fbb462be671b713a7a4184bd5d4f9c6ce46e0d204104db0e88eadadd","title":"\u0420\u0435\u043a\u043b\u0430\u043c\u0430 \u043d\u0430 Ozon \u0434\u043b\u044f \u0442\u0435\u0445, \u043a\u0442\u043e \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043c \u043d\u0435 \u043f\u0440\u043e\u0434\u0430\u0451\u0442","buttonText":"","imageUuid":""}

Универсальный загрузчик XML на java. Или как загрузить файлы ГАР на 250 гб и остаться при памяти

С проблемой загрузки больших XML столкнулся при переходе с КЛАДР и ФИАС на справочники ГАР — Государственный адресный реестр (Федеральная информационная адресная система).

Справочник ГАР содержит более подробную информацию чем предыдущие классификаторы. В том числе информацию по муниципальным делениям. В связи с чем справочник после распаковки занимет около 250 ГБ, что примерно в 3 раза больше чем тот же ФИАС.

Предыдущая загрузка работала на DOM-модели, т. е. весь XML-файл считывался в память. Соответственно при попытке загрузить ГАР таким же способом стали стабильно получать OutOfMemory. А значит настало время менять подход к загрузке)

Немного теории:

DOM (Document Object Model) — это стандартный интерфейс для работы с документами в формате XML (Extensible Markup Language). DOM-модель представляет XML-документ в виде дерева объектов, где каждый элемент и атрибут документа является узлом дерева.

SAX (Simple API for XML) является событийно-ориентированным API для чтения XML-документа. Он предоставляет возможность читать XML-документ последовательно и обрабатывать события, такие как начало и конец элемента, содержимое элемента и т. д.

StAX (Streaming API for XML) также является API для последовательного чтения и записи XML-документов. Он предоставляет потоковый доступ к XML-документу, позволяя читать его и записывать по частям. StAX предоставляет возможность читать и записывать XML-документы в виде потока событий, аналогично SAX, но также предоставляет возможность читать и записывать XML-документы в виде итерируемых наборов событий. StAX позволяет эффективно обрабатывать большие XML-документы и не требует реализации обработчиков событий.

Другими словами:

Загрузка XML-документа с помощью DOM-модели довольно медленная, особенно для больших документов, поскольку требует создания полной структуры DOM в памяти. Однако, DOM-модель позволяет легко и удобно работать с XML-документами, поэтому она широко используется в Java-приложениях.

SAX и StAX позволяет обрабатывать XML-документы любого размера, поскольку он не хранит всю структуру в памяти. Однако, для работы необходимо реализовать обработчики событий.

Одним из главных преимуществ использования StAX является его скорость работы и эффективность. И и отличие от DOM, который загружает весь XML-документ в память перед его обработкой, StAX-парсер обрабатывает XML-документ по одному элементу за раз, что позволяет работать с большими XML-файлами.

Понятно. Останавливаемся на StAX: )

Реализуем класс для универсальной загрузки XML. Будем читать данные комфортными порциями и мапить их на произвольные объекты. Класс объекта передаем в загрузчик.

public class XMLAttributeReader { private Logger logger = LoggerFactory.getLogger(XMLAttributeReader.class); private InputStream inputStream; private String attr; private XMLEventReader eventReader; private ObjectMapper mapper; private final Integer RECORDS_COUNT; private void configure() { mapper = new ObjectMapper(); mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try { XMLInputFactory factory = XMLInputFactory.newInstance(); eventReader = factory.createXMLEventReader(inputStream); } catch (XMLStreamException e) { logger.error(e.getMessage()); } } public void close(){ try { eventReader.close(); } catch (XMLStreamException e) { e.printStackTrace(); } } public XMLAttributeReader(InputStream inputStream, String attr, Integer RECORDS_COUNT) { this.inputStream = inputStream; this.attr = attr; this.RECORDS_COUNT = RECORDS_COUNT; configure(); } public Boolean hasNext() { return eventReader != null ? eventReader.hasNext() : false; } public <T> List<T> getNextPart(Class<T> valueType) { List<T> valueList = new ArrayList<>(); int count = 0; try { while (eventReader.hasNext() && count < RECORDS_COUNT) { XMLEvent event = eventReader.nextEvent(); switch (event.getEventType()) { case XMLStreamConstants.START_ELEMENT: StartElement startElement = event.asStartElement(); String qName = startElement.getName().getLocalPart(); if (qName.equalsIgnoreCase(attr)) { Map map = attributesToMap(startElement.getAttributes()); T value = mapper.convertValue(map, valueType); valueList.add(value); count++; } break; } } } catch (XMLStreamException e) { logger.error(e.getMessage()); } return valueList; } private static Map attributesToMap(Iterator<Attribute> attributes) { Map<String, String> map = new HashMap<>(); while (attributes.hasNext()) { Attribute attr = attributes.next(); map.put(attr.getName().toString(), attr.getValue()); } return map; } }

Структура справочника состоит из нескольких типов XML-файлов. Для каждой создадим таблицу в БД, опишим сущности. Пример для адресных объектов:

package com.example.XMLToBase.db.entity; import javax.persistence.*; import java.util.Date; //<OBJECT ID="1178934" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" // CHANGEID="2615278" // NAME="Ветеран квартал 6" // TYPENAME="снт" LEVEL="7" // OPERTYPEID="50" PREVID="1178870" // NEXTID="1909861" UPDATEDATE="2021-05-07" STARTDATE="2016-09-29" ENDDATE="2021-05-07" // ISACTUAL="0" ISACTIVE="0" /><OBJECT ID="1909471" OBJECTID="101148944" // OBJECTGUID="73104935-cc12-4bc7-b2d1-70c431aa7005" CHANGEID="192807935" // NAME="Южный" TYPENAME="пер" LEVEL="8" OPERTYPEID="10" PREVID="0" NEXTID="0" // UPDATEDATE="2021-05-05" STARTDATE="2021-05-05" ENDDATE="2079-06-06" ISACTUAL="1" // ISACTIVE="1" /><OBJECT ID="1909861" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" // CHANGEID="192832273" NAME="Ветеран квартал 6" TYPENAME="снт" LEVEL="7" OPERTYPEID="30" PREVID="1178934" NEXTID="0" // UPDATEDATE="2021-05-07" STARTDATE="2021-05-07" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="0" /></ADDRESSOBJECTS> @Data @Entity @Table(name = "gar_addressobject") public class GarAddressobject { @Id @Column(name = "id") private Long id; @Column(name = "objectid") private Long objectid; @Column(name = "objectguid") private String objectguid; @Column(name = "changeid") private Long changeid; @Column(name = "name") private String name; @Column(name = "typename") private String typename; @Column(name = "level") private String level; @Column(name = "opertypeid") private Long opertypeid; @Column(name = "previd") private Long previd; @Column(name = "nextid") private Long nextid; @Column(name = "updatedate") private Date updatedate; @Column(name = "startdate") private Date startdate; @Column(name = "enddate") private Date enddate; @Column(name = "isactual") private Long isactual; @Column(name = "isactive") private Long isactive; }

Читаем нашим XML загрузчиком адреса пачками в структуру GarAddressobject и тут же производим сохранение в БД.

private void processAddr(File file){ logger.info("Start loading " + file.getParent() + "/" + file.getName()); try (InputStream is = new FileInputStream(file)) { XMLAttributeReader xmlReader = new XMLAttributeReader(is, "OBJECT", RECORDS_PER_ITERATION); int i = 0; int j = 1; List<GarAddressobject> list; while (xmlReader.hasNext()) { list = xmlReader.getNextPart(GarAddressobject.class); garAddressobjectRepository.saveAll(list); i += Math.min(RECORDS_PER_ITERATION, list.size()); if ((i / RECORDS_PER_ITERATION) != j){ j = i / RECORDS_PER_ITERATION; logger.info("saved records: " + i); } list.clear(); } logger.info("saved records: " + i); xmlReader.close(); } catch (IOException e) { e.printStackTrace(); } }

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

В итоге имеем:

- Регламентная загрузка ГАР перестала падать изза нехватки памяти

- Можно управлять кол-вом строк, которые за раз вычитывает XML-загрузчик

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

- Понимаем отличия в подходах DOM и SAX. Знаем где какой вариант лучше подойдет: )

Всем спасибо! Комментарии приветствуются)

0
2 комментария
john volt

Зарегался здесь, чтобы сказать огромное спасибо автору!) Очень актуальная и полезная инфа для меня, задавался вопросом как работать с такими объёмами, но ещё не начал реализовывать и тут так кстати пришлась эта статья.

Надеюсь на чудо, что автор ответит, и спрошу, как в системе ГАР связываются данные в файлах между собой? Я никак не могу найти внятной документации. Есть дом, как он привязывается к улице? Как улица к населенному пункту и т.д.? Буду благодарен за ответ или за ссылку на доку, если она всё таки есть

Ответить
Развернуть ветку
Liashov Evgenii
Автор

Добрый день! Извините что долго не отвечал, только увидел комментарий)
К сожалению по связям таблиц ГАР у меня нет информации.
Задача стояла загрузить как есть - каждый XML в отдельную таблицу в БД. Дальше уже спецы по базам делают свою магию с этими данными)

Большое спасибо за положительный отзыв!)

Ответить
Развернуть ветку
-1 комментариев
Раскрывать всегда