[Программирование, Функциональное программирование, Визуализация данных, Kotlin, Разработка под Linux] Продолжаем обрабатывать NDJSON

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
04-Июл-2021 04:31

...на 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
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_funktsionalnoe_programmirovanie (Функциональное программирование), #_vizualizatsija_dannyh (Визуализация данных), #_kotlin, #_razrabotka_pod_linux (Разработка под Linux), #_kotlin, #_jvm, #_json, #_jq, #_ndjson, #_programmirovanie (
Программирование
)
, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
, #_vizualizatsija_dannyh (
Визуализация данных
)
, #_kotlin, #_razrabotka_pod_linux (
Разработка под Linux
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 15-Май 02:10
Часовой пояс: UTC + 5