JSON -> CSV создание строки заголовка и заполнения заголовка, если найдено пустое поле - PullRequest
0 голосов
/ 18 марта 2019

У меня есть программа на bash, которая получает файлы JSONline с несколькими миллионами этих объектов в строке (см. source )

{
  "company_number": "09626947",
  "data": {
    "address": {
      "address_line_1": "Troak Close",
      "country": "England",
      "locality": "Christchurch",
      "postal_code": "BH23 3SR",
      "premises": "9",
      "region": "Dorset"
    },
    "country_of_residence": "United Kingdom",
    "date_of_birth": {
      "month": 11,
      "year": 1979
    },
    "etag": "7123fb76e4ad7ee7542da210a368baa4c89d5a06",
    "kind": "individual-person-with-significant-control",
    "links": {
      "self": "/company/09626947/persons-with-significant-control/individual/FFeqke7T3LvGvX6xmuGqi5SJXAk"
    },
    "name": "Ms Angela Lynette Miller",
    "name_elements": {
      "forename": "Angela",
      "middle_name": "Lynette",
      "surname": "Miller",
      "title": "Ms"
    },
    "nationality": "British",
    "natures_of_control": [
      "significant-influence-or-control"
    ],
    "notified_on": "2016-06-06"
  }
}

У меня есть JQ-запрос, который выглядит следующим образом:

for file in psc_chunk_*; do
jq --slurp --raw-output 'def pad($n): range(0;$n) as $i | 
.[$i]; ([.[] | .data.natures_of_control | length] | max) as $mx |
.[] | 
select(.data) |
[.company_number, .data.kind, .data.address.address_line_1, .data.address.country, .data.address.locality, .data.address.postal_code, .data.address.premises, .data.identification.country_registered, .data.identification.legal_authority, .data.identification.legal_form, .data.identification.place_registered, .data.identification.registration_number, .data.ceased_on, .data.country_of_residence, "\(.data.date_of_birth.year)-\(.data.date_of_birth.month)", .data.etag, .data.links.self, .data.name, .data.name_elements.title, .data.name_elements.forename, .data.name_elements.middle_name, .data.name_elements.surname, .data.nationality, .data.notified_on, (.data.natures_of_control | pad($mx))] |
@csv' $file > $file.csv;
done

Что, вероятно, ранит глаза многих профессионалов JQ - это неэффективно при извлечении пар ключ: значение, и если провайдер случайно поменяет имя ключа, мой код больше не будет работать,

Есть ли способ просто объединить все json в csv , сохранив ключи в качестве заголовков - с дополнительной сложностью, что есть список natures_of_control, который имеет различныеколичество записей (для которых я использовал функцию pad для получения прямоугольного результата).

Ответы [ 2 ]

1 голос
/ 18 марта 2019

Вот подход, основанный на программном определении заголовков. Чтобы проиллюстрировать это, мы ограничиваем внимание одним объектом.

Поскольку встроенная функция jq paths игнорирует пути к нулю, и поскольку здесь одно из требований НЕ игнорировать такие пути, мы начнем с определения некоторых фильтров, аналогичных paths/0 и paths/1:

# Generate a stream of all paths, including paths to null
def allpaths:
  def conditional_recurse(f):  def r: ., (select(.!=null) | f | r); r;
  path(conditional_recurse(.[]?)) | select(length > 0);

def allpaths(filter):
  allpaths as $p | getpath($p) as $v | select($v | filter) | $p;

Далее мы определим функцию для сокращения длинных путей. Вы можете приспособить это к вашим потребностям.

# Input: an array denoting a path; output: a string
def abbreviate: if .[-1]|type == "number" then "\(.[-2]):\(.[-1])" else "\(.[-1])" end;

Наконец, мы собираем все вместе для случая одного объекта, генерируя строку заголовков, за которой следует строка соответствующих значений:

[allpaths(scalars)] as $p
| ($p | map(abbreviate) | @csv),
  ([getpath($p[])] | @csv)

выход

Для объекта JSON, о котором идет речь, результатом, полученным выше (с использованием параметра командной строки -r), будет следующий CSV:

"company_number","address_line_1","country","locality","postal_code","premises","region","country_of_residence","month","year","etag","kind","self","name","forename","middle_name","surname","title","nationality","natures_of_control:0","notified_on"
"09626947","Troak Close","England","Christchurch","BH23 3SR","9","Dorset","United Kingdom",11,1979,"7123fb76e4ad7ee7542da210a368baa4c89d5a06","individual-person-with-significant-control","/company/09626947/persons-with-significant-control/individual/FFeqke7T3LvGvX6xmuGqi5SJXAk","Ms Angela Lynette Miller","Angela","Lynette","Miller","Ms","British","significant-influence-or-control","2016-06-06"
0 голосов
/ 19 марта 2019

Вот решение, которое обрабатывает массивы во входном JSON путем преобразования их в «значения, разделенные двоеточиями»:

def atos: map(tostring) | join(":");

Используются те же общие allpaths фильтры, что и в других местах на этой странице:

# Generate a stream of all paths, including paths to null
def allpaths:
  def conditional_recurse(f):  def r: ., (select(.!=null) | f | r); r;
  path(conditional_recurse(.[]?)) | select(length > 0);

def allpaths(filter):
  allpaths as $p | getpath($p) as $v | select($v | filter) | $p;

Опять же для случая одного объекта решение может быть получено следующим образом:

walk( if type == "array" then atos else . end )
| [allpaths(scalars)] as $p
| ($p | map(last) | @csv),
  ([getpath($p[])] | @csv)

выход

Для данного ввода, вывод будет:

"company_number","address_line_1","country","locality","postal_code","premises","region","country_of_residence","month","year","etag","kind","self","name","forename","middle_name","surname","title","nationality","natures_of_control","notified_on"
"09626947","Troak Close","England","Christchurch","BH23 3SR","9","Dorset","United Kingdom",11,1979,"7123fb76e4ad7ee7542da210a368baa4c89d5a06","individual-person-with-significant-control","/company/09626947/persons-with-significant-control/individual/FFeqke7T3LvGvX6xmuGqi5SJXAk","Ms Angela Lynette Miller","Angela","Lynette","Miller","Ms","British","significant-influence-or-control","2016-06-06"

Протест

Представленное здесь решение предназначено только для использования, когда все массивы на входе имеют скалярное значение.

Эффективная обработка потока изоморфных объектов

Далее предполагается, что поток объектов изоморфен в том смысле, что упорядочение ключей внутри объекта JSON не имеет значения.

Инфраструктура

Инфраструктура allpaths и atos такая же, как и выше, поэтому здесь повторяться не будет.

Вспомогательные функции

# input: an object
def paths:
  walk( if type == "array" then atos else . end )
  | [allpaths(scalars)] ;

# input: an array of paths
def headers:
  map(last) | @csv ; 

# input: an object
def row($paths):
  walk( if type == "array" then atos else . end )
  | [getpath($paths[])]
  | @csv ;

Обработка входного потока

Следующее использует input для чтения первого объекта и inputs для чтения остальных, поэтому важно вызывать jq с параметром командной строки -n:

input as $first
| ($first|paths) as $paths
| ($paths | headers),
  ($first | row($paths)),
  (inputs | row($paths))
...