jq: Как я могу передать объекты из массива в разные файлы на основе данных в объекте?

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

Я использую существующее выражение JQ для фильтрации только тех данных, которые мне действительно нужны:

{ fipscode: .fipscode, level: .level, polid: .polid, polnum: .polnum, precinctsreporting: .precinctsreporting, precinctsreportingpct: .precinctsreportingpct, precinctstotal: .precinctstotal, raceid: .raceid, runoff: .runoff, statepostal: .statepostal, votecount: .votecount, votepct: .votepct, winner: .winner }

Вот образец моего ввода:

[
    { "ballotorder": 2, "candidateid": "9718", "delegatecount": 0, "description": null, "electiondate": "2018-08-28", "electtotal": 0, "electwon": 0, "fipscode": null, "first": "Doug", "id": "3015-polid-64364-state-AZ-1", "incumbent": true, "initialization_data": false, "is_ballot_measure": false, "last": "Ducey", "lastupdated": "2018-08-30T00:01:38.897Z", "level": "state", "national": true, "officeid": "G", "officename": "Governor", "party": "GOP", "polid": "64364", "polnum": "5554", "precinctsreporting": 1488, "precinctsreportingpct": 0.9993000000000001, "precinctstotal": 1489, "raceid": "3015", "racetype": "Primary", "racetypeid": "R", "reportingunitid": "state-AZ-1", "reportingunitname": null, "runoff": false, "seatname": null, "seatnum": null, "statename": "Arizona", "statepostal": "AZ", "test": false, "uncontested": false, "votecount": 355455, "votepct": 0.705493, "winner": true },
    { "ballotorder": 2, "candidateid": "21689", "delegatecount": 0, "description": null, "electiondate": "2018-08-28", "electtotal": 0, "electwon": 0, "fipscode": null, "first": "Ron", "id": "10046-polid-62557-state-FL-1", "incumbent": false, "initialization_data": false, "is_ballot_measure": false, "last": "DeSantis", "lastupdated": "2018-08-29T19:29:50.367Z", "level": "state", "national": true, "officeid": "G", "officename": "Governor", "party": "GOP", "polid": "62557", "polnum": "13918", "precinctsreporting": 5968, "precinctsreportingpct": 1.0, "precinctstotal": 5968, "raceid": "10046", "racetype": "Primary", "racetypeid": "R", "reportingunitid": "state-FL-1", "reportingunitname": null, "runoff": false, "seatname": null, "seatnum": null, "statename": "Florida", "statepostal": "FL", "test": false, "uncontested": false, "votecount": 913997, "votepct": 0.564728, "winner": true },
    { "ballotorder": 2, "candidateid": "45555", "delegatecount": 0, "description": null, "electiondate": "2018-08-28", "electtotal": 0, "electwon": 0, "fipscode": null, "first": "Rex", "id": "38538-polid-67011-state-OK-1", "incumbent": false, "initialization_data": false, "is_ballot_measure": false, "last": "Lawhorn", "lastupdated": "2018-08-29T02:44:44.610Z", "level": "state", "national": true, "officeid": "G", "officename": "Governor", "party": "Lib", "polid": "67011", "polnum": "40784", "precinctsreporting": 1951, "precinctsreportingpct": 1.0, "precinctstotal": 1951, "raceid": "38538", "racetype": "Runoff", "racetypeid": "L", "reportingunitid": "state-OK-1", "reportingunitname": null, "runoff": false, "seatname": null, "seatnum": null, "statename": "Oklahoma", "statepostal": "OK", "test": false, "uncontested": false, "votecount": 379, "votepct": 0.409287, "winner": false }
]

В качестве вывода я ожидал бы получить Arizona.json, содержащий только элемент (ы) из этого состояния, а также отфильтрованный для удаления нежелательных полей:

[
  { "fipscode": null, "level": "state", "polid": "64364", "polnum": "5554", "precinctsreporting": 1488, "precinctsreportingpct": 0.9993000000000001, "precinctstotal": 1489, "raceid": "3015", "runoff": false, "statepostal": "AZ", "votecount": 355455, "votepct": 0.705493, "winner": true }
]

... и то же самое для других вовлеченных государств (Florida.json и Oklahoma.json).


Вот скрипт bash и jq, который у меня есть:

cat master.json |
jq -cn --stream 'fromstream(1|truncate_stream(inputs))' |
jq -c '.statename as $state | {
    fipscode: .fipscode,
    level: .level,
    polid: .polid,
    polnum: .polnum,
    precinctsreporting: .precinctsreporting,
    precinctsreportingpct: .precinctsreportingpct,
    precinctstotal: .precinctstotal,
    raceid: .raceid,
    runoff: .runoff,
    statepostal: .statepostal,
    votecount: .votecount,
    votepct: .votepct,
    winner: .winner
}'

Я не могу понять, как перехватить каждую строку, чтобы определить, куда должен идти вывод. Это возможно?


person Tyler    schedule 18.09.2018    source источник
comment
Когда вы говорите «добавить каждую строку в файл», что вы имеете в виду? Вы хотите отредактировать существующий файл на месте? Создать выходной поток? Что-то другое?   -  person Charles Duffy    schedule 18.09.2018
comment
Создание правильного минимального воспроизводимого примера с полезными входными данными (не поддельными данными, которые не являются настоящим JSON, а входными данными кто-то может действительно проверить предложенный ответ с помощью), и образцы выходных данных, которые будут сгенерированы этими входами, станут большим шагом к получению ответа на вопрос.   -  person Charles Duffy    schedule 18.09.2018
comment
(Если вам нужен, скажем, отдельный выходной файл для каждого состояния, это другой вопрос, и интересный - требуются инструменты, которых нет в чистом / базовом jq, но это выполнимо даже в этом случае ... но, пожалуйста, отредактируйте вопрос, чтобы мы могли быть полностью уверены в том, что именно вы просите).   -  person Charles Duffy    schedule 18.09.2018
comment
Да, мне нужен отдельный выходной файл для каждого состояния. Отредактирую вопрос. Спасибо за предложения.   -  person Tyler    schedule 18.09.2018
comment
Ознакомьтесь с минимальной частью рекомендаций MCVE - в идеале мы хотим, чтобы образец был максимально упрощен, но при этом оставался достаточно полным, чтобы можно было проверять ответы, не требуя изменений.   -  person Charles Duffy    schedule 18.09.2018
comment
... удаление некоторых дополнительных полей, чтобы мы могли иметь как минимум две записи для одного и того же состояния, например, было бы значительным улучшением с точки зрения обеспечения тестового покрытия для важного поведения.   -  person Charles Duffy    schedule 18.09.2018


Ответы (2)


Вы можете сделать это с помощью одной копии jq разделения элементов данных из входного файла, а затем другого экземпляра для каждого состояния, сопоставляющего эти элементы данных вместе, а связующим звеном является bash. См. Следующий пример для bash 4.2 или новее (может работать с 4.1, мне нужно проверить).

#!/usr/bin/env bash
case $BASH_VERSION in ''|[123].*|4.[01].*) echo "ERROR: Bash 4.2 required" >&2; exit 1;; esac

input_file=$1
[[ -s $input_file ]] || { echo "Usage: ${0##*/} input-file" >&2; exit 1; }

jq_split_script='
# modify this function to fit your needs
def relevantContentOnly:
  { fipscode, level, polid, polnum, precinctsreporting, precinctsreportingpct, precinctstotal, raceid, runoff, statepostal, votecount, votepct, winner };

.[] | [.statename, (relevantContentOnly | tojson)] | @tsv
'

# Use an associative array to map from state names to output FDs
declare -A out_fds=( )

# Read state / line-of-data pairs from our JQ script...
while IFS=$'\t' read -r state data; do
  # If we don't already have a writer for the current state, start one.
  if [[ ! ${out_fds[$state]} ]]; then
    exec {new_fd}> >(jq -n '[inputs]' >"$state.json")
    out_fds[$state]=$new_fd
  fi
  # Regardless, send the data to the FD we have for this state
  printf '%s\n' "$data" >&${out_fds[$state]}
done < <(jq -rc "$jq_split_script" <"$input_file") # ...running the JQ script above.

# close output FDs, so the JQ instances all flush
for fd in "${!out_fds[@]}"; do
  exec {fd}>&-
done
person Charles Duffy    schedule 18.09.2018

Вот простое решение, основанное на том, с чего вы начали:

< master.json jq -cn --stream 'fromstream(1|truncate_stream(inputs))' |
  jq -cr '.statename, {
    fipscode,
    level,
    polid,
    polnum,
    precinctsreporting,
    precinctsreportingpct,
    precinctstotal,
    raceid,
    runoff,
    statepostal,
    votecount,
    votepct,
    winner
}' | while read -r statename && read -r object
do
  echo "$object" >> "$statename.json"
done

Обратите внимание, что это добавит объекты к любым существующим файлам «$ statename.json».

С вашими [исходными] образцами данных из приведенного выше получается Arizona.json, Florida.json и Oklahoma.json.

Твик

Если накладные расходы при использовании echo являются проблемой, вы можете использовать awk:

awk '
  fn!="" {print > fn; fn=""; next}
  {fn=$0 ".json";
   if (fns[fn]!=1){fns[fn]=1; print fn > "filenames.txt"}}'

Финал

Поскольку вы хотите, чтобы эти файлы содержали массивы объектов, вы можете использовать jq -s для достижения окончательных результатов. Я бы, вероятно, собрал имена файлов в цикле while (наивно, например, echo "$statename.json" >> filenames.txt), а затем использовал бы sponge:

sort -u filenames.txt | 
  while read -r fn ; do 
    jq -s . "$fn" | sponge "$fn"
  done
person peak    schedule 18.09.2018
comment
Я понимаю, что пытаюсь избежать bash 4.x-isms, но повторное открытие выходного файла для каждой отдельной строки довольно беспорядочно. Можно использовать awk для замены цикла while в конце - по крайней мере, GNU awk будет автоматически поддерживать кеш предварительно открытых файловых файлов для различных выходных файлов и повторно использовать их соответствующим образом, а не выполнять повторяющиеся операции открытия / записи / закрытия, как это код bash делает. - person Charles Duffy; 18.09.2018
comment
@CharlesDuffy - Конечно, я думал об использовании awk в конце, но подумал, что сосредоточусь на главном препятствии, с которым столкнулся OP, думая, что в процентах от общего времени процессора разница, вероятно, будет довольно небольшой. У тебя есть номера? - person peak; 18.09.2018
comment
Я бы хотел смотреть на время настенных часов, а не на процессорное время; в зависимости от деталей файловой системы будет много времени на ожидание ввода-вывода и накладные расходы на системные вызовы. Посмотрим - в США около 500 000 выборных должностей, если считать на уровне штатов и местных властей. Используя тайминг best-of-3, я получаю настенные часы 0 м 59,324 с для time for ((i=0; i<500000; i++)); do echo >>test; done и 9,885 с для time for ((i=0; i<500000; i++)); do echo test; done >>test. - person Charles Duffy; 18.09.2018