Разбор XML через xq (jq) с помощью GNU Parallel
У меня есть входной xml-файл следующим образом:
<?xml version="1.0" encoding="UTF-8"?>
<yml_catalog date="2022-12-19T18:02:16+03:00">
<shop>
<offers>
<offer id="177484701" available="true">
<url>http:/site.com/offer1</url>
...
</offer>
<offer id="177485701" available="true">
<url>http:/site.com/offer2</url>
...
</offer>
<offer id="177484401" available="true">
<url>http:/site.com/offer3</url>
...
</offer>
...
</offers>
</shop>
</yml_catalog>
Этот файл содержит от 2 до 8 миллионов предложений и после очистки занимает 10 гигабайт.
Я использую встроенный функционал xq без циклов (с циклами время обработки сильно увеличивается). Xq предоставляется пакетом Python yq и использует jq.
cat file.xml | xq \
--arg id "${id}" \
--arg available "${available}" \
--arg url "${url}" \
-r '.yml_catalog.shop.offers[][] | "~~~\(.["@id"])~~~\(.["@available"])~~~\(.url)"' > /tmp/data.txt
Но обработка использует только 1 ядро. Этот файл периодически обновляется и должен быть обработан как можно скорее. Как я могу распараллелить его через параллель GNU? Можно с xq?
Я пробовал разные варианты, но все они дают ошибки.
$ parallel -j8 --spreadstdin "xq --arg id '${id}' --arg available '${available}' --arg url '${url}' -r '.yml_catalog.shop.offers[][] | \"~~~\(.[\"@id\"])~~~\(.[\"@available\"])~~~\(.url)\"'" < file.xml > /tmp/data.txt
...
xq: Error running jq: ExpatError: junk after document element: line 2, column 4.
xq: Error running jq: ExpatError: no element found: line 11643, column 0.
...
$ cat file.xml | parallel --results output/{/} -q xq \
--arg id "${id}" \
--arg available "${available}" \
--arg url "${url}" \
-r '.yml_catalog.shop.offers[][] | "~~~\(.["@id"])~~~\(.["@available"])~~~\(.url)"' > /tmp/data.txt
...
usage: xq [-h] [--xml-output] [--xml-dtd] [--xml-root XML_ROOT]
[--xml-force-list XML_FORCE_LIST] [--in-place] [--version]
[jq_filter] [files ...]
xq: error: argument files: can't open '<?xml version="1.0" encoding="UTF-8"?>': [Errno 2] No such file or directory: '<?xml version="1.0" encoding="UTF-8"?>'
...
$ parallel -j8 --pipepart --block 100M -a file.xml -k "xq --arg id '${id}' --arg available '${available}' --arg url '${url}' -r '.yml_catalog.shop.offers[][] | \"~~~\(.[\"@id\"])~~~\(.[\"@available\"])~~~\(.url)\"'" > /tmp/data.txt
...
xq: Error running jq: ExpatError: no element found: line 801268, column 0.
xq: Error running jq: ExpatError: junk after document element: line 2, column 4.
xq: Error running jq: ExpatError: not well-formed (invalid token): line 2, column 4.
...
Последний вариант мне кажется нелогичным, так как скорее всего данные разрываются между ядрами. Остальные проверенные варианты еще более нелогичны.
1 ответ
Я знаю, что вы просили о решении, и этот ответ не касается этого предпочтения. Но, возможно, следующее решение безGNU parallel
Вам поможет.
Идея состоит в том, чтобы разделить массивoffers
на разделы одинакового размера (PARTITIONS
) с использованием недокументированного фильтра jq_nwise
а затем параллельно обрабатывать каждый раздел в подоболочке.
Я добавил образец обработки в качестве заполнителя для произвольной обработки в приведенный ниже код (отфильтруйте доступные предложения и извлеките идентификатор). Я также добавил код для объединения результатов обработки в конце.
Обратите внимание, что это решение шифрует предложения. Если это нежелательно, вы можете отсортировать файлы по порядковому номеру перед объединением результатов.
#!/bin/bash
INPUT='<?xml version="1.0" encoding="UTF-8"?>
<yml_catalog date="2022-12-19T18:02:16+03:00">
<shop>
<offers>
<offer id="177484701" available="true">
<url>http:/site.com/offer1</url>
</offer>
<offer id="177485702" available="false">
<url>http:/site.com/offer2</url>
</offer>
<offer id="177484403" available="true">
<url>http:/site.com/offer3</url>
</offer>
<offer id="177484404" available="true">
<url>http:/site.com/offer4</url>
</offer>
</offers>
</shop>
</yml_catalog>'
PARTITIONS=2 # number of partitions wanted
FILENAME_PARTITIONS="result_partition_"
CODE_SPLIT_OFFERS='
.yml_catalog.shop.offers.offer |
_nwise((length - (length % $partitions)) / $partitions)
'
CODE_PROCESSING='
[.[] | select(."@available" == "true") |
."@id" |
tonumber]
'
PARTITION=1
while read LINE
do
echo "start processing partition $PARTITION"
export FILENAME="${FILENAME_PARTITIONS}$PARTITION"
jq -r -c "$CODE_PROCESSING" <<< "$LINE" > $FILENAME &
((PARTITION++))
done <<< $(xq -c --argjson partitions $PARTITIONS "$CODE_SPLIT_OFFERS" <<< "$INPUT")
echo -e "\nwait until all partitions are processed"
wait
echo -e "\nall partitions are processed. Processing results:"
find . -name "${FILENAME_PARTITIONS}*" -print -exec cat {} \;
echo -e "\nmerged results:"
jq -s -c 'flatten' <<< $(find . -name "${FILENAME_PARTITIONS}*" -exec cat {} \;)
Выход
start processing partition 1
start processing partition 2
wait until all partitions are processed
all partitions are processed. Processing results:
./result_partition_2
[177484403,177484404]
./result_partition_1
[177484701]
merged results:
[177484403,177484404,177484701]
Замените две части приведенного выше кода на альтернативную обработку:
CODE_PROCESSING='
.[] | "~~~\(.["@id"])~~~\(.["@available"])~~~\(.url)"
'
echo -e "\nmerged results:"
find . -name "${FILENAME_PARTITIONS}*" -exec cat {} \;
Выход
start processing partition 1
start processing partition 2
wait until all partitions are processed
all partitions are processed. Processing results:
./result_partition_2
~~~177484403~~~true~~~http:/site.com/offer3
~~~177484404~~~true~~~http:/site.com/offer4
./result_partition_1
~~~177484701~~~true~~~http:/site.com/offer1
~~~177485702~~~false~~~http:/site.com/offer2
merged results:
~~~177484403~~~true~~~http:/site.com/offer3
~~~177484404~~~true~~~http:/site.com/offer4
~~~177484701~~~true~~~http:/site.com/offer1
~~~177485702~~~false~~~http:/site.com/offer2