...на Kotlin разумеется. В комментариях к предыдущей статье было задано несколько вопросов, как сделать конвертацию в tsv, почему утилита собрана в Docker образ и предложение использовать нативный образ GraalVM.
В этой статье содержится ответ на них и заодно рассказывается о последнем обновлении функций утилиты. Кто по работе часто занимается процессингом JSON - добро пожаловать под кат.
Первое о чем хотелось бы рассказать, так это о полной поддержке Kotlin в последнем обновлении утилиты. Выглядит это так:
cat example.ndjson | analyze '
.filter{it.bool("active")}.sortedByDescending{it.long("size")}.take(3)
'
# Увидим что-то вроде
{"id":488013,"size":342,"active":true}
{"id":239636,"size":264,"active":true}
{"id":306804,"size":115,"active":true}
Повторяем за jq, или как делать преобразования
В качестве цепочки выражений можно использовать любые расширения последовательностей.
Например, можно использовать их чтобы получить преобразование из предыдущего примера:
cat example.ndjson | analyze '
.filter{it.bool("active")}.sortedByDescending{it.long("size")}.map{"id=${it.int("id")} and size=${it.int("size")}"}.take(3)
'
# Увидим что-то вроде
id=488013 and size=342
id=239636 and size=264
id=306804 and size=115
Если кого-то смущает типизация в этих примерах, то напомню что вместо любого типа можно использовать .obj("name")
чтобы получить сырое значение из json как оно есть.
Кроме этого, мы все еще можем использовать dsl запросов из первой статьи. С ним наш предыдущий пример будет выглядеть проще:
cat example.ndjson | analyze '
where{bool("active")} max{long("size")} select{int("id") + "\t" + int("size")} top 3
'
# В этот раз для разнообразия сконвертировали результат в TSV
488013 342
239636 264
306804 115
Таким образом, любые преобразования доступны внутри выражений .map{ } и select{ }
Как установить, или почему не используется GraalVM
Проведя эксперименты, убедился что интструмент native-image из GraalVM все еще не готов к использованию: при сборке потерялась часть runtime зависимостей и утилита перестала обнаруживать нужный для интерпретации запросов класс. Поэтому тем кто предпочитает использовать одну команду вместо docker контейнера могу предложить следующее решение:
Нам потребуется jvm16+
jar файл утилиты который можно скачать здесь
Пара строчек в терминале и получаем команду
analyze:
mv analyze.jar /usr/local/bin/analyze.jar
echo "java -jar /usr/local/bin/analyze.jar \$@" > /usr/local/bin/analyze
chmod a+x /usr/local/bin/analyze
Последняя версия также доступна и в Docker, кроме него никаких зависимостей не требуется, эту команду без установки можно использовать в любой директории, а ресурсы будут освобождены после исполнения:
docker run -v `pwd`:`pwd` -w `pwd` -it --rm demidko/analyze example.ndjson 'where{bool("active")} max{long("size")} top 10'
Технические решения
Благодаря использованию стандартных последовательностей Kotlin вместо самописных, стало возможным сократить весь код утилиты до одного файла из (!) 41 строки не считая импортов.
Теперь в коде все запросы просто конкатенируются с последовательностью строк: lines.map(json::readTree) $query
и отправляется на исполнение.
Просто? Даже очень. Однако это работает лучше предыдущего варианта и возможно еще эффективнее за счет внутренних оптимизаций Kotlin sequences.
Новая документация
Любой запрос состоит из набора инструкций и выражений расположенных внутри. В общем виде:
# Вызвать утилиту можно так
analyze file 'instruction { expression } nextInstruction { expression } ...'
# Или так
cat file | analyze 'instruction { expression } nextInstruction { expression } ...'
В качестве инструкций нам доступно все что есть в стандартной библиотеке Kotlin sequences. Это такие инструкции как filter, map, reduce, take, drop, и другие. Они должны следовать друг за другом начинаясь с точки как в примерах выше. Внутри таких инструкций, в выражении, текущий объект доступен под именем it
Однако дополнительно определены следующие запросы, для которых ставить точку вначале необязательно:
/* Псевдоним для filter */
where { /* expression */ }
/* Сортировка выражением по возрастанию */
min { /* expression */ }
/* Сортировка выражением по убыванию */
max { /* expression */ }
/* Взять первые n элементов */
top(n)
/* Псевдоним для map */
select { /* expression */ }
Внутри этих последовательностей в качестве выражения мы по прежнему используем Kotlin, однако текущий объект в них будет доступен через ключевое слово this
, которое, в отличии от it можно просто опускать и не писать. Также, в этих выражениях, нам доступны следующие функции:
/* Возвращает любой вложенный объект */
obj(name: String) // можно получить по имени
obj(idx: Int) // а можно по индексу
/* Логическое значение */
bool(name: String)
bool(idx: Int)
/* Целое число */
int(name: String)
int(idx: Int)
/* Число с плавающей точкой */
double(name: String)
double(idx: Int)
/* Текст */
text(name: String)
text(idx: Int)
/* Объект LocalDateTime для времени */
time(name: String)
time(idx: Int)
Обязательно ли использовать типизацию? Нет. Однако она дает нам возможность использовать определенные для соответситвующих типов функции java, например, для временых меток возможно использовать следующее выражение
max { between(time("from"), time("to")) } top 3
Здесь мы получили топ 3 самых длительных промежутков времени из ndjson. Без дополнений которые нам дает типизация это было бы почти невозможно (попробуйте провернуть в jq).
Обработка сложных объектов
Например, у нас есть ndjson с вложенными объектами, пример такой записи развернуто:
{
"id": 4,
"name": "Vasya Ivanov",
"skills": [
{"name": "бухать", "level": 80},
{"name": "смотреть телевизор", "level":95},
{"name": "общаться за жизнь", "level": 25},
{"name": "решать диффуры", "level": 2}
]
}
Например, нам нужен топ по навыкам Ивановых в tsv. Нет ничего проще:
where {
text("name").endWith("Ivanov") // получили Ивановых
} select {
obj("skills") // преобразовали в наборы навыков
} max {
int("level") // отсортировали по убыванию
} top 3 { // взяли из навыков топ 3
obj("name") + "\t" + obj("level") // сохранили в tsv, тут типы не нужны
}
## В результате что-то вроде
смотреть телевизор 95
бухать 80
И да простят меня все Ивановы за этот надуманный пример.
Больше примеров
Что насчет использования из командой строки? Не будет ли сложнее jq? Судите сами:
analyze file.ndsjon 'max {long("size")} top 3 where {bool("active")}'
cat file.ndjson | analyze 'top 5 where{!bool("active")} min{int("some")}'
analyze file.ndsjon 'top 5 min{between(time("first"), time("last"))}'
cat file.ndjson | analyze 'where { obj("arr").int(0) > 5 }'
analyze file.ndsjon 'where{!bool("broken")} top 3 min{ obj(4).obj("nested").bool("flag") }'
Исходный код
Репозиторий доступен на GitHub: https://github.com/demidko/analyze
Есть страничка релизов: https://github.com/demidko/analyze/releases
Есть контейнер с утилитой на DockerHub: https://hub.docker.com/r/demidko/analyze
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Полезная ли получилась штука?
-
100,0%Да1
-
0,0%Нет0