Всем привет.

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

В двух словах для быстрого парсинга файлов надо пользоваться XMLReader в связке с yield.

О моей реализации этой связки читайте ниже.

XMLReader

XMLReader это единственный класс в PHP, который умеет читать файл по частям. В PHP есть SimpleXML, есть DOMDocument, но они работают с файлом только полностью считывая его с диска, только полностью загружая в оперативную память.

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

yield

Следующее что нам поможет это выражение yield.

Прочитали один элемент из файла, распарсили, вернули его с помощью yield, сформировали SQL команду insert, выполнили команду, читаем следующий элемент из файла, и так далее, до победного конца. Ни кто не простаивает, всё примерно одинаково загружено.

Теперь сложим всё вместе и получим FastXmlToArray.

FastXmlToArray

FastXmlToArray это класс со статическим методом prettyPrint(), на вход можно подать или ссылку на XML файл (путь к файлу) или собственно XML строку. На выходе будет PHP массив со всеми атрибутами и значениями корневого элемента и всех вложенных элементов.

$xml =<<<XML
<outer any_attrib="attribute value">
    <inner>element value</inner>
    <nested nested-attrib="nested attribute value">nested element value</nested>
</outer>
XML;
$result =
    \SbWereWolf\XmlNavigator\FastXmlToArray::prettyPrint($xml);
echo json_encode($result, JSON_PRETTY_PRINT);

Вывод в консоль

{
  "outer": {
    "@attributes": {
      "any_attrib": "attribute value"
    },
    "inner": "element value",
    "nested": {
      "@value": "nested element value",
      "@attributes": {
        "nested-attrib": "nested attribute value"
      }
    }
  }
}

Killer feature этого класса это статический метод extractElements(), который принимает XMLReader, а выдаёт всё тоже самое: массив со всеми атрибутами и значениями этого элемента и всех вложенных элементов.

    /**
     * @param XMLReader $reader
     * @param string $valueIndex index for element value
     * @param string $attributesIndex index for element attributes collection
     * @return array
     */
    public static function extractElements(
        XMLReader $reader,
        string $valueIndex = IFastXmlToArray::VALUE,
        string $attributesIndex = IFastXmlToArray::ATTRIBUTES,
    ): array;

Существенная разница, в том что prettyPrint() обрабатывает сразу корневой элемент XML документа (по сути весь файл), а extractElements() работает с произвольным элементом (часть файла), из XML документа можно передать любой элемент.

То есть не надо весь файл загружать в парсер, что бы этот файл распарсить, можно дочитать до нужного элемента, и уже только его парсить, обработать результат парсинга, и вычитать ещё часть файла, ещё один элемент, и снова обработать.

Такая потоковая обработка даёт более плавную загрузку сервиса парсинга и сервиса СУБД.

Пример использования

Будем парсить что-то такое:

<?xml version="1.0" encoding="utf-8"?>
<CARPLACES>
    <CARPLACE
            ID="11361653"
            OBJECTID="20326793"
    />
    <CARPLACE
            ID="94824"
            OBJECTID="101032823"
    />
</CARPLACES>

Допустим нас во всё документе интересуют только элементы CARPLACE.

Переведём XMLReader на первый элемент CARPLACE

$reader = XMLReader::XML($xml);
$mayRead = true;
while ($mayRead && $reader->name !== 'CARPLACE') {
    $mayRead = $reader->read();
}

Пройдёмся по всем элементам CARPLACE, пока не перейдём к элементу с другим именем или пока документ не кончиться

while ($mayRead && $reader->name === 'CARPLACE') {
    $elementsCollection = FastXmlToArray::extractElements(
        $reader,
    );
    $result = FastXmlToArray::createTheHierarchyOfElements(
        $elementsCollection,
    );
    echo json_encode([$result], JSON_PRETTY_PRINT);

    while (
        $mayRead &&
        $reader->nodeType !== XMLReader::ELEMENT
    ) {
        $mayRead = $reader->read();
    }
}

Что мы тут делаем ?

С помощью FastXmlToArray::extractElements() получаем список элементов со всеми значениями и атрибутами(в этом примере список всего из одной позиции)

$elementsCollection = FastXmlToArray::extractElements(
    $reader,
);

С помощью FastXmlToArray::createTheHierarchyOfElements() формируем иерархию элементов как они есть в исходном документе, иерархию будет в формате массива (в этом примере из одного элемента и его атрибутов).

С помощью FastXmlToArray::composePrettyPrintByXmlElements() можно сформировать более компактное представление, но работать с ним не так удобно.

$result = FastXmlToArray::createTheHierarchyOfElements(
    $elementsCollection,
);

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

echo json_encode([$result], JSON_PRETTY_PRINT);

Вместо echo можно, делать yield и отдавать массив на обработку наружу, я в своём парсере ФИАС ГАР так и делаю. Возвращаю через yield массив, и делаю insert в базу.

Проматываем "файл" до следующего элемента или до конца "файла"

while (
    $mayRead &&
    $reader->nodeType !== XMLReader::ELEMENT
) {
    $mayRead = $reader->read();
}

В консоли будет что-то такое:

[
    {
        "n": "CARPLACE",
        "a": {
            "ID": "11361653",
            "OBJECTID": "20326793"
        }
    }
][
    {
        "n": "CARPLACE",
        "a": {
            "ID": "94824",
            "OBJECTID": "101032823"
        }
    }
]

Другой пример

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<QueryResult
        xmlns="urn://x-artefacts-smev-gov-ru/services/service-adapter/types">
    <smevMetadata
            b="2">
        <MessageId
                c="re">c0f7b4bf-7453-11ed-8f6b-005056ac53b6
        </MessageId>
        <Sender>CUST01</Sender>
        <Recipient>RPRN01</Recipient>
    </smevMetadata>
    <Message
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:type="RequestMessageType">
        <RequestMetadata>
            <clientId>a0efcf22-b199-4e1c-984a-63fd59ed9345</clientId>
            <linkedGroupIdentity>
                <refClientId>a0efcf22-b199-4e1c-984a-63fd59ed9345</refClientId>
            </linkedGroupIdentity>
            <testMessage>false</testMessage>
        </RequestMetadata>
        <RequestContent>
            <content>
                <MessagePrimaryContent>
                    <ns:Query
                            xmlns:ns="urn://rpn.gov.ru/services/smev/cites/1.0.0"
                            xmlns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.2"
                    >
                        <ns:Search>
                            <ns:SearchNumber
                                    Number="22RU006228DV"/>
                        </ns:Search>
                    </ns:Query>
                </MessagePrimaryContent>
            </content>
        </RequestContent>
    </Message>
</QueryResult>

Пример запроса сведений, пришедший по СМЭВу, нас тут интересует только элемент ns:Query

Код будет аналогичным:

$mayRead = true;
$reader = XMLReader::XML($xml);
while ($mayRead && $reader->name !== 'ns:Query') {
    $mayRead = $reader->read();
}

while ($reader->name === 'ns:Query') {
    $elementsCollection = FastXmlToArray::extractElements(
        $reader,
    );
    $result = FastXmlToArray::createTheHierarchyOfElements(
        $elementsCollection,
    );

    echo json_encode([$result], JSON_PRETTY_PRINT);

    while (
        $mayRead &&
        $reader->nodeType !== XMLReader::ELEMENT
    ) {
        $mayRead = $reader->read();
    }
}
$reader->close();

Вывод в консоль:

[
    {
        "n": "ns:Query",
        "a": {
            "xmlns:ns": "urn:\/\/rpn.gov.ru\/services\/smev\/cites\/1.0.0",
            "xmlns": "urn:\/\/x-artefacts-smev-gov-ru\/services\/message-exchange\/types\/basic\/1.2"
        },
        "s": [
            {
                "n": "ns:Search",
                "s": [
                    {
                        "n": "ns:SearchNumber",
                        "a": {
                            "Number": "22RU006228DV"
                        }
                    }
                ]
            }
        ]
    }
]

На самом деле, конечно, нас интересует только <ns:SearchNumber Number="22RU006228DV"/>, и в коде продакшена было бы while ($reader->name === 'ns:SearchNumber'), ради наглядности я привёл получение кусочка побольше.

Замеры производительности

Прежде чем переписать свою библиотеку для работы с XML, я посмотрел что нам может предложить Open Source.

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

На самом деле время везде примерно одно и тоже. С той разницей, что функции работают быстрей статических методов, а статические методы работают быстрее методов экземпляра класса, а самые медленные это просто PHP скрипты.

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

Мои замеры быстродействия:

91 mcs 200 ns \Mtownsend\XmlToArray\XmlToArray::convert()
82 mcs 0 ns xmlstr_to_array()
139 mcs 600 ns getNextElement
95 mcs 700 ns \SbWereWolf\XmlNavigator\Converter->prettyPrint
105 mcs 200 ns \SbWereWolf\XmlNavigator\FastXmlToArray::prettyPrint
107 mcs 0 ns \SbWereWolf\XmlNavigator\Converter->xmlStructure
91 mcs 900 ns \SbWereWolf\XmlNavigator\FastXmlToArray::convert

От запуска к запуску числа отличаются, но общая картина примерно такая.

Мой use case заключался в том, что бы 280 гигабайт XML файлов превратить в базу данных Федеральной Информационной Адресной системы (БД ФИАС).

Надеюсь, теперь понятно почему меня волновало время парсинга XML файлов.

Что удивительно это то, что 280 гигабайт XML файлов превратить в 190 гигабайт базы данных, такое чувство что у СУБД нет ни какой оптимизации, если из XML выкинуть имена элементов и имена атрибутов и всю разметку, то что там останется ? На мой взгляд объём должен был сократиться в два раза, хотя бы, но нет. 190 гигабайт это без индексов, с индексами наверное все 220 будут.

В первых версиях парсер жрал по 4 гига оперативы, грузил БД на 25% процессора, сколько процесс PHP отъедал оперативки я уже не помню. Вставка 100 000 записей занимала от 2 минут, чтение файла могло затянуться на 10 минут.

Последняя версия парсера ровно каждые 9 секунд вставляет очередные 100 0000 записей, открытие файла занимает ноль секунд. PHP и БД каждый отъедают не больше 8 мегабайт оперативки и не больше 12% процессора. Но даже при этих условиях Добавление полной базы ГАР заняло 40 часов.

Rows was read is `1 571 374 861`
Import duration is 1 days, 15:56:02 855 ms

Полтора миллиарда записей за один день и 16 часов (24+16=40), 40 часов это очень долго, из оптимизаций в голову приходит только асинхронности добавить, что бы один поток парсил XML, а другой поток отправлял данные в СУБД.

Ваши предложения ?

Заключение

Если вам хочется попробовать этот парсер в деле, то вы можете установить пакет через Композер

composer require sbwerewolf/xml-navigator

Больше информации в репозитории в README.md

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


  1. Shado_vi
    23.01.2023 05:41
    -2

    регулярками или бинарным сопоставлением судя по отсутствию в голосовании в голову даже не приходило похоже. это было например 3 пунктом


    1. SbWereWolf Автор
      23.01.2023 08:15
      +1

      Парсить файл размером 4 гигабайта с помощью регулярок ? Не думаю что это будет быстрее.

      Такого пукта в голосовании нет, но есть пункт "сторонняя библиотека", пожалуйста.

      Не знаю, что имеется в виду под бинарным сопоставлением, но "бинарный" это кажется не уровень абстракции PHP. XMLReader использует libxml, вы предлагаете мне написать свой аналог ? Не думаю, что у меня получиться лучше чем у авторов libxml. Я точно не готов столько времени на это потратить ????


      1. a-tk
        23.01.2023 10:24
        +1

        Вы таки серьёзно XML размером 4 ГБ потом представляете в виде массива в памяти?


      1. Shado_vi
        23.01.2023 17:56
        -1

        просто опыт работы с не валидными большими xml.
        XMLReader так же как если использовать многие другие инструменты такое плохо прожёвывают.
        я например жалею что тогда мною было потрачено много времени на изучение и заставить библиотеки работать с не валидным xml.
        и обрабатывать я рассматриваю "Разбор XML как строки или массива байт" что не требует загрузки в память всего документа. при желании можно попробовать отправлять элементы документа на асинхронную обработку.


  1. sepetov
    23.01.2023 07:25

    Я парсил с помощью htmlSQL (она xml-файлы тоже кушает). Но на таких объёмах я её не тестировал и предполагаю, что от 290 Гб ей будет плохо :-)

    У неё под капотом чтение файла идёт через вызов file(), а эта функция читает файл целиком.

    Да и вообще, библиотека экспериментальная и очень старая.


    1. sepetov
      23.01.2023 09:21

      Провёл тест и библиотека htmlSQL, как и ожидалось, разгромно проиграла. Она не смогла осилить даже файл в 2.5 Гб. Так что ваша работа превосходна.

      Протестировал очень грубо. Для примера создал файл big.xml на 810 Мб, где было 10 000 000 идентичных фрагментов:

      <parent_node>
          <children>
              <test>007</test>
          </children>
      </parent_node>

      Затем разобрал его вот так:

      $wsql = new htmlsql();
      $wsql->connect('file', 'big.xml');
      $wsql->query('SELECT text FROM test');

      Этот запрос просто находит все "007" между тегами test.

      Расход памяти больше 10 Гб. Время выполнения около 13.5 с. Кошмар!


  1. ShefEr
    23.01.2023 09:26

    Опечатка, XMLReder -> XMLReader


    1. strelkan
      23.01.2023 13:57

      и ещё одна "а надо сказать, что XML файл парситься именно для того "
      https://tsya.ru/


    1. Krasnoarmeec
      23.01.2023 20:07
      +2

      Выделяете текст с ошибкой, нажимаете Ctrl+Enter и будет Вам и автору благодать:
      Автору не придётся краснеть, а Вам перепадёт общение с умным человеком. :-)


  1. SpiderEkb
    23.01.2023 09:35
    +4

    Кроме DOM, который строит в памяти все дерево XML документа и потом позволяет с ним работать ("ходить" по нему, изменять содержимое, сохранять обратно), есть еще более простой потоковый SAX. Который просто идет по XML и вызывает заранее заданный handler, передавая ему тип события и "содержимое". Например, для фрагмента

    <tag1>
      <tag2>
        Data
      </tag2>
    </tag1>  

    handler будет вызван 5 раз:

    handler(*XML_START_ELEMENT, "tag1")
    handler(*XML_START_ELEMENT, "tag2")
    handler(*XML_CHARS, "Data")
    handler(*XML_END_ELEMENT, "tag2")
    handler(*XML_END_ELEMENT, "tag1")

    Что со всем этим делать - это уже вы в handler'е пишете.

    Естественно, что тут нет никаких ограничений на размер обрабатываемого файла - он не грузится в память целиком. И естественно, все это работает быстро (скорость определяется исключительно скоростью работы handler'а).

    Оптимальный вариант для тех ситуаций, когда нужно просто вытащить данные из XML и, например, разложить их в БД.

    Естественно, что типов событий там достаточно много:

    Events discovered before the first XML element:

    • *XML_START_DOCUMENT - Indicates that parsing has begun

    • *XML_VERSION_INFO - The "version" value from the XML declaration

    • *XML_ENCODING_DECL - The "encoding" value from the XML declaration

    • *XML_STANDALONE_DECL - The "standalone" value from the XML declaration

    • *XML_DOCTYPE_DECL - The value of the Document Type Declaration

    Events related to XML elements

    • *XML_START_ELEMENT- The name of the XML element that is starting

    • *XML_CHARS - The value of the XML element

    • *XML_PREDEF_REF - The value of a predefined reference

    • *XML_UCS2_REF - The value of a UCS-2 reference

    • *XML_UNKNOWN_REF - The name of an unknown entity reference

    • *XML_END_ELEMENT - The name of the XML element that is ending

    Events related to XML attributes

    • *XML_ATTR_NAME - The name of the attribute

    • *XML_ATTR_CHARS - The value of the attribute

    • *XML_ATTR_PREDEF_REF - The value of a predefined reference

    • *XML_ATTR_UCS2_REF - The value of a UCS-2 reference

    • *XML_UNKNOWN_ATTR_REF - The name of an unknown entity reference

    • *XML_END_ATTR - Indicates the end of the attribute

    Events related to XML processing instructions

    • *XML_PI_TARGET - The name of the target

    • *XML_PI_DATA - The value of the data

    Events related to XML CDATA sections

    • *XML_START_CDATA - The beginning of the CDATA section

    • *XML_CHARS - The value of the CDATA section

    • *XML_END_CDATA - The end of the CDATA section

    Other events

    • *XML_COMMENT - The value of the XML comment

    • *XML_EXCEPTION - Indicates that the parser discovered an error

    • *XML_END_DOCUMENT - Indicates that parsing has ended

    На все случаи жизни.

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

    Определенную сложность представляют ситуации типа такой:

            <ТипРешения>
              <Идентификатор>3</Идентификатор>
              <Наименование>Решение МВК</Наименование>
            </ТипРешения>
    
            <ВидРешения>
              <Идентификатор>1</Идентификатор>
              <Наименование>Решение на приостановление (заморозка)</Наименование>
            </ВидРешения>
    
            <ТипСубъекта>
              <Идентификатор>2</Идентификатор>
              <Наименование>Физическое лицо</Наименование>
            </ТипСубъекта>
    
            <ТипДокумента>
              <Идентификатор>1631726</Идентификатор>
              <Наименование>ПАСПОРТ РФ</Наименование>
            </ТипДокумента>
    
            <ТипАдреса>
              <Идентификатор>6</Идентификатор>
              <Наименование>Гражданство</Наименование>
            </ТипАдреса>
    

    Когда смысл тегов <Идентификатор> и <Наименование> определяется тем, внутри какого тега они находятся - все это приходится отслеживать вручную.

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


    1. a-tk
      23.01.2023 10:27

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


    1. mayorovp
      23.01.2023 11:10

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


      Для разбора тяжёлых файлов проще всего использовать как раз XMLReader-подобный API.


      1. SpiderEkb
        24.01.2023 10:03
        +1

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

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

        С тегами - решаемо на самом деле. Например, внутри обработчика есть статическая переменная curTagName. При получении события *XML_START_ELEMENT (которое случается когда парсер встретил <tagName>) сопровождаемое именем тега, смотрим - если переменная пуста, то заносим в нее имя тега. Если нет - добавляем, например, точку и имя тега. При получении *XML_END_ELEMENT - наоборот - удаляем из конца строки имя тега, затем, если последний символ точка, удаляем ее.

        Тогда (в приведенном выше примере) будем иметь "составные" теги типа
        ТипДокумента.Идентификатор или ТипДокумента.Наименование и никакой путаницы не будет.

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

        Объем содержимого элемента - да. Глубина вложенности - нет. SAX парсеру все равно какая там глубина. Он работает с атомарными элементами - имена тегов, их содержимое. Он просто генерирует события - "начало тега ААА" - "содержимое АБВГДЕ" - "конец тега ААА". Все остальное уже внутри обработчика - надо отслеживать вложенность - отслеживаем. Не надо - не отслеживаем.

        Иногда для отслеживания вложенности достаточно просто выставлять (статический) флаг - "сейчас мы внутри тега ААА" чтобы правильно интерпретировать вложенный тег БББ. Иногда - работать с составными тегами типа ААА.БББ

        Важно что SAX просто идет по файлу и генерирует события.

        Для повышения производительности попробуй вставлять в базу не по одной записи, а батчами. Хотя бы insert'ы на сотню или тысячу записей формируй для начала.

        Это уже следующий уровень, определяемый бизнес-логикой. Тут вариантов тысячи. Логика может быть такой, что сначала требуется собрать некоторый блок информации, затем соотнести его с тем, что уже лежит в БД, определить наличие изменений, занести новые данные в БД (иногда раскидав их по разным таблицам), зафиксировать "историю" - что именно изменилось, сохранить в архиве предыдущую версию (на предмет возможного отката в случае "ой, мы вам позавчера не то прислали, вот правильная версия")...

        Но собственно к разбору XML все это уже не имеет отношения.


        1. mayorovp
          24.01.2023 11:41

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

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


          Тут важно добавить, что [...]

          Это вы вообще мне отвечаете?


          1. SpiderEkb
            25.01.2023 10:48
            -1

            Честно говоря, не вижу никаких причин почему SAX API должно быть фундаментально быстрее XMLReader.

            Потому что SAX радикально проще. Фактически это даже не парсер, а потоковый токенизатор. Там вообще ничего лишнего нет.

            Так-то способов работы с XML много. Можно даже скулем их разбирать (правда, сложные я не пробовал).

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


            1. mayorovp
              25.01.2023 12:08

              Проще для парсера — возможно. Для потребителя — не уверен.


              Но к скорости разбора это отношения не имеет.


  1. ABIDB
    23.01.2023 11:23
    +3

    Для повышения производительности попробуй вставлять в базу не по одной записи, а батчами. Хотя бы insert'ы на сотню или тысячу записей формируй для начала.


  1. shornikov
    23.01.2023 11:33
    +1

    "Insert into ... values (),()..." будет быстрее на порядок. Но, с ним не стоит увлекаться слишком большой строкой с values. И база может не прожевать, и, с какого-то момента, сама контектация строки начинает экспоненциально тормозить. Думаю, 10000 записей уже на грани фейла.

    А если максимально отказаться от классов (о чем упоминает автор) включая Std, то можно еще ускориться существенно. Конвертация xml объекта в массив через json_decode(json_encode()) - удачное решение для этого.


    1. ABIDB
      23.01.2023 12:32

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


  1. zVadim
    24.01.2023 14:44

    SQL запросы на вставку выполняются каждый в отдельной транзакции. Лучше не только вставлять в одном INSERT по несколько строк, но и вручную управлять транзакциями. Открывать транзакцию, вставлять много строк (напр. 1-10 тыс), а потом комитить транзакцию. Кроме того, разные СУБД поддерживают различные варианты массовой вставки данных, такие как BULK COPY (MS SQL), COPY (PostgreSQL). Они работают значительно быстрее, чем команды INSERT. В вашем случае (для заполнения справочника "с нуля") они должны подойти


  1. 1ove1
    24.01.2023 19:35
    +1

    Есть у меня проект, связанный с парсингом ГАР БД ФИАС, прекрасно понимаю описаные в статье проблемы, XMLReader единственное, на чем можно спарсить этот "склад *****" да к тому же на пхп. Честно, не понимаю зачем тебе вся база фиас-а, там столько данных (еще и с ошибками), что в конце концов ни ты, ни бд не понимают что с этой гегемонией делать. Первым делом я бы выбрал целевые файлы, обдумал схему базы, индексы, связи и т.п.. Может скорость парсинга и пострадает от постоянной индексации, итоговая базенка будет реально быстрой и компактной (вот в чем к слову проблема большого объема в твоем случае - ты просто качаешь данные в базу, и она знать не знает как их оптимизировать). Но всё же могу предложить некоторые оптимизации, которые я применил своем проекте:

    • Т.к. файлы в каталогах регионов шаблонные, впоне реально создать по отдельному процессу на каждый регион или документ (я делал и так и так, но все же остановился на регионах, ибо так меньше гемора). Сделать это можно через тот же pcntl. Если нет упора в бд или диск это сильно бустит темпы загрузки;

    • Использовать массовые insert-вставки (вплоть до десятков тысяч на запрос);

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


    1. SbWereWolf Автор
      24.01.2023 19:46
      +1

      Моя следущая статья будет про готовый парсер ФИАС ГАР, в смысле про готоаые классы и скрипты, что бы базу развернуть и что бы обновления накатывать.

      У меня сделано парционирование по региону, и я добавляю записи в транзакции пачками по 100 000.

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

      Может быть я допишу скрипты и классы что бы обновление скачивать и автоматически накатывать.

      Сайт налоговой имеет API, которое отдает ссылки на скачивание обновлений на определённую дату.


      1. 1ove1
        24.01.2023 20:28
        +1

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