UNPKG

jl

Version:

Command-line JSON manipulation library

291 lines (221 loc) 13.3 kB
node-jl ===== Набор утилит для работы с файлами/потоками, содержащими JSON-записи, разделённые символом перевода строки (`\n`). JSON-представление каждой записи не должно иметь переводов строки. Установка ----- ``` npm install -g jl ``` Примеры использования ----- Имеет лог-файл `/tmp/test.json`, содержащий такие записи ```json {"ts": 1416595508, "type": "click"} {"ts": 1416478467, "type": "buy", "price": 10} {"ts": 1416466930, "type": "click"} {"ts": 1416622653, "type": "buy", "price": 20} {"ts": 1416699396, "type": "click"} {"ts": 1416624334, "type": "click"} {"ts": 1416518859, "type": "click"} {"ts": 1416569870, "type": "click"} {"ts": 1416573325, "type": "click"} {"ts": 1416682270, "type": "click"} ``` ### SQL-интерфейс Утилита `jl-sql` поддерживает язык запросов, очень похожий на SQL ```sh cat /tmp/test.json \ | jl-sql 'SELECT type, COUNT(*) AS c, SUM(price) AS sum GROUP BY type ORDER BY c NUMERIC DESC' ``` ```json {"type":"click","c":8,"sum":0} {"type":"buy","c":2,"sum":30} ``` Как видим, на выходе получаем ровно то, что ожидаем. Обратите внимание на нестандартное ключевое слово `NUMERIC` в `ORDER BY`: без него сортировка будет идти не по числовому значению поля, а по его строковому представлению. #### Константы - `NULL` - `TRUE` - `FALSE` #### Операторы ##### Арифметические - `+` - `-` - `*` - `/` - `%` ##### Сравнения - `=`, `==` - `!=` - `===` - сравнение с учётом типов операндов - `!==` - отрицательное сравнение с учётом типов операндов - `>` - `<` - `>=` - `<=` ##### Логические - `AND`, `&&` - `OR`, `||` - `!` #### Функции ##### Функции для работы с датами - `FROM_UNIXTIME(unixTimestamp)` - конвертирует unix timestamp в дату - `UNIX_TIMESTAMP(date)` - конвертирует дату в unix timestamp, дата может быть строкой формата `'YYYY-MM-DD hh:mm:ss'` - `DATE(date)` - возвращает дату в формате `'YYYY-MM-DD'` ##### Разное - `IF(expression, ifTrue, ifFalse)` - `COALESCE(expression1[, expression2[, ...]])` ##### Агрегирующие На данный момент поддерживаются только самые базовые агрегирующие функции: - `SUM(expression)` - считает сумму значений поля. Нечисловые начения пропускаются - `MIN(expression)` - считает минимальное значение поля. Нечисловые начения пропускаются - `MAX(expression)` - считает максимальное значение поля. Нечисловые начения пропускаются - `COUNT(expression)` - считает количество элементов, значение `expression` для который !== null и !== undefined - `COUNT(*)` - считает количество элементов - `HLL_COUNT_DISTINCT(arg1[, args2[, ...]])` - считает количество уникальных комбинаций аргументов. Потреляет `O(1)` памяти и `O(N)` вычислительной сложности, но даёт результат со стандартной ошибку 0.1%. Для подробностей можно почитать про алгоритм HyperLogLog #### Ограничния - нет и, скорее всего, никогда не будет `JOIN` - не поддерживается сортировка по нескольким полям одновременно - не поддерживается `LIMIT`, но его легко заменить обычными `head` и `tail` ### Низкоуровневый интерфейс SQL-интерфейс является всего лишь надстройкой над набором утилит, описанных ниже. Комбинируя эти утилиты, можно делать намного больше, чем позволяет SQL. #### Фильтрация и агрегация Отфильтруем только события с типом `buy` ```sh cat /tmp/test.json \ | jl-filter 'type == "buy"' ``` ```json {"ts": 1416478467, "type": "buy", "price": 10} {"ts": 1416622653, "type": "buy", "price": 20} ``` Добавим подсчёт суммы всех `price` ```sh cat /tmp/test.json \ | jl-filter 'type == "buy"' | jl-sum price ``` ```json {"value":30} ``` Чтобы получить просто `30` можно добавить в конце вызов `jl-extract value` - эта команда извлекает строковое представление значения поля. #### Сортировка Отсортируем лог по полю `ts` ```sh cat /tmp/test.json \ | jl-sort ts ``` ```json {"ts": 1416466930, "type": "click"} {"ts": 1416478467, "type": "buy", "price": 10} {"ts": 1416518859, "type": "click"} {"ts": 1416569870, "type": "click"} {"ts": 1416573325, "type": "click"} {"ts": 1416595508, "type": "click"} {"ts": 1416622653, "type": "buy", "price": 20} {"ts": 1416624334, "type": "click"} {"ts": 1416682270, "type": "click"} {"ts": 1416699396, "type": "click"} ``` Также поддерживаются стандартные аргументы утилиты `sort`: `-r`, `-n`, `-u`, `-m`, `-s`, `-T`, `-S` #### Модификация Добавим в каждый объект поле `date`, содержащее дату события в UTC ```sh cat /tmp/test.json \ | jl-transform '{r.date = (new Date(r.ts * 1000)).toUTCString()}' ``` ```json {"ts":1416595508,"type":"click","date":"Fri, 21 Nov 2014 18:45:08 GMT"} {"ts":1416478467,"type":"buy","price":10,"date":"Thu, 20 Nov 2014 10:14:27 GMT"} {"ts":1416466930,"type":"click","date":"Thu, 20 Nov 2014 07:02:10 GMT"} {"ts":1416622653,"type":"buy","price":20,"date":"Sat, 22 Nov 2014 02:17:33 GMT"} {"ts":1416699396,"type":"click","date":"Sat, 22 Nov 2014 23:36:36 GMT"} {"ts":1416624334,"type":"click","date":"Sat, 22 Nov 2014 02:45:34 GMT"} {"ts":1416518859,"type":"click","date":"Thu, 20 Nov 2014 21:27:39 GMT"} {"ts":1416569870,"type":"click","date":"Fri, 21 Nov 2014 11:37:50 GMT"} {"ts":1416573325,"type":"click","date":"Fri, 21 Nov 2014 12:35:25 GMT"} {"ts":1416682270,"type":"click","date":"Sat, 22 Nov 2014 18:51:10 GMT"} ``` Внутренний конвейер ----- При использовании стандартных системных конвейеров (pipes) для взаимодействия между несколькими `jl-`утилитами появляется много накладных расходов на сериалицацию/десериализацию в/из JSON т.к. каждая утилита в отдельности принимает JSON из stdin и отдаёт JSON в stdout. Для решения этой проблемы все `jl-`утилиты поддерживают внутреннюю реализацию конвейеров, которые позволяют им взаимодействовать между собой в контексте одного процесса без затрат на парсинг. Делается это простой заменой `|` на `\|` между `jl-`командами в командной строке. Например ```sh cat /tmp/test.json \ | jl-filter 'type == "buy"' | jl-sum price ``` можно заменить на ```sh cat /tmp/test.json \ | jl-filter 'type == "buy"' \| jl-sum price ``` Производительность ----- Потребление CPU всех утилит кроме `jl-sort` - `O(n)`, по памяти `O(1)`. Сложность `jl-sort` целиком зависит от реализации системного `sort`, дополнительные расходы по CPU также `O(n)`, по памяти `O(1)`. Использование встроенного механизма конвейеров сильно увеличивает производительность т.к. парсинг JSON очень затратная операция. Но есть обратная сторона: все операции в пределах конвейера выполняются в одном потоке, поэтому при очен длинных конвейерах стоит соблюдать баланс. Утилиты ----- #### `jl-sql` - SQL Принимает один обязательный аргумент - SQL и имеет несколько опций: - `-T DIR` - временный каталог, в котором будет происходить сортировка, если она не помещается в буфер сортировки в памяти. По умолчанию берётся `$TMPDIR` или `/tmp`. Будьте внимательны, когда сортируете большие объёмы, имея раздел `/tmp` на рамдиске - память может закончиться - `-S BUF` - размер буфера сортировки в памяти, задваётся в байтах. Если буфер переполняется, то используется ФС (см. опцию `-T`) #### `jl-sort` - сортировка Враппер над GNU Sort, позволяющий сортировать JSON. Сохраняет все плюсы обычного `sort`, такие как поддержка сортировки в ФС, merge-sort, стабильная сортировка, ограничение размера буфера сортировки. #### `jl-filter` - фильтрация Аналог `grep`, но для JSON. #### `jl-reduce` - группировка по ключу и агрегация Обобщённая утилита, которая умеет производит свёртку элементов в одно значение для каждой группы. Принимает 3 аргумента обязательных в виде JS-функций: - `-i FUNC` - инициализатор аккумулятора: вызывается для каждой новой группы. Функция запускается в контексте группы. - `-u FUNC` - обновление аккумулятора: функция, вызываемая для каждого элемента группы. Функция запускается в контексте группы и принимает аргумент `r`, в котором текущий элемент потока - `-r FUNC` - получение значения аккумулятора: вызывается в конце группы для получения результата Функция запускается в контексте группы. и один необазятельный аргумент - `-k KEYDEF` - ключ, который будет использован для группировки, может быть функций. Для правильной работы входной поток должен быть отсортирован по этому ключу в любом направлении Для лучшего понимания, приведу пример простой реализации `jl-sum`, которая считают сумму поля `amount` для каждого юзера, который идентификируется полем `uid` ```sh jl-reduce -k uid -i '{this.sum = 0}' -u '{this.sum += r.amount}' -r '{return this.sum}' ``` Для входного потока, содержащего ```json {"uid": 1, "amount": 10} {"uid": 1, "amount": 11} {"uid": 2, "amount": 12} {"uid": 3, "amount": 13} ``` На выходе получим ```json {"key":1,"value":21} {"key":2,"value":12} {"key":3,"value":13} ``` Если не указывать `-k`, то свёртка будет проходить по всему потоку и на выходе получим одно значение ```json {"value":46} ``` #### `jl-sum` - суммирование Предефайн для `jl-reduce`, который считает сумму параметра по группам #### `jl-count` - подсчёт количества Предефайн для `jl-reduce`, который считает количество элементов в группе #### `jl-transform` - модификация #### `jl-extract` - извлечение значения поля #### `jl-from-csv` - конвертация CSV в JSON #### `jl-plainify` - конвертация сложного объекта в одноуровневый k-v