Pull to refresh

Comments 31

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

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

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

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

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

Статья начинается со слов:

надо читать файл последовательно, частями, парсить только нужные элементы

и заканчивается словами:

PHP и БД каждый отъедают не больше 8 мегабайт оперативки и не больше 12% процессора

Нет, все 4 гигабайта моя библиотека в память не грузит, если ей выдать ссылку на файл, если ей выдать строку размером 4 гига, то да, прямо эта строка и будет парситься, и я не знаю что тут придумать, думаете если дали строку то надо её во временный файл записать и вычитывать по кусочку ?

Сложный кейс :) не для библиотеки на сделанной на коленке за пять минут.

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

Играл когда-то в поиск подстроки в xml. Очень удивился, когда mb_substr проиграл регуляркам, а те отстали от DomDocument в несколько раз уже на файлах в пару мегабайт.

Э-э-э, а как вообще искать подстроку через mb_substr? Неужто перебирать все и сравнивать? Разумеется, это будет медленнее регулярок,


Вот если mb_strpos окажется медленнее — тут и правда пора удивляться.

Упс, думаю mb_strpos, пишу mb_substr -_-"

Да, именно mb_strpos оказался медленнее регулярок с /u. Мне тоже казалось, что быстрее него только strpos, но нет.

Я искал относительно длинные строки (символов 8 - 12) так что PCRE2 скорее всего использовал умные алгоритмы, а mb_strpos тупой перебор всех символов. Проверял я это на php 7.3 или даже 7.1, а в 8.2 вроде завезли пачку оптимизаций для mb_ функций.

Хохма в том, что поиск подстроки — это же уровень школьных олимпиад, его в 7м классе школьники наизусть пишут, а то и раньше...


Как они вообще смогли сделать его медленнее регулярки?

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

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

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

Провёл тест и библиотека 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 с. Кошмар!

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

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

Кроме 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</Идентификатор>
          <Наименование>Гражданство</Наименование>
        </ТипАдреса>

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

Кто есть потребитель? Разработчик? Повторюсь, если речь идет о предельной эффективности, удобство разработчика не волнует ровным счетом никого. Не уложитесь в заданные рамки - на нагрузочном тестировании не согласуют внедрение и будете все переделывать.

У нас практически все мало-мальски важное проходит через НТ сейчас. И очень жесткое НТ. Завернуть могут на раз-два. Как по скорости, так и по потреблению ресурсов (память, проц...).

И еще постоянно старое тестят и на оптимизацию задачи кидают.

Да, есть не критичные задачи, там можно как проще и удобнее делать. А есть критичные. Там приходится выжиматься.

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


Ну и мне всё ещё неочевидно что XMLReader медленнее SAX. Время, сэкономленное на простоте парсера, запросто может потратиться на обработку вложенных скоупов.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Sign up to leave a comment.

Articles