Комментарии 31
Парсить файл размером 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_ функций.
Я парсил с помощью 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 с. Кошмар!
Опечатка, XMLReder -> XMLReader
и ещё одна "а надо сказать, что 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, которое отдает ссылки на скачивание обновлений на определённую дату.
С обновлениями там раньше косяки были серьезные, прямо катастрофические ошибки, получалось так, что проще заново перекачать все данные. Но сейчас, когда база разрастается на глазах, уже и сам начинаю на обновления смотреть. Сейчас, чтобы полностью обновить мою базу (в нее входят адресные объекты, дома, и коды, все с индексацией) нужны целые сутки на вполне приличной машине. Буду следить за тем, как у тебя получится.
Работаем с XML как с массивом, версия 2