Всем привет! Язык разметки 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 встроенных командлетов, а для конвертации данных из одного формата в другой не требуется изучение самого язык.
Примеры по работе с этими и другими командами вы можете найти в моем репозитории на 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
Как видно из примера на скриншоте, после обработки входных данных командой 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
.
В данном случае три хоста из разных уголков земли отправили по 4 пакета с помощью команды ping
на указанный нами хост в запросе. Если необходимо произвести проверку другого протокола, то для удобства в скрипте выше я добавил две переменные, где можно указать другой протокол (http
, tcp
, udp
или dns
). Например, проверим доступность TCP
порта 443
:
host="yandex.ru:443"
protocol="tcp" # udp/http/dns
Как видно из примера, хост 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
.
Также, существует и несколько других утилит, например, 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
:
По мимо конвертации, данный инструмент предоставляет очень много возможностей для работы со строкой, например, возможно кодировать и декодировать HEX
или Base64
, менять регистр, сортировать, извлекать уникальные строки и многое другое.
Если вам требуется проверить синтаксис входных данных в формате JSON
, можете воспользоваться достаточно старым инструментом jsonlint, который по сегодняшний день отлично с этим справляется. Для работы данного инструмента потребуется установить в системе Node.js
и с помощью менеджера пакетов npm
установить данную утилиту:
apt-get install -y nodejs
npm install jsonlint -g
Утилита подскажет, где была допущена ошибка, а в случае корректного чтения входных данных, выведет содержимое JSON на экран:
Также возможно использовать встроенные возможности большинства IDE, например, VSCode, который способен показать, где именно у вас допущена ошибка и форматировать документ.
Итог
Хотя это далеко не все инструменты и на просторах GitHub можно найти несколько десятков, достаточно выбрать один, который будет удовлетворять вашим потребностям и привыкнуть к его синтаксису. Также существует много источников с заметками по разным утилитам Linux, например, manned, где присутствует документация для jq, и за время практики у меня тоже их накопилось немало, по этому решил опубликовать их в репозитории на GitHub до кучи к заметкам по PowerShell (предварительно оформив документ в формате Markdown), а также добавил в веб-версию, где присутствует удобный поисковик. Так как многие команды и ключи забывается когда с ними регулярно не работаешь, по этому такие заметки я части использую с рабочего компьютера, и меня такой подход выручает.