Большинство разработчиков рано или поздно сталкиваются с XML. Этот язык разметки настолько глубоко вошел в нашу жизнь, что сложно представить систему, в которой не используется он сам или его подмножества. Разбор XML - достаточно типовая задача, но даже в ней можно выделить несколько основных подходов. В этой статье мы хотим рассказать, зачем нам потребовалось парсить XML, какие подходы мы опробовали, а заодно продемонстрировать замеры производительности для самых популярных реализаций на C++.

О нас

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

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

Еще на заре разработки мы поняли, что пользователям нужны одни и те же операции с файлами:

  • Просмотр - для отображения в браузере.

  • Печать - для любителей бумажных носителей данных.

  • Редактирование - для внесения изменений в удобной форме.

  • Проверка - для контроля данных на соответствие требованиям.

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

Так родилась идея создать систему, которая будет предоставлять механизмы обработки формализованных электронных документов.

Такие разные взгляды пользователя на обычную счет-фактуру
Такие разные взгляды пользователя на обычную счет-фактуру

Как было сказано выше, все начинается с описания.

Спецификацией мы называем описание конкретных форматов документов (НД по НДС, счет-фактура и т.д.) в нашей системе. Спецификация состоит из следующих компонентов:

  • Структура документа.

  • Печатная форма (при необходимости).

  • Правила проверки (при необходимости).

  • Форма редактирования (при необходимости).

Формализованный документ - документ, имеющий спецификацию определенного формата, что позволяет обрабатывать его автоматически.

Настройка формата документа XML. Просто и со вкусом.
Настройка формата документа XML. Просто и со вкусом.

Формализовать можно не только XML. Подобным образом могут быть строго описаны и другие форматы файлов (JSON, TXT и т.д.). Но в данной статье мы сосредоточимся на парсерах XML и истории их применения в наших продуктах.

История обработки XML в Saby

Общая схема обработки документов

Любая операция с документом имеет свои особенности. Но если отбросить все обертки, кэширование и прочую специфику, то все операции можно свести к следующим этапам:

  1. Получение файла

  2. Определение типа документа (НДС перед нами или счет-фактура)

  3. Получение спецификации

  4. Парсинг файла и прикладная обработка

  5. Формирование ответа

Основное время приходится именно на парсинг файла и прикладную обработку.

Этап 1. Начало.

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

  • Произвольный доступ к элементам XML-дерева.

  • Достаточно быстрая обработка всего документа.

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

В качестве основной библиотеки остановились на Xerces-C, предоставляющей наиболее полную поддержку стандартных API для работы с XML. Рассматривали также PugiXml, но решили, что не готовы жертвовать функционалом ради скорости обработки.

Сравнение производительности DOM-парсеров
Время
Время
Скорость парсинга
Скорость парсинга
Выделение памяти
Выделение памяти

Ох, сколько же полезного функционала мы тогда реализовали:

  • Проверка документов с помощью XPath-выражений и собственного мнемоязыка

  • Упрощенное API работы с DOM-деревом для прикладного кода

  • API редактирования документов.

  • И многое-многое другое.

Если бы мы только знали, что нас ждет..

Этап 2. Поиск альтернатив.

В какой-то момент к нам пришли обеспокоенные разработчики из соседнего отдела. Налоговая запросила новый тип отчетов (Уведомление о контролируемых сделках), и наша обработка стала падать на некоторых документах, которые мы получали для тестирования. Оказалось, что пользователи стали загружать отчеты в сотни мегабайт. А они превращались в гигабайты оперативной памяти при обработке (см. графики выше). И несколько таких документов, обрабатываемых одновременно, роняли сервер!

Оперативного решения у нас не было, поэтому прикладникам пришлось сделать быструю склейку/расклейку таких отчетов на небольшие документы на своей стороне (за что им огромное спасибо). А мы ушли искать альтернативное решение, которое бы всех устроило. Наткнулись на библиотеку VTD-XML, которая по предварительным тестам неплохо подходила нам для проверки документов (произвольный доступ, поддерживает XPath, малые затраты по памяти), но лицензия GPL не позволяла использовать данное решение в наших продуктах.

Если сравнивать VTD с разбором шкафа, то ты оставил по шкафу записки, и дальше быстро ориентируешься в вещах с помощью этих записок.

Сравнение производительности VTD и DOM-парсеров
Время
Время
Скорость парсинга
Скорость парсинга
Выделение памяти
Выделение памяти

Так мы пришли к очевидному, хоть и не столь удобному для нас решению.

Этап 3. Обработка на высокой скорости.

Парсеры с последовательным доступом (событийные и потоковые) позволяют работать исключительно с текущим элементом XML-документа. Скорость обработки документов у них примерно одинаковая, при этом они не проседают по памяти в процессе парсинга. В своем решении мы решили использовать событийный SAX-парсер. Из хорошего - в Xerces-C он уже был, поэтому нам не пришлось подключать новые библиотеки. Из плохого было все остальное - ни один наш механизм не был заточен под последовательное чтение данных.

Если сравнить подобные парсеры с разбором шкафа, то ты его в первый раз видишь, и проходишь последовательно, полка за полкой, не зная, сколько же еще осталось.

Сравнение производительности DOM и парсеров с последовательным доступом
Время
Время
Скорость парсинга
Скорость парсинга
Выделение памяти
Выделение памяти

Отложив на несколько месяцев всю прочую разработку, мы занялись переводом на SAX-парсер всех существующих механизмов. Наши цели были максимально просты:

  • Обрабатывать документы любого размера.

  • По возможности - поддержать весь существующий функционал, чтобы не переписывать уже разработанные спецификации форматов документов и не переучивать прикладников.

И надо сказать, что у нас получилось!

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

Где-то мы стали сами "на лету" разбивать файл и выполнять операции с меньшими документами. Где-то отказались от хранения XML вовсе. Где-то - по-прежнему обрабатываем документ целиком, работая в памяти только с нужными нам данными в процессе выполнения операций.

Мы даже частично поддержали функционал XPath-выражений в рамках потокового чтения SAX'ом, но это уже материал для отдельной статьи...

Методика измерений

Выше мы привели графики сравнения производительности различных парсеров, но не рассказали о методике измерений. Сравнение парсеров производились на одинаковых файлах. Размер файла изменялся от 1 до 1000 Мб. Сравнение производилось по следующим метрикам:

  1. Время работы

  2. Пиковое потребление оперативной памяти

  3. Скорость парсинга

При помощи псевдокода эксперимент можно представить следующим образом:

for file_size in <размер файла от 1 до 1000 с шагом 25>:
    # Генерируем XML-файл
    for parser in <список парсеров>:
        # Запускаем парсер
        # Измеряем время работы и потребляемую память
        # Записываем результаты

В качестве полезной нагрузки для парсера мы выбрали задачу: посчитать количество узлов и атрибутов в XML-файле. Такая постановка задачи требует прохода по всему файлу и ставит все парсеры в одинаковые условия.

Характеристики тестовой среды
Модель ноутбука: MacBook Air M1 2020
Процессор: Apple M1
OS: 13.4.1 (22F82)
RAM: 16 Gb
SSD: Apple SSD AP0256Q

В сравнении принимали участие следующие библиотеки:

  • xerces-c 3.2.3 (DOM/SAX)

  • libxml2 2.9.13 (DOM/SAX)

  • pugixml 1.12.1

  • expat 2.4.7

  • rapidxml 1.13

  • vtd-xml 2.12

Исходный код парсинга

Ниже выложен код наших программ на C++.

expat_sax
#include <iostream>
#include <expat.h>

struct XMLData
{
    int nodeCount;
    int attributeCount;
};

void startElement(void* data, const XML_Char* element, const XML_Char** attribute)
{
    XMLData* xmlData = static_cast<XMLData*>(data);
    xmlData->nodeCount++;

    for (int i = 0; attribute[i]; i += 2)
    {
        xmlData->attributeCount++;
    }
}

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    // Открытие файла
    FILE* file = fopen(args[1], "r");
    if (!file)
    {
        std::cout << "Failed to open file: " << args[1] << std::endl;
        return 1;
    }

    // Создание парсера Expat
    const auto& parser = XML_ParserCreate(NULL);
    if (!parser)
    {
        std::cout << "Failed to create XML parser" << std::endl;
        return 1;
    }

    XMLData xml_data;
    xml_data.nodeCount = 0;
    xml_data.attributeCount = 0;
    XML_SetUserData(parser, &xml_data);

    // Установка обработчика начала элемента
    XML_SetElementHandler(parser, startElement, nullptr);
    
    char buffer[4096];
    int bytes_read;

    // Чтение и парсинг XML-файла
    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0)
    {
        if (XML_Parse(parser, buffer, bytes_read, feof(file)) == XML_STATUS_ERROR)
        {
            std::cout << "XML parsing error" << std::endl;
            return 1;
        }
    }

    // Вывод результатов
    std::cout << xml_data.nodeCount << std::endl;
    std::cout << xml_data.attributeCount << std::endl;

    // Освобождение ресурсов
    XML_ParserFree(parser);
    fclose(file);

    return 0;
}

libxml2_dom
#include <iostream>
#include <libxml/parser.h>
#include <libxml/tree.h>

// Рекурсивная функция для подсчета узлов и атрибутов
void countNodesAndAttributes(xmlNode* node, int& nodes_count, int& attributes_count)
{
    if (node->type == XML_ELEMENT_NODE)
    {
        nodes_count++;
        xmlAttr* attribute = node->properties;
        while(attribute)
        {
            attributes_count++;
            attribute = attribute->next;
        }
    }

    // Рекурсивный вызов для каждого потомка узла
    for (xmlNode* child = node->children; child != nullptr; child = child->next)
    {
        countNodesAndAttributes(child, nodes_count, attributes_count);
    }
}


int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    const char *filename = args[1];
    // Открытие XML файла
    xmlDoc* doc = xmlReadFile(filename, nullptr, 0);
    if (doc == nullptr)
    {
        std::cout << "Failed to parse xml file." << std::endl;
        return 1;
    }

    xmlNode* root = xmlDocGetRootElement(doc);

    int num_nodes = 0;
    int attributes_count = 0;

    // Вызов рекурсивной функции для подсчета узлов и атрибутов
    countNodesAndAttributes(root, num_nodes, attributes_count);

    std::cout << num_nodes << std::endl;
    std::cout << attributes_count << std::endl;

    // Освобождение ресурсов
    xmlFreeDoc(doc);
    xmlCleanupParser();

    return 0;
}

libxml2_sax
#include <iostream>
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/parserInternals.h>

// Структура для хранения информации о количестве узлов и атрибутов
struct CountData
{
    int nodeCount = 0;
    int attributeCount = 0;
};

void startElementCallback(void *user_data, const xmlChar *name, const xmlChar **attrs)
{
    CountData *countData = static_cast<CountData *>(user_data);
    countData->nodeCount++;
    while (NULL != attrs && NULL != attrs[0])
    {
        countData->attributeCount++;
        attrs = &attrs[2];
    }
}

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    xmlSAXHandler sh = {0};

    CountData countData;

    // Регистрация функции обратного вызова
    sh.startElement = startElementCallback;

    xmlParserCtxtPtr ctxt;

    // Создание контекста
    if ((ctxt = xmlCreateFileParserCtxt(args[1])) == NULL)
    {
        std::cout << "Failed to create XML parser context." << std::endl;
        return EXIT_FAILURE;
    }

    ctxt->sax = &sh;
    ctxt->userData = &countData;

    // Парсинг документа
    xmlParseDocument(ctxt);
    std::cout << countData.nodeCount << std::endl;
    std::cout << countData.attributeCount << std::endl;

    return 0;
}

pugixml_dom
#include <iostream>
#include <pugixml.hpp>

// Функция для рекурсивного подсчета количества узлов и атрибутов
void countNodesAndAttributes(const pugi::xml_node& node, int& node_count, int& attribute_count)
{
    // Увеличение счетчика узлов
    node_count++;

    const auto& attrs = node.attributes();

    // Подсчет атрибутов
    attribute_count += std::distance(attrs.begin(), attrs.end());

    // Рекурсивный вызов для дочерних узлов
    for (const auto& child : node.children())
    {
        if (child.type() == pugi::node_element)
        {
            countNodesAndAttributes(child, node_count, attribute_count);
        }
    }
};

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }
    
    pugi::xml_document doc;
    const auto& result = doc.load_file(args[1]);
    
    if (!result)
        return 1;
    
    // Получение корневого узла
    const auto& root = doc.first_child();

    int node_count = 0;
    int attribute_count = 0;
    // Вызов функции для корневого узла
    countNodesAndAttributes(root, node_count, attribute_count);
    
    std::cout << node_count << std::endl;
    std::cout << attribute_count << std::endl;
    
    return 0;
}

rapidxml_dom
#include <iostream> // std::cout
#include "rapidxml/rapidxml.hpp" // rapidxml::xml_document, rapidxml::xml_node
#include "rapidxml/rapidxml_utils.hpp" // rapidxml::count_attributes, rapidxml::file

// Рекурсивная функция для подсчета узлов и атрибутов
void countNodesAndAttributes(rapidxml::xml_node<>* node, int& nodes_count, int& attributes_count)
{
    ++nodes_count;
    attributes_count += rapidxml::count_attributes(node);
    for (rapidxml::xml_node<>* child = node->first_node(); child; child = child->next_sibling())
    {
        if (child->type() == rapidxml::node_element)
        {
            countNodesAndAttributes(child, nodes_count, attributes_count);
        }
    }
}

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    // Читаем файл
    rapidxml::file<> xmlFile{args[1]};

    // Парсим XML-документ
    rapidxml::xml_document<> doc;
    doc.parse<0>(xmlFile.data());

    int node_count = 0;
    int attribute_count = 0;
    countNodesAndAttributes(doc.first_node(), node_count, attribute_count);

    std::cout << node_count << std::endl;
    std::cout << attribute_count << std::endl;

    return 0;
}

vtd-xml
#include <iostream>
#include <fstream>
#include "vtd-xml/vtdNav.h"
#include "vtd-xml/vtdGen.h"
#include "vtd-xml/autoPilot.h"

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    try
    {
        // Открываем XML-файл
        std::ifstream xml_file(args[1]);
        if (!xml_file.is_open())
        {
            std::cout << "Failed to open file." << std::endl;
            return 1;
        }
        
        // Определяем размер файла
        xml_file.seekg(0, std::ios::end);
        int file_size = xml_file.tellg();
        xml_file.seekg(0, std::ios::beg);
        
        // Читаем содержимое файла
        char* xml_data = new char[file_size];
        xml_file.read(xml_data, file_size);
        xml_file.close();
        
        // Инициализируем VTD-XML
        com_ximpleware::VTDGen vg;
        vg.setDoc(xml_data, file_size);
        vg.parse(true);
        
        // Создаем VTD-навигатор
        const auto& vn = vg.getNav();
        com_ximpleware::AutoPilot ap(vn);
        ap.selectXPath((com_ximpleware::UCSChar*) L"//*"); // Получаем все узлы
        
        // Подсчет узлов
        int node_count = 0;
        int attr_count = 0;
        while(ap.evalXPath() != -1)
        {
            node_count++;
            attr_count += vn->getAttrCount();
        }
        
        std::cout << node_count << std::endl;
        std::cout << attr_count << std::endl;
        
        // Освобождаем ресурсы
        delete[] xml_data;
    }
    catch (com_ximpleware::VTDException& e)
    {
        std::cerr << e.what() << ":" << e.getMessage() << endl;
    }
    
    return 0;
}

xerces-c_dom
#include <iostream>
#include <xercesc/util/PlatformUtils.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/dom/DOM.hpp>

// Рекурсивная функция обхода DOM-дерева для подсчета узлов и атрибутов
void countNodesAndAttributes(const xercesc::DOMNode* node, int& numNodes, int& numAttributes)
{
    if (node->getNodeType() == xercesc::DOMNode::ELEMENT_NODE)
    {
        // Подсчет узлов
        numNodes++;

        // Получение атрибутов текущего узла
        const auto& attributes = node->getAttributes();
        if (attributes)
        {
            // Подсчет атрибутов
            numAttributes += attributes->getLength();
        }
    }

    // Рекурсивный обход дочерних узлов
    for (const xercesc::DOMNode* child = node->getFirstChild(); child != nullptr; child = child->getNextSibling())
    {
        countNodesAndAttributes(child, numNodes, numAttributes);
    }
}

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    // Инициализация Xerces-C++
    xercesc::XMLPlatformUtils::Initialize();

    {
        // Создание парсера
        xercesc::XercesDOMParser parser;
        parser.setDoNamespaces(false);
        parser.setDoSchema(false);
        parser.setValidationScheme(xercesc::XercesDOMParser::Val_Never);

        try
        {
            // Парсинг файла
            parser.parse(args[1]);

            // Получение корневого элемента документа
            const xercesc::DOMDocument* doc = parser.getDocument();
            const xercesc::DOMElement* root = doc->getDocumentElement();

            // Переменные для подсчета узлов и атрибутов
            int numNodes = 0;
            int numAttributes = 0;

            // Вызов рекурсивной функции обхода дерева
            countNodesAndAttributes(root, numNodes, numAttributes);

            // Вывод результатов
            std::cout << numNodes << std::endl;
            std::cout << numAttributes << std::endl;
        }
        catch (...)
        {
            std::cerr << "Parsing error." << std::endl;
            return 1;
        }

    }

    // Освобождение ресурсов и завершение программы
    xercesc::XMLPlatformUtils::Terminate();
    return 0;
}

xerces-c_sax
#include <iostream>

#include <xercesc/parsers/SAXParser.hpp>
#include <xercesc/sax/HandlerBase.hpp>
#include <xercesc/util/XMLString.hpp>
#include <xercesc/sax/AttributeList.hpp>
#include <xercesc/sax/SAXParseException.hpp>
#include <xercesc/sax/SAXException.hpp>

using namespace xercesc_3_2;

class CounterSaxHandler : public HandlerBase
{
public:
    void startElement(const XMLCh* const, AttributeList& attributes)
    {
        ++m_nodesCount;
        m_attributesCount += attributes.getLength();
    }

    size_t NodesCount()
    {
        return m_nodesCount;
    }

    size_t AttributesCount()
    {
        return m_attributesCount;
    }
private:
    size_t m_nodesCount = 0;
    size_t m_attributesCount = 0;
};

int main(int argc, char *args[])
{
    // Проверка наличия аргумента командной строки
    if (argc < 2)
    {
        std::cerr << "You must pass the filename as an argument." << std::endl;
        return 2;
    }

    try
    {
        XMLPlatformUtils::Initialize();
    }
    catch (const XMLException& e)
    {
        char* message = XMLString::transcode(e.getMessage());
        std::cout << "Error during initialization! : " << message << std::endl;
        XMLString::release(&message);
        return 1;
    }
  
    SAXParser parser;
    parser.setDoNamespaces(true);    // optional

    CounterSaxHandler handler;
    parser.setDocumentHandler(&handler);
    parser.setErrorHandler(&handler);

    try
    {
        parser.parse(args[1]);
        std::cout << handler.NodesCount() << std::endl;
        std::cout << handler.AttributesCount() << std::endl;
    }
    catch (const XMLException& e)
    {
        char* message = XMLString::transcode(e.getMessage());
        std::cout << "Exception message is: " << message << std::endl;
        XMLString::release(&message);
        return -1;
    }
    catch (const SAXParseException& toCatch)
    {
        char* message = XMLString::transcode(toCatch.getMessage());
        std::cout << "Exception message is: " << message << std::endl;
        XMLString::release(&message);
        return -1;
    }
    catch (...)
    {
        std::cout << "Unexpected Exception." << std::endl;
        return -1;
    }

    return 0;
}

Генератор тестовых XML-файлов

Для генерации тестовых файлов мы написали скрипт на Python.

В качестве параметров задаются:

  • Имя файла (параметр --file_name)

  • Размер файла в мегабайтах (параметр --file_size)

  • Высота дерева (константа HEIGHT)

  • Длина названий узлов и атрибутов (константы NODE_NAME_LENGTH и ATTRIBUTE_NAME_LENGTH соответственно)

  • Количество атрибутов в узле (константа NUM_ATTRIBUTES)

  • Количество атрибутов в самом глубоком узле (константа DEEPEST_LEVEL_SIZE)

Имена узлов и атрибутов генерируются случайным образом.

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

Код генератора
"""Генератор тестовых XML-файлов"""
import argparse
import os
import random
import string
import xml.etree.ElementTree as ET

# Высота дерева
HEIGHT = 5
# Длины названий узлов и атрибутов
NODE_NAME_LENGTH = 5
ATTRIBUTE_NAME_LENGTH = 3
# Количество атрибутов в узле
NUM_ATTRIBUTES = 2
# Количество атрибутов в самом глубоком узле
DEEPEST_LEVEL_SIZE = 10


def random_string(length):
    """Генерация случайной строки заданной длины"""
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for _ in range(length))


def generate_xml_tree(height, node_name_length, attribute_name_length, num_attributes):
    """Генерация XML-дерева"""
    if height <= 0:
        return None

    root = ET.Element(random_string(node_name_length))

    for _ in range(num_attributes):
        attr_name = random_string(attribute_name_length)
        attr_value = random_string(attribute_name_length)
        root.set(attr_name, attr_value)

    level_size = 1
    if height == 2:
        level_size = DEEPEST_LEVEL_SIZE
    for _ in range(level_size):
        child = generate_xml_tree(height - 1, node_name_length, attribute_name_length, num_attributes)
        if child is not None:
            root.append(child)

    return root


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Fake XML generator')
    # Опциональные аргументы
    parser.add_argument('--file_size', type=int, default=1, help='Size of file in megabytes')
    parser.add_argument('--file_name', type=str, default='output.xml', help='File name')
    args = parser.parse_args()

    args.file_size = args.file_size * 1024 * 1024

    # Генерация шаблона xml_str
    tree = generate_xml_tree(HEIGHT, NODE_NAME_LENGTH, ATTRIBUTE_NAME_LENGTH, NUM_ATTRIBUTES)
    xml_tree = ET.ElementTree(tree)
    ET.indent(xml_tree)

    xml_str = ET.tostring(tree, encoding='utf8', xml_declaration=False).decode()

    # Сохранение XML-документа в файл
    with open(args.file_name, 'w', encoding='utf8') as f:
        f.write('<?xml version="1.0"?>')
        f.write(os.linesep)
        f.write('<root>')
        while os.path.getsize(args.file_name) < args.file_size:
            f.write(xml_str)
            f.write(os.linesep)
            f.flush()
        f.write('</root>')

Функциональное сравнение парсеров

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

DOM и VTD

libxml2

pugixml

rapidxml

vtd-xml

xerces-c

Поддержка пространств имен

+

-

-

только чтение

+

Поддержка processing instruction

+

+

только чтение

только чтение

+

Поддержка кодировок

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, можно добавлять свои

UTF-8, UTF-16 (LE/BE), UTF-32 (LE/BE), ISO-8859-1, ASCII, можно добавлять свои

UTF-8

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, ISO--8859-{2-10}, Windows {1250-1258}

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, UCS4BE/LE, IBM037, IBM1047, IBM1140, Windows-1252, можно добавлять свои

Поддержка XPath

+

+

-

+

+

Потоковые парсеры

Expat, Xerces-C и libxml2 предлагают схожий функционал, отличаясь лишь набором кодировок, которые они поддерживают по умолчанию.

libxml2

expat

xerces-c

Поддержка пространств имен

+

+

+

Поддержка processing instruction

+

+

+

Поддержка кодировок

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, можно добавлять свои

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, можно добавлять свои

UTF-8, UTF-16 (LE/BE), ISO-8859-1, ASCII, UCS4BE/LE, IBM037, IBM1047, IBM1140, Windows-1252, можно добавлять свои

Поддержка XPath

-

-

-

Выводы

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

Мы же в процессе изменения кодовой базы пришли к достаточно очевидным выводам относительно общих подходов к проектированию и разработке:

  • Используйте технологии, которые отвечают вашим текущим требованиям. Но помните, что требования могут меняться, и это стоит учитывать при проектировании архитектуры.

  • Чаще получайте обратную связь от заказчиков и обкатывайте свои решения на реальных данных.

  • Заботьтесь о безопасном внесении изменений. Пишите тесты и применяйте подходящие паттерны проектирования.

  • Относитесь критично к существующим решениям. В процессе внесения изменений мы нашли несколько древних ошибок в legacy-коде, о которых даже не задумывались.

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


  1. WondeRu
    26.10.2023 10:49

    но лицензия GPL не позволяла использовать данное решение в наших продуктах.

    Мы проговаривали с юристами, если ваш продукт не форк GPL-продукта, а лишь использует, то делаете два инсталлятора: один для чистых лицензий и вашего кода, а второй - для gpl-библиотек. При установке запускаете два отдельных инсталлятора. Если микросервисная архитектура, то gpl-компоненты обертываете в сервисы. Если у вас один контейнер с приложением и gpl-компонентами, то собираете контейнеры из двух инсталляторов в прод среде. Все это нужно не для удобства разработчиков, а для того, чтобы не показывать весь код, когда запросят интересующиеся. #подстелитьсоломку #подорожник


    1. alexac
      26.10.2023 10:49
      +1

      Линковаться с GPL библиотекой нельзя, не публикуя свой код под GPL. Даже если не распространяете GPL-код вместе с продуктом. Как раз распространять код вместе GPL не запрещает. Линковка же весьма спорный момент, но по факту большинство разночтений сводятся к тому, что линковка к библиотеке подразумевает включение ее логики в программу, и тогда вся программа должна быть опубликована под GPL. Есть LGPL, которая разрешает линковку к LGPL библиотеке без публикации кода остальной программы. А еще есть AGPL - самая лютая хрень, с ней даже обернуть в отдельный сервис, который доступен по сети, нельзя, не выкладывая весь свой код под AGPL.

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


      1. LightKitten Автор
        26.10.2023 10:49

        Согласен.

        А заморочки с VTD будут не только с лицензией, но и с кодом.


  1. SpiderEkb
    26.10.2023 10:49

    Приходилось работать и с DOM и с SAX.

    Для себя критерий такой - если документ невелик (и помещается в памяти) и с ним нужно работать как с документом (открыл - отредеактировал - сохранил - закрыл) - DOM.

    Если нужно просто получить какие-то данные, которые приходят в виде XML, которые нужно как-то обработать (в настоящее время у нас это приходящий откуда-то извне документ, из которого нужно извлечь интересующие нас данные и разложить их по нашей БД) - SAX без вопросов.

    Если сравнивать со шкафами:

    DOM - сначала все вещи выкладывает из шкафа и раскладываете в нужном порядке на полу. Потом уже делаете с ними то, что нужно. Можете потом, если надо, сложить обратно в шкаф.

    SAX - берете вещь из шкафа, если нужная - используете или кладете в стопочку. Ненужные сразу выкидываете в окно. Сложить все обратно в шкаф будет уже отдельной задачей.

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

    Есть еще более интегрированная XML-INTO которая сразу раскладывает содержимое XML в структуру, но оно работает только с очень простыми XML, "завести" ее для реального для нас XML документа мне не удалось...

    С DOM работал давно и не очень много (когда-то была нужда в работе с gpx файлами - это XML, содержащий GPS данные). Не помню уж что тогда использовал, вроде бы TinyXML - для тех задач хватало, глубже не копал.


    1. igorts
      26.10.2023 10:49

      по аналогии с БД - таблица маленькая, тогда fullscan (построение полной модели), большая - только индексы


    1. gev
      26.10.2023 10:49

      А еще есть StAX!


      1. LightKitten Автор
        26.10.2023 10:49

        StAX - как раз про "взять следующую вещь из шкафа". А вот SAX - это когда вещи из шкафа в тебя летят сами.


        1. gev
          26.10.2023 10:49

          StAX – это про попросить следующую вещь из шкафа, и если дали, а она не подошла, то выкинуть в форточку сразу, не вдаваясь в подробности что там "у ней унутри", а дальше решить просить ли следующую =). То есть по ресурсам получаем меньше чем у SAX, и за меньшее количество приседаний, не получая и не обрабатывая все события: "начало" вещи, "начало" подвещи, "конец" подвещи, "конец" вещи и т. п.


          1. strvv
            26.10.2023 10:49

            я когда лет 10 назад пробовал разбирать в лоб MS OOXML да OpenDocument в виде приложения, в итоге с OOXML так и "не шмогла"...

            сейчас деталей не помню, надо на github.com поднимать код. qt4 тогда использовал. недавно просто пересобрал на qt5, для интереса, но логику не разбирал. давно уже не программирую. хотя есть задачи, которые хотелось бы автоматизировать, но это и sql и qt и дофига всего, а главное - время где найти 8-0 !!!

            насколько помню - парсил последовательно, искал нужные "переменные" да "таблицы".

            и с таблицами в OOXML было тяжко.


    1. atd
      26.10.2023 10:49
      +1

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