Памятка по работе с JSON в консоли Linux на примере api

от автора

Всем привет! Язык разметки JSON (что такое JSON отлично подано в другой статье на Хабр) используется в огромном количестве приложений и систем благодаря своей простоте (например, Docker использует его для описания конфигурации контейнеров), а также является стандартным форматом обмена данными между клиентом и сервером в RESTful API. В зависимости от платформы сервера, исходные данные могут иметь любой формат, а перед отправкой конвертироваться в JSON, или другой, который будет запрошен клиентом, и если это поддерживается на стороне сервера.

Ранее уже писал статью, как на базе PowerShell можно создать Web/API сервер, где наглядно можно посмотреть, как реализуется механизм конвертации данных на основе используемого агента и заголовков запроса, полученных от клиента.

Многие приложения и системы предоставляют интерфейс удаленного управления API, который может использоваться как для получения информации (например, для передачи данных в систему мониторинга), так и для управления этой системой через другое приложение или скрипты, которые чаще всего настраивают системные администраторы или специалисты в области DevOps.

Как дела в Windows

Большим преимуществом PowerShell (который по умолчанию используется в операционной системе Windows) перед интерпретатором Bash, это возможно конвертировать данные различных форматов без необходимости устанавливать дополнительные утилиты или библиотеки, а благодаря своему единому подходу к написанию скриптов, возможно фильтровать и управлять полученными данными с помощью объектной модели. Вот пример, который выведет запущенные процессы с фильтрацией по ключевому слову torrent в формате JSON:

$json = Get-Process *torrent* | Select-Object name,ws,cpu | ConvertTo-Json $json [   {     "Name": "qbittorrent",     "WS": 57962496,     "CPU": 12243.375   },   {     "Name": "WebTorrent",     "WS": 116465664,     "CPU": 0.515625   } ]

Если необходимо обработать данные, изначально полученные в формате JSON, используется команда ConvertFrom-Json . Полученные данные обрабатываются типовыми командами данного языка, и если это необходимо, конвертируются обратно в любой поддерживаемый формат:

$json | ConvertFrom-Json | Where-Object cpu -gt 1 | ConvertTo-Json {   "Name": "qbittorrent",   "WS": 57962496,   "CPU": 12243.375 }

Для поддержки остальных языков разметки можно установить соответствующий модуль с помощью команды Install-Module (например, PSYaml или PSToml), которые будет иметь точно такой же синтаксис.

Благодаря кроссплатформенной версии PowerShell Core, вы можете установить и использовать данный интерпретатор в системе Linux. Например, последняя версия 7.4.3 насчитывает 300 встроенных командлетов, а для конвертации данных из одного формата в другой не требуется изучение самого язык.

Список поддерживаемых команд по умолчанию, для конвертации данных в PowerShell

Список поддерживаемых команд по умолчанию, для конвертации данных в PowerShell

Примеры по работе с этими и другими командами вы можете найти в моем репозитории на GitHub. Если вы не хотите устанавливать и вызывать другой интерпретатор, а использовать только встроенный (чаще всего это Bash), потребуется установить в систему специальную утилиту, которая реализует данный механизм.

jqlang/jq

Самым популярным инструментом для обработки JSON данных является jq, который написан на языке C. Можно сказать, что это не просто утилита, а язык запросов, который позволяет выполнять сложные операции по извлечению, фильтрации и преобразованию данных.

Для установки в системе Ubuntu можно воспользоваться встроенным менеджером пакетов:

sudo apt install jq

Для начала определимся с шаблоном данных, который будет использоваться в дальнейшем. Для примера я возьму онлайн-инструмент Check-Host, который применяется для проверки доступности веб-сайтов и хостов, а главное, данный сервис предоставляет бесплатный API, так что попутно разберемся, как с ним работать.

Для начала, получим список доступных адресов нод (nodes), с которых возможно будет производить проверки любых хостов в Интернете, и передадим полученный вывод в команду jq:

nodes=$(curl -s -H "Accept: application/json" https://check-host.net/nodes/ips) echo $nodes | jq
Список нод Check-Host до и после обработки с помощью jq

Список нод Check-Host до и после обработки с помощью jq

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

Если необходимо получить количество дочерних элементов выбранного блока (в примере, nodes), используется функция length через pipe (|) внутри запроса, который заключен в кавычки:

echo $nodes | jq '.nodes | length' 43

Если обратиться к дочерним элементам блока .nodes, используя в запросе .nodes[], то получим массив цифр, каждая из которых будет составлять длину элемента в массиве (количество букв и символов в строке). Тем самым, обращаясь к дочерним элементам по имени, мы фильтруем полученный вывод, например, что бы получить содержимое выбранного элемента в массиве nodes[], обратимся к нему по его порядковому номеру индекса (отчет начинается с 0):

echo $nodes | jq .nodes[0] "185.143.223.66"  echo $nodes | jq .nodes[1] "38.145.202.12"  echo $nodes | jq -r .nodes[1] 38.145.202.12  echo $nodes | jq -r .nodes[-1] 65.109.182.130

Ключ -r используется для вывода в формате строки (raw string).

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

hosts=$(curl -s -H "Accept: application/json" https://check-host.net/nodes/hosts)

Теперь, при обращении к блоку .nodes вместо ip-адресов, мы получим имена хостов, для каждого из которых есть вложенные элементы. Так как .nodes теперь не является массивом, а представляет из себя формат объекта, где каждый отдельный элемент это набор пар: ключ-значение. Что бы получить список этих ключей, используем функцию to_entries[] и обращаемся к свойству key :

echo $hosts | jq -r '.nodes | to_entries[].key' bg1.node.check-host.net br1.node.check-host.net ch1.node.check-host.net ...  echo $hosts | jq -r '.nodes | to_entries[0].key' bg1.node.check-host.net

Если необходимо получить только значение, обращаемся к свойству value:

echo $hosts | jq -r '.nodes | to_entries[0].value' {   "asn": "AS9028",   "ip": "93.123.16.89",   "location": [     "bg",     "Bulgaria",     "Sofia"   ] }

Тем самым каждый элемент объекта содержит 3 свойства, одно из которых является массивом (location). Такой же результат можно получить, обратившись на прямую к элементу объекта по имени ключа:

echo $hosts | jq '.nodes."bg1.node.check-host.net"' {   "asn": "AS9028",   "ip": "93.123.16.89",   "location": [     "bg",     "Bulgaria",     "Sofia"   ] }

Что бы получить все значения из последнего объекта, сначала преобразуем отдельные объекты внутри nodes в массив, и передаем полученный вывод в функцию last:

echo $hosts | jq '.nodes | [.[]] | last' {   "asn": "AS207713",   "ip": "185.143.223.66",   "location": [     "us",     "USA",     "Atlanta"   ] }

Можно произвести проверку содержимого элементов, например, первое значение массива location содержит код страны в формате ISO 3166.

echo $hosts | jq '.nodes | to_entries[].value.location[0] == "ru"'

Такая конструкция вернет результат для каждого элемента в виде массива строк, в каждой из которых будет true, если содержимое элемента равно ru или false, если условие ложно.

Теперь произведем выборку данных. Вначале заберем все ключи и значения объектов из элемента .nodes, затем передадим значения дочерних элементов в ключи с новыми названиями, тем самым пересоберем массив объектов:

echo $hosts | jq '.nodes | to_entries[] | {   Host: .key,   Country: .value.location[1],   City: .value.location[2] }'  {   "Host": "bg1.node.check-host.net",   "Country": "Bulgaria",   "City": "Sofia" } {   "Host": "br1.node.check-host.net",   "Country": "Brazil",   "City": "Sao Paulo" } {   "Host": "ch1.node.check-host.net",   "Country": "Switzerland",   "City": "Zurich" } ...

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

echo $hosts | jq -r '.nodes | to_entries[] | "\(.key) (\(.value.location[1]), \(.value.location[2]))"' bg1.node.check-host.net (Bulgaria, Sofia) br1.node.check-host.net (Brazil, Sao Paulo) ch1.node.check-host.net (Switzerland, Zurich) ...

Что бы передать внешнюю переменную, которая будет использоваться внутри запроса, используется параметр --arg:

echo $hosts | jq --arg v "$var" -r '.nodes | to_entries[] | "\(.key) \($v) \(.value.location[1]) \($v) \(.value.location[2])"' bg1.node.check-host.net - Bulgaria - Sofia br1.node.check-host.net - Brazil - Sao Paulo ch1.node.check-host.net - Switzerland - Zurich ...

Для того, что бы получить только нужные объекты, необходимо произвести фильтрацию с помощью select . Например, выведем список хостов, которые в первом значение массива location содержат ключевое слово ru:

echo $hosts | jq -r '.nodes | to_entries[] | select(.value.location[0] == "ru") | .key' ru1.node.check-host.net ru2.node.check-host.net ru3.node.check-host.net ru4.node.check-host.net

Всего имеется 4 ноды в регионе ru. Произведем обратный процесс, отфильтруем объекты, которые не содержат (!=) указанное значение, и получим общее количество найденных элементов:

echo $hosts | jq '.nodes | length' 43 echo $hosts | jq '.nodes | to_entries | map(select(.value.location[0] != "ru")) | length' 39

Функция map() создает массив только из тех объектов, которые соответствуют условию, тем самым исходные отдельные объекты формата {} {} группируются в один массив формата [{},{}].

Возможно также использовать сразу несколько условий с помощью and и or :

echo $hosts | jq -r '.nodes | to_entries[] | select(.value.location[0] == "ru" or .value.location[0] == "tr") | .key' ru1.node.check-host.net ru2.node.check-host.net ru3.node.check-host.net ru4.node.check-host.net tr1.node.check-host.net tr2.node.check-host.net

Важный момент, если мы проверяем тип данных string (строка), которая по умолчанию заключена в кавычки, то мы их используем и в ключевом слове (как в примере выше, "ru" или "tr"), но, если значение содержит тип int (целое число), то кавычки применяться не будут, это также можно увидеть в исходном выводе синтаксиса JSON:

echo '{"type_string": "string", "type_int": 123}' | jq {   "type_string": "string",   "type_int": 123 }

В примере, содержимое элемента type_int не содержит кавычек.

Если же необходимо отфильтровать объекты по частичному совпадению содержимого, то искомое слово помещается в функцию index() . Например, выведем список хостов, которые в названии ключа содержат ключевое слово jp (регион Japan):

echo $hosts | jq -r '.nodes | to_entries[] | select(.key | index("jp")) | .key' jp1.node.check-host.net

Теперь произведем icmp проверку любого публичного ресурса в Интернете. Передаем в url запроса параметры адрес хоста host=yandex.ru и количество нод max_nodes=3, которые будут использоваться для проверки. Первым запросом мы запускаем проверку и забираем ее id c помощью jq, после чего по этому id получаем результат проверки.

host="yandex.ru" protocol="ping" # Забрать id для получения результатов check_id=$(curl -s -H "Accept: application/json" "https://check-host.net/check-$protocol?host=$host&max_nodes=3" | jq -r .request_id) # Функция получения результатов проверки по id function check-result {     curl -s -H "Accept: application/json" https://check-host.net/check-result/$1 | jq . } # Получить суммарное количество хостов, с которых производится проверка hosts_length=$(check-result $check_id | jq length) while true; do     check_result=$(check-result $check_id)     # Забираем результат и проверем, что содержимое всех проверок не равны null     check_values_not_null=$(echo $check_result | jq -e 'to_entries | map(select(.value != null)) | length')     if [[ $check_values_not_null == $hosts_length ]]; then         echo $check_result | jq         break     fi     sleep 1 done

Так как проверка занимает какое-то время, если сразу попытаться получить результат, нам вернется только список нод с которых запущена проверка, но их значения будут равны null. По этому, вначале фиксируем суммарное количество нод (хотя это делать не обязательно, так как мы задаем его в параметре), и в цикле while сопоставляем это количество (hosts_length) с тем количеством, у которых вывод (.value) не равен null.

Результат проверки хоста yandex.ru с помощью трех нод

Результат проверки хоста yandex.ru с помощью трех нод

В данном случае три хоста из разных уголков земли отправили по 4 пакета с помощью команды ping на указанный нами хост в запросе. Если необходимо произвести проверку другого протокола, то для удобства в скрипте выше я добавил две переменные, где можно указать другой протокол (http, tcp, udp или dns). Например, проверим доступность TCP порта 443 :

host="yandex.ru:443" protocol="tcp" # udp/http/dns
Результат проверки TCP порта 443

Результат проверки TCP порта 443

Как видно из примера, хост ua2.node.check-host.net не смог получить доступ к указанному порту, вернув ошибку:Connection refused.

Также возможно указать, с каких именно хостов (из разных регионов) производить проверку, используя соответствующий параметр, например, добавить в конец url-запроса: &node=ru4.node.check-host.net. Полную версию скрипта с обработкой входных параметров для Bash и PowerShell вы можете найти в исходном репозитории на GitHub. Далее, используя утилиту jq можно обработать эти данные, и например, настроить вывод в систему мониторинга.

Функций у данной утилиты достаточно много, например, с помощью следующей конструкции можно получить ГБ из байт и округлить вывод до 2 символом после запятой:

echo '{ "iso":     [         {"name": "Ubuntu", "size": 4253212899},         {"name": "Debian", "size": 3221225472}     ] }' | jq '.iso[] | {name: .name, size: (.size / 1024 / 1024 / 1024 | tonumber * 100 | floor / 100 | tostring + " GB")}'  {   "name": "Ubuntu",   "size": "3.96 GB" } {   "name": "Debian",   "size": "3 GB" } 

Или получить процент из дробной чисти:

echo '{ "iso": [ { "name": "Ubuntu", "progress": 0.333 } ] }' | jq '.iso[] | {name: .name, progress: (.progress * 100 | floor / 100 * 100 | tostring + " %")}'  {   "name": "Ubuntu",   "progress": "33 %" } 

Хотя возможностей у данного языка запросов очень много, приведенных мною пример будет достаточно для решения большинства задач. Хочу заметить, так как JSON представляет из себя объектную модель, в связке с Bash способность хранить и обрабатывать данные очень сильно расширяют возможности при написании скриптов, даже если вы не работаете с API, а главное, такой подход куда надежнее по сравнению с классической обработкой строк.

Другие инструменты

У jq существует много поклонников, по мимо обширного сообщества которое принимают участие в разработке, исправлении багов, а также оказывают поддержку в разделе issues на GitHub, присутствуют и интерфейсы командной строки, которые упрощают работу с данной утилитой, например jid и jqp.

Первый инструмент позволяет интерактивно взаимодействовать с данными, где помимо автоматической подстановки значений с помощью кнопки Tab, вы можете получить список всех ключей в формате выпадающего списка и переключаться между ними стрелочками, при этом сразу наблюдая результат на экране. Хотя инструмент предназначен в первую очередь для удобного и быстрого просмотра JSON данных в вашей консоли, такие запросы в дальнейшем можно применять в jq. Второй инструмент похож на первый, но использует две панели, в левой отображается исходный документ, а в правой отфильтрованный с помощью введённого вами запроса, который можно скопировать с помощью комбинации клавиш Ctrl+Y .

Интерактивное взаимодействие с jq через jqp

Интерактивное взаимодействие с jq через jqp

Также, существует и несколько других утилит, например, dasel, который написан на современном и быстром GoLang. По мимо обработки JSON, он также поддерживает YAML, TOML, XML и CSV. Базовый синтаксис при обращении к элементам объектов очень похож на jq, и имеет единый формат для всех языков разметки.

curl -s https://check-host.net/nodes/ips | dasel -r json '.nodes.[0]' "185.86.77.126"

По мимо этого, dasel способен конвертировать данные между языками:

# Конвертируем JSON в YAML echo '{"name": "Tom"}' | dasel -r json -w yaml name: Tom  # Конвертируем JSON в MXL echo '{"name": "Tom"}' | dasel -r json -w xml <name>Tom</name>

Для конвертации данных из одного формата в другой еще можно воспользоваться утилитой sttr. Пример для YAML :

Конвертация JSON в YAML и наоборот.

Конвертация JSON в YAML и наоборот.

По мимо конвертации, данный инструмент предоставляет очень много возможностей для работы со строкой, например, возможно кодировать и декодировать HEX или Base64, менять регистр, сортировать, извлекать уникальные строки и многое другое.

Если вам требуется проверить синтаксис входных данных в формате JSON, можете воспользоваться достаточно старым инструментом jsonlint, который по сегодняшний день отлично с этим справляется. Для работы данного инструмента потребуется установить в системе Node.js и с помощью менеджера пакетов npm установить данную утилиту:

apt-get install -y nodejs npm install jsonlint -g

Утилита подскажет, где была допущена ошибка, а в случае корректного чтения входных данных, выведет содержимое JSON на экран:

Работа с jsonlint

Работа с jsonlint

Также возможно использовать встроенные возможности большинства IDE, например, VSCode, который способен показать, где именно у вас допущена ошибка и форматировать документ.

VSCode подсказывает, что в 12 строке пропущена запятая

VSCode подсказывает, что в 12 строке пропущена запятая

Итог

Хотя это далеко не все инструменты и на просторах GitHub можно найти несколько десятков, достаточно выбрать один, который будет удовлетворять вашим потребностям и привыкнуть к его синтаксису. Также существует много источников с заметками по разным утилитам Linux, например, manned, где присутствует документация для jq, и за время практики у меня тоже их накопилось немало, по этому решил опубликовать их в репозитории на GitHub до кучи к заметкам по PowerShell (предварительно оформив документ в формате Markdown), а также добавил в веб-версию, где присутствует удобный поисковик. Так как многие команды и ключи забывается когда с ними регулярно не работаешь, по этому такие заметки я части использую с рабочего компьютера, и меня такой подход выручает.


ссылка на оригинал статьи https://habr.com/ru/articles/827762/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *