Learning Platform
Глоссарий Troubleshooting
Урок 20.01 · 22 мин
Средний
jqyqJSONYAMLJSONLCLI tools

jq и yq: JSON и YAML в shell

В data engineering 80% interchange format — JSON. API возвращает JSON, Airflow конфигурируется JSON-ом, Kafka events часто JSON, CloudWatch logs тоже JSON. Без удобного инструмента для shell это превращается в боль: парсить grep’ом не работает (line-oriented vs structured), python-one-liner громоздкий, sed/awk не понимают вложенность.

jq решает это. Это sed/awk для JSON — компактный язык запросов, который встраивается в pipelines, обрабатывает gigabyte-файлы стрим-режимом, делает фильтрацию/трансформацию/агрегацию за одну команду.

yq — то же самое для YAML. Конфиги Kubernetes, GitHub Actions workflows, Ansible playbooks — всё YAML. yq позволяет редактировать их из CI/скриптов программно.

В этом уроке: jq основы (фильтры, селекторы, типы), частые DE-паттерны, jsonl streaming, и yq для YAML.


Установка

# Debian/Ubuntu:
sudo apt install jq

# macOS:
brew install jq

# yq (Mike Farah, Go-based, де-факто стандарт):
brew install yq
# или: snap install yq
# или: docker run --rm -v "$PWD:/workdir" mikefarah/yq ...
WARNING

Существуют два разных yq: 1) Python-based python-yq (https://github.com/kislyuk/yq) — wrapper на jq для YAML, с jq-синтаксисом. 2) Go-based mikefarah/yq (https://github.com/mikefarah/yq) — отдельный язык, не совместим с jq. В большинстве туториалов и production используется mikefarah/yq — он быстрее, без зависимостей Python. Проверь установленную версию: yq --version (если упоминает Mike Farah — Go-based).


jq основы: pretty-print, identity

# Pretty-print произвольного JSON:
curl -s https://api.github.com/repos/torvalds/linux | jq '.'

. — identity filter, “вернуть как есть”. Если вход — minified JSON, выход — отформатированный с отступами и подсветкой (если stdout — TTY).

# Минифицировать обратно:
echo '{"a": 1, "b": 2}' | jq -c '.'
# {"a":1,"b":2}

-c — compact, одна строка. Полезно для логов и pipeline-stream.


Field access

echo '{"name": "alice", "age": 30}' | jq '.name'
# "alice"

Вложенные поля:

echo '{"user": {"name": "alice", "email": "[email protected]"}}' | jq '.user.email'
# "[email protected]"

Опциональный доступ (если поле может отсутствовать):

echo '{}' | jq '.missing'
# null

echo '{}' | jq '.missing?'
# null (no error)

echo '{}' | jq '.deep.missing'
# null

echo '{}' | jq '.deep.missing // "default"'
# "default" (// — coalesce operator, как ?? в JS/Python)

Raw output (-r)

Без -r строковые значения jq возвращает с кавычками:

echo '{"name": "alice"}' | jq '.name'
# "alice"     <- с кавычками

echo '{"name": "alice"}' | jq -r '.name'
# alice       <- без

-r (raw) обязателен, когда выход jq идёт в bash-переменную:

USERNAME=$(curl -s ... | jq -r '.user.name')
# Без -r получишь "alice" с кавычками внутри переменной, и аргументы поломаются
TIP

Правило: в pipeline в bash почти всегда нужен -r. Кавычки нужны только когда передаёшь output в другую jq команду или другой JSON parser, который сам ожидает quoted strings.


Arrays: iteration и индексы

data='[{"name":"alice"},{"name":"bob"},{"name":"carol"}]'

# Доступ по индексу:
echo "$data" | jq '.[0]'      # {"name":"alice"}
echo "$data" | jq '.[1].name' # "bob"
echo "$data" | jq '.[-1]'     # {"name":"carol"} (last)

# Slice:
echo "$data" | jq '.[0:2]'    # первые два

# Развернуть массив в поток отдельных объектов:
echo "$data" | jq '.[]'
# {"name":"alice"}
# {"name":"bob"}
# {"name":"carol"}

# Длина:
echo "$data" | jq 'length'    # 3

.[] — iterator, “развернуть в стрим”. Это ключевой оператор: вместо одного output (массив) jq выдаёт N outputs (по одному на элемент). Каждый можно обработать дальше.


Filter и select

users='[{"name":"alice","age":30},{"name":"bob","age":17},{"name":"carol","age":25}]'

# Взрослые (age >= 18):
echo "$users" | jq '.[] | select(.age >= 18)'
# {"name":"alice","age":30}
# {"name":"carol","age":25}

# Только имена взрослых:
echo "$users" | jq '.[] | select(.age >= 18) | .name'
# "alice"
# "carol"

# С -r — без кавычек:
echo "$users" | jq -r '.[] | select(.age >= 18) | .name'
# alice
# carol

| в jq — pipe внутри выражения, как pipe в shell. Каждый этап принимает input от предыдущего.

jq pipeline anatomy
.[]iterator — развернуть массив в поток отдельных values. После этого каждое value обрабатывается отдельно
|
select()filter — оставить только те values, для которых выражение true. Аналог WHERE в SQL
|
.nameprojection — извлечь только нужное поле. Аналог SELECT name FROM ...

Сложные предикаты

# AND:
jq '.[] | select(.age >= 18 and .country == "RU")'

# OR:
jq '.[] | select(.status == "active" or .status == "pending")'

# IN-список (через any):
jq '.[] | select(.tag | IN("a","b","c"))'

# Regex match:
jq '.[] | select(.email | test("@example\\.com$"))'

# Negation:
jq '.[] | select(.age >= 18) | select(.country != "blocked_country")'

Трансформация: map, объекты, конструкторы

# Удвоить все числа:
echo '[1,2,3]' | jq '[.[] * 2]'
# [2, 4, 6]

# map() — синтаксический сахар:
echo '[1,2,3]' | jq 'map(. * 2)'
# [2, 4, 6]

# Реструктурировать объекты:
users='[{"first":"alice","last":"smith","age":30}]'
echo "$users" | jq '.[] | {name: (.first + " " + .last), age}'
# {"name": "alice smith", "age": 30}

Заметь: {name: ..., age} — короткий синтаксис. age без двоеточия — age: .age.

# Переименовать поле:
jq '.[] | {user_name: .name, user_age: .age}'

# Добавить поле:
jq '.[] | . + {tag: "production"}'

# Удалить поле:
jq '.[] | del(.password)'

DE-паттерн 1: API -> JSONL для downstream

API возвращает массив, downstream ожидает JSONL (one JSON object per line). Стандартный pattern data ingestion.

curl -s https://api.example.com/users \
  | jq -c '.users[]' \
  > users.jsonl

-c — compact (one line per output). .users[] — развернуть массив users в поток. Каждый объект уходит отдельной строкой. Результат:

{"id":1,"name":"alice"}
{"id":2,"name":"bob"}
{"id":3,"name":"carol"}

JSONL — золотой стандарт для:

  • Snowflake COPY — встроенный JSONL ingestion.
  • BigQuery load jobs--source_format=NEWLINE_DELIMITED_JSON.
  • Spark/DuckDBread_json понимает JSONL быстрее, чем nested arrays.
  • Streaming: можно обрабатывать построчно, не грузя в память весь массив.

DE-паттерн 2: агрегация (group-by)

events='[
  {"type":"click","user":"alice"},
  {"type":"view","user":"bob"},
  {"type":"click","user":"alice"},
  {"type":"click","user":"carol"}
]'

# Count by type:
echo "$events" | jq 'group_by(.type) | map({type: .[0].type, count: length})'
# [
#   {"type":"click","count":3},
#   {"type":"view","count":1}
# ]
jq vs SQL для group-by

jq может многое, что SQL делает запросами.

SQLSELECT type, COUNT(*) FROM events GROUP BY type
jqgroup_by(.type) | map({type: .[0].type, count: length})
SELECTselect() в jq отвечает за WHERE; map() — за SELECT projection
GROUP BYgroup_by создаёт массив массивов, по одному на группу
ORDER BYsort_by(.field) или sort_by(.field) | reverse для DESC

DE-паттерн 3: csv export

# JSON -> CSV для Excel/manual review:
echo '[{"name":"alice","age":30},{"name":"bob","age":25}]' \
  | jq -r '.[] | [.name, .age] | @csv'
# "alice",30
# "bob",25

# С заголовком:
echo '[{"name":"alice","age":30},{"name":"bob","age":25}]' \
  | jq -r '["name","age"], (.[] | [.name, .age]) | @csv'
# "name","age"
# "alice",30
# "bob",25

@csv — встроенный formatter. Экранирует кавычки, обрамляет strings. Аналогично есть @tsv, @uri, @html, @sh.

@sh особенно полезен для безопасного передачи jq-значений в bash:

# Безопасный путь — jq экранирует strings для shell:
eval "args=( $(jq -r '.[] | @sh' < items.json) )"

# Теперь args — массив bash, корректно с любыми символами в значениях

DE-паттерн 4: large-file stream processing

Для больших файлов (1 GB JSON-логов) не загружай в память — стримь:

# Streaming mode — последовательная обработка без памяти на весь файл:
jq --stream -c 'select(length==2)' huge_logs.json

--stream режим выдаёт path-value пары, что позволяет процессить multi-gigabyte файлы. Сложнее в синтаксисе, но необходим для больших данных.

Простая альтернатива — если файл уже JSONL (каждая запись на отдельной строке):

# Обработать построчно:
cat huge.jsonl | jq -c 'select(.status == "error")' > errors.jsonl

jq читает построчно, обрабатывает каждый JSON независимо. Память — на одну строку.

TIP

Если работаешь с большими файлами и jq оказывается медленным, посмотри на jaq (Rust-based) или gojq (Go) — drop-in replacement для jq, в 2-5 раз быстрее на больших файлах. Синтаксис тот же.


Реальный DE-script с jq

#!/bin/bash
set -euo pipefail

# Fetch users, filter active, export to JSONL with normalized schema
curl --fail --max-time 30 -H "Authorization: Bearer $API_TOKEN" \
     https://api.example.com/users \
| jq -c '.users[]
         | select(.status == "active")
         | {
             id,
             email: (.email | ascii_downcase),
             name: (.first_name + " " + .last_name),
             created_at,
             plan: (.plan // "free")
         }' \
> users_$(date +%Y%m%d).jsonl

wc -l users_*.jsonl   # сколько записей

Что тут происходит:

  1. curl --fail — упасть на HTTP 4xx/5xx.
  2. .users[] — развернуть массив.
  3. select(.status == "active") — фильтр.
  4. {...} — нормализованный output: id, email (lowercase), name (concat), created_at (as is), plan (с default “free”).
  5. -c — compact (один объект на строку, JSONL).

Один pipeline — fetch + parse + filter + transform + save. На Python было бы 30+ строк.


yq: то же самое для YAML

yq от Mike Farah — Go-based, отдельный язык (НЕ jq-compatible несмотря на похожий синтаксис).

# Pretty-print:
yq '.' config.yaml

# Field access:
yq '.database.host' config.yaml

# Изменить значение (in-place):
yq -i '.database.host = "newhost.example.com"' config.yaml

# Добавить элемент в массив:
yq -i '.users += [{"name": "alice", "role": "admin"}]' config.yaml

# Конвертация YAML <-> JSON:
yq -o json config.yaml > config.json
yq -p json -o yaml config.json > config.yaml
yq vs jq syntax
.keyДоступ к полю — одинаковый
.[]Iterator массива — одинаковый
select()Фильтрация — одинаковая
yq -iIn-place edit — yq feature. jq не поддерживает (нужно > tmp + mv)
yq -o jsonOutput format conversion — встроено в yq. С jq нужно через дополнительные инструменты
mergeyq merge: `yq '. * load('override.yaml')' base.yaml`. Удобно для config-overlays
kubectl и декларативный API: JSON/YAML манифесты HTTP API: структура запроса и ответа

Real DE use case: edit Kubernetes manifest in CI

# Обновить image tag в deployment.yaml перед apply:
yq -i ".spec.template.spec.containers[0].image = \"myimage:${BUILD_ID}\"" deployment.yaml

# Применить:
kubectl apply -f deployment.yaml

Без yq это было бы sed-magic с регулярками, что хрупко и страшно.


Tips & gotchas

1. Кавычки внутри jq filter в bash

Если jq-filter содержит double quotes, используй single quotes снаружи:

# OK:
jq '.users[] | select(.role == "admin")'

# Сломается:
jq ".users[] | select(.role == \"admin\")"   # экранирование нужно, hell

2. null vs missing vs empty

echo '{}' | jq '.missing'       # null
echo '{"x":null}' | jq '.x'     # null

# Различение через has():
echo '{}' | jq 'has("x")'           # false
echo '{"x":null}' | jq 'has("x")'   # true

# Очистка nulls:
jq 'del(..|nulls)'

3. -e flag — strict exit codes

# Без -e: jq всегда возвращает 0 если выражение валидно
jq '.missing' < data.json   # вывод null, exit 0

# С -e: exit 1 если результат null/false
jq -e '.users | length > 0' < data.json
if [ $? -ne 0 ]; then echo "No users!"; fi

jq -e критичен для validation в скриптах: позволяет fail loudly если данные не такие, как ожидалось.

4. Сравнение типов

# Числа vs строки — разные типы:
echo '{"x":"5"}' | jq '.x > 3'   # error! string vs number

# Конверсия:
echo '{"x":"5"}' | jq '(.x | tonumber) > 3'   # true

  • Модуль 11 (networking) — curl + jq — основа любого API ingestion.
  • Урок 02 (fzf) — fuzzy search для JSON-логов часто комбинируется с jq.
  • Модуль 17 (advanced scripting) — production-paterns для скриптов с jq.

Попробуй сам

  1. Скачай GitHub users API ответ и поэкспериментируй:
curl -s https://api.github.com/users/torvalds | jq '.'
curl -s https://api.github.com/users/torvalds | jq '.public_repos'
curl -s https://api.github.com/users/torvalds | jq -r '.login, .name, .public_repos'
  1. Получи список репозиториев пользователя и выведи только имена:
curl -s https://api.github.com/users/torvalds/repos | jq -r '.[].name' | head -20
  1. Сделай JSONL из массива:
curl -s https://api.github.com/users/torvalds/repos | jq -c '.[]' > repos.jsonl
wc -l repos.jsonl
  1. Фильтр + projection:
curl -s https://api.github.com/users/torvalds/repos \
  | jq -r '.[] | select(.stargazers_count > 100) | "\(.name): \(.stargazers_count)"' \
  | sort -t: -k2 -n -r
  1. YAML edit:
cat > test.yaml <<EOF
version: 1.0
services:
  api:
    image: myapi:old
    replicas: 3
EOF

yq '.services.api.image' test.yaml
yq -i '.services.api.image = "myapi:new"' test.yaml
cat test.yaml

Проверка знанийKnowledge check
Junior хочет: 1) скачать данные из API, 2) отфильтровать только записи с status=active, 3) для каждой извлечь поля id, email (в lowercase) и name, 4) сохранить как JSONL для загрузки в Snowflake. Какой jq-pipeline решает это и почему именно JSONL для Snowflake?
ОтветAnswer
Pipeline:\nbash\ncurl -s -H \"Authorization: Bearer \$TOKEN\" https://api.example.com/users \\n | jq -c '.users[] | select(.status==\"active\") | {id, email: (.email | ascii_downcase), name}' \\n > users.jsonl\n\nРазбор: -c — compact output (одна строка на объект). .users[] — развернуть массив. select() — фильтр (как SQL WHERE). {id, email: (...), name} — projection (объект-constructor). ascii_downcase — встроенная функция jq для lowercase. Почему JSONL для Snowflake: 1) Snowflake COPY INTO ... FILE_FORMAT = (TYPE = JSON) нативно поддерживает JSONL (newline-delimited). 2) Streaming: Snowflake может параллельно загружать строки в multiple workers — массив пришлось бы парсить целиком. 3) Большие массивы (100GB+ JSON) не помещаются в память для single-pass parsers, JSONL обрабатывается построчно. 4) Universal: BigQuery, Redshift, DuckDB, Spark — все знают JSONL. Аналогичный подход применим для BigQuery (--source_format=NEWLINE_DELIMITED_JSON) и Kafka producers. Для production: добавить --fail к curl, -e к jq (strict mode), валидацию через jq -e 'length > 0'.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В чём разница между `jq '.name'` и `jq -r '.name'` при выводе строкового значения?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5