Используя jq, как я могу разбить очень большой файл JSON на несколько файлов, каждый из которых содержит определенное количество объектов?

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

Итак, данные как это:

[{
  "id": 1,
  "user": {
    "name": "Nichols Cockle",
    "email": "ncockle0@tmall.com",
    "address": {
      "city": "Turt",
      "state": "Thị Trấn Yên Phú"
    }
  },
  "product": {
    "name": "Lychee - Canned",
    "code": "36987-1526"
  }
}, {
  "id": 2,
  "user": {
    "name": "Isacco Scrancher",
    "email": "iscrancher1@aol.com",
    "address": {
      "city": "Likwatang Timur",
      "state": "Biharamulo"
    }
  },
  "product": {
    "name": "Beer - Original Organic Lager",
    "code": "47993-200"
  }
}, {
  "id": 3,
  "user": {
    "name": "Elga Sikora",
    "email": "esikora2@statcounter.com",
    "address": {
      "city": "Wenheng",
      "state": "Piedra del Águila"
    }
  },
  "product": {
    "name": "Parsley - Dried",
    "code": "36987-1632"
  }
}, {
  "id": 4,
  "user": {
    "name": "Andria Keatch",
    "email": "akeatch3@salon.com",
    "address": {
      "city": "Arras",
      "state": "Iracemápolis"
    }
  },
  "product": {
    "name": "Wine - Segura Viudas Aria Brut",
    "code": "51079-385"
  }
}, {
  "id": 5,
  "user": {
    "name": "Dara Sprowle",
    "email": "dsprowle4@slate.com",
    "address": {
      "city": "Huatai",
      "state": "Kaduna"
    }
  },
  "product": {
    "name": "Pork - Hock And Feet Attached",
    "code": "0054-8648"
  }
}]

Где это один законченный объект:

{
  "id": 1,
  "user": {
    "name": "Nichols Cockle",
    "email": "ncockle0@tmall.com",
    "address": {
      "city": "Turt",
      "state": "Thị Trấn Yên Phú"
    }
  },
  "product": {
    "name": "Lychee - Canned",
    "code": "36987-1526"
  }
}

И каждый файл будет определенным количеством таких объектов.

2 ответа

Решение

[РЕДАКТИРОВАТЬ: Этот ответ был пересмотрен в соответствии с пересмотром вопроса.]

Ключом к использованию jq для решения проблемы является -c опция командной строки, которая производит вывод в формате JSON-Lines (т. е. в данном случае по одному объекту на строку). Затем вы можете использовать инструмент, такой как awk или же split распределить эти строки среди нескольких файлов.

Если файл не слишком большой, то самым простым будет начать конвейер с:

jq -c '.[]' INPUTFILE

Если файл слишком велик для удобного размещения в памяти, вы можете использовать потоковый анализатор jq, например, так:

jq -cn --stream 'fromstream(1|truncate_stream(inputs))'

Для дальнейшего обсуждения потокового парсера см., Например, соответствующий раздел в FAQ по jq: https://github.com/stedolan/jq/wiki/FAQ

Разметка

Для различных подходов к разделению вывода, полученного на первом шаге, смотрите, например, Как разбить большой текстовый файл на меньшие файлы с равным количеством строк?

Если требуется, чтобы каждый из выходных файлов был массивом объектов, то, вероятно, я бы использовал awk выполнить как разделение, так и восстановление за один шаг, но есть много других разумных подходов.

Если вход представляет собой последовательность объектов JSON

Для справки: если исходный файл состоит из потока или последовательности объектов JSON, соответствующий вызов будет:

jq -n -c inputs INPUTFILE

С помощью inputs таким образом позволяет эффективно обрабатывать произвольное количество объектов.

Можно разрезать файл или поток json с помощью jq. См. Сценарий ниже. В sliceSizeПараметр устанавливает размер срезов и определяет, сколько входов одновременно хранится в памяти. Это позволяет контролировать использование памяти.

Вход для нарезки

Вход не нужно форматировать.

В качестве ввода возможны:

  • массив входных данных Json
  • поток входных данных Json

Нарезанный вывод

Файлы могут быть созданы с помощью форматированного или компактного Json.

Нарезанные выходные файлы могут содержать:

  • массив входных данных Json с размером = $ sliceSize
  • поток входных данных Json с элементами $ sliceSize

Представление

Быстрый тест показывает время и потребление памяти во время нарезки (измерено на моем ноутбуке)

файл со 100000 json-объектов, 46 МБ

  • sliceSize = 5.000: время = 35 секунд
  • sliceSize = 10.000: время = 40 секунд
  • sliceSize = 25,000: время = 1 мин.
  • sliceSize = 50.000: время = 1 мин 52 сек

файл с 1.000.000 json-объектов, 450МБ

  • sliceSize = 5000: время = 5 мин 45 сек
  • sliceSize = 10.000: время = 6 мин. 51 сек.
  • sliceSize = 25,000: время = 10 мин 5 сек
  • sliceSize = 50.000: время = 18 мин 46 сек, максимальное потребление памяти: ~150 МБ
  • sliceSize = 100.000: время = 46 мин 25 сек, максимальное потребление памяти: ~ 300 МБ
      #!/bin/bash

SLICE_SIZE=2

JQ_SLICE_INPUTS='
   2376123525 as $EOF |            # random number that does not occur in the input stream to mark the end of the stream
   foreach (inputs, $EOF) as $input
   (
      # init state
      [[], []];                    # .[0]: array to collect inputs
                                   # .[1]: array that has collected $sliceSize inputs and is ready to be extracted
      # update state
      if .[0] | length == $sliceSize   # enough inputs collected
         or $input == $EOF             # or end of stream reached
      then [[$input], .[0]]        # create new array to collect next inputs. Save array .[0] with $sliceSize inputs for extraction
      else [.[0] + [$input], []]   # collect input, nothing to extract after this state update
      end;

      # extract from state
      if .[1] | length != 0
      then .[1]                    # extract array that has collected $sliceSize inputs
      else empty                   # nothing to extract right now (because still collecting inputs into .[0])
      end
   )
'

write_files() {
  local FILE_NAME_PREFIX=$1
  local FILE_COUNTER=0
  while read line; do
    FILE_COUNTER=$((FILE_COUNTER + 1))
    FILE_NAME="${FILE_NAME_PREFIX}_$FILE_COUNTER.json"
    echo "writing $FILE_NAME"
    jq '.'      > $FILE_NAME <<< "$line"   # array of formatted json inputs
#   jq -c '.'   > $FILE_NAME <<< "$line"   # compact array of json inputs
#   jq '.[]'    > $FILE_NAME <<< "$line"   # stream of formatted json inputs
#   jq -c '.[]' > $FILE_NAME <<< "$line"   # stream of compact json inputs
  done
}


echo "how to slice a stream of json inputs"
jq -n '{id: (range(5) + 1), a:[1,2]}' |   # create a stream of json inputs
jq -n -c --argjson sliceSize $SLICE_SIZE "$JQ_SLICE_INPUTS" |
write_files "stream_of_json_inputs_sliced"

echo -e "\nhow to slice an array of json inputs"
jq -n '[{id: (range(5) + 1), a:[1,2]}]' |                  # create an array of json inputs
jq -n --stream 'fromstream(1|truncate_stream(inputs))' |   # remove outer array to create stream of json inputs
jq -n -c --argjson sliceSize $SLICE_SIZE "$JQ_SLICE_INPUTS" |
write_files "array_of_json_inputs_sliced"

вывод скрипта

      how to slice a stream of json inputs
writing stream_of_json_inputs_sliced_1.json
writing stream_of_json_inputs_sliced_2.json
writing stream_of_json_inputs_sliced_3.json

how to slice an array of json inputs
writing array_of_json_inputs_sliced_1.json
writing array_of_json_inputs_sliced_2.json
writing array_of_json_inputs_sliced_3.json

сгенерированные файлы

array_of_json_inputs_sliced_1.json

      [
  {
    "id": 1,
    "a": [1,2]
  },
  {
    "id": 2,
    "a": [1,2]
  }
]

array_of_json_inputs_sliced_2.json

      [
  {
    "id": 3,
    "a": [1,2]
  },
  {
    "id": 4,
    "a": [1,2]
  }
]

array_of_json_inputs_sliced_3.json

      [
  {
    "id": 5,
    "a": [1,2]
  }
]
Другие вопросы по тегам