Используем Bash в SQL-стиле

  • Tutorial
Приветствую! Данная небольшая статья призвана осветить некоторые аспекты применения Bash для анализа файлов в SQL-стиле. Будет интересна для новичков, возможно, опытные пользователи также найдут для себя что-нибудь новое.

Структура задачи:

  • projects
    1. project1/ — проекты
      • conf/
        • <run_configurations>*.conf — конфигурации построения отчетов по таблицам
      • reports/
        • <run_configurations>/
          • report1.json — сами отчеты, содержат статистику по таблицам Apache Hive
          • report2.json
    2. project2/
      ...

Надо: найти просроченные отчеты.

Итак, расчехляем Bash, открываем отдельный терминал для man-ов и приступаем)

Всех, кому интересно — прошу под кат.

Имеем: внутреннюю систему построения отчетов в виде папки с проектами. В каждом проекте в папке conf лежат конфигурации построения отчетов, содержащие в себе имена Hive-овых баз данных в полях "schema", по таблицам которых строятся отчеты. В папке reports — сами отчеты, разложенные в папки с именами конфигураций. Каждый отчет — это json, содержащий статистику по Hive-овым таблицам в массиве объектов "table", а также дату создания в поле "created_date". Возьмем ее вместо даты создания файла, раз уж есть. Нам надо найти такие отчеты, в которых содержатся таблицы, которые были изменены после создания отчета.

Почему в SQL-стиле? Bash предоставляет большие возможности работы с текстом, разделенным на колонки (обычно пробелами), напоминающие обработку таблиц в SQL.

Инструментарий:

  • cat, find, grep и прочее — в представлении не нуждаются)
  • sed — используем для тупой автозамены sed s/что/на что/g
  • awk — позволяет отображать/переставлять/сливать колонки, фильтровать строки по содержимому колонок
  • sort, uniq — наверное, любимые инструменты разгребателей логов) Первый — сортирует, второй — удаляет/подсчитывает дубликаты. Используются часто для всяких
    top N
    awk '...' log | sort -k field_n | uniq -c | sort -n -r | head -n N

  • xargs — обрабатывает поток строк одной командой. Может развернуть строки в argument-list для заданной команды, а может для каждой строки эту команду выполнить.
  • join — натуральный SQL-евский INNER JOIN. Сливает 2 сортированных файла по значению одного одинакового поля в один, сначала идет общее поле, затем оставшиеся поля первого файла, потом — второго.

Приступим. Для начала — просто нагрепаем используемые таблицы:

 grep -r "\"table\":" projects/*/reports/* | ...

Он отдает данные в таком виде:
projects/project1/reports/run1/report1.json: «table»: «table1»,
projects/project1/reports/run2/report2.json: «table»: «table2»,
projects/project2/reports/run3/report3.json: «table»: «table3»,
...
 ... | sed 's/:/ /g' |  awk '{print $1 " " $3}' | sed 's/[\r\n",:]//g' | ...
... | sort -k 1b,1 | uniq > report_tables

Меняем ':' на пробел, чтобы точно отделить имя файла от колонки «table», печатаем первую (файл отчета) и третью (имя таблицы) колонки, чистим мусор sed-ом, пересортировываем и сохраняем в нашу первую таблицу — report_tables.

Затем таким же способом строим таблицу report_dates, только грепаем created_date и выводим чуть больше колонок (дату и время):

grep -r "\"created_date\":" projects/*/reports/* | sed 's/:/ /g' | ...
... | awk '{print $1 " " $3"T"$4":"$5":"$6}' | sed 's/[\r\n",:]//g' | ...
... | sort -k 1b,1 | uniq > report_dates

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

join report_tables report_dates | awk '{print $1"#"$2 " " $3}' | ...
... | sort -k 1b,1 > report_table_date
projects/project1/reports/run1/report1.json#table1 2017-08-07T070918.024907
projects/project1/reports/run1/report1.json#table2 2017-08-07T070918.024907
projects/project1/reports/run1/report1.json#table3 2017-08-07T070918.024907
...

Первая часть вроде бы готова. Теперь по аналогии нагрепаем используемые базы:

grep -r "schema\":" projects/*/conf/* | sed 's/:/ /g' | ...
... | awk '{print $3 " " $1}' | sed 's/[\r\n":,]//g' | ...
... | sort -k 1b,1 | uniq > schema_configs
schema1 projects/project1/conf/run1.conf
schema1 projects/project1/conf/run2.conf
schema2 projects/project2/conf/run1.conf
...

Вот и первая трудность. Предыдущая таблица построена по файлам отчетов, а эта — по файлам конфигов. Надо проставить между ними соответствие:

cat schema_configs | awk '{print $2}' | sort | uniq | ...

А теперь задумаемся. Просто поставить xargs -n1 find ... мы не можем, так как потеряем саму строку с конфигом, а она нужна. Значит, будем итерироваться циклом. Ну да ладно. Ставим пайп и поехали:
... | while read line; do <statements>; done | sort -k 1b,1 > config_reports

Далее пишем все внутри statements:
dir=$(dirname $line); dir2=$(dirname $dir); ...
run=$(echo $line | sed "s/.*\///" | sed 's/\.conf//g'); ...
reps=$(find $dir2/reports/$run/ -name *.json); ...
for r in $reps; do echo $line $r ; done

Выглядит сложно. dirname вытаскивает из пути к файлу путь до последнего слеша, этим мы и воспользовались, чтобы подняться выше файла с отчетом на пару уровней ($dir2). Следующее выражение run=... вытаскивает из $line имя файла run.conf и обрезает расширение, получая имя конфигурации запуска. Далее reps — имена файлов с отчетами для данного конфига, и циклом по ним выводим файл с конфигом $line и файл с отчетом $r. Пересортировываем и пишем табличку config_reports.
projects/project1/conf/run1.conf projects/project1/reports/run1/report1.json
projects/project1/conf/run1.conf projects/project1/reports/run1/report2.json
projects/project1/conf/run2.conf projects/project1/reports/run2/report3.json
...

Это была самая важная часть работы — проставить соответствие между пространством конфигов и пространством отчетов. Осталось только определить даты последнего изменения таблиц в используемых бд, и у нас будет вся нужная инфа, останется только все правильно переджойнить. Поехали:

cat schema_configs | awk '{print $1}' | sort | uniq | ...
... |sed 's/^/path_in_hive/g' | sed 's/$/\.db/g' | ...
... | xargs -n1 -I dr hdfs dfs -ls dr | sed 's/\// /g' | ...
... | sed 's/\.db//g' | awk '{print $12 " " $13 " " $6"T"$7}' | ...
... | sort -k 1b,1 | uniq > schema_tables

Несмотря на длину, тут все просто. Сначала берем schema_configs, оттуда выделяем уникальные схемы, затем sed-ом приписываем к началу путь к Hive-вому хранилищу, в конец — расширение .db. Теперь для каждой такой строки выполняем hdfs dfs -ls, это показывает нам все таблицы в заданной базе с датами их последнего изменения. Меняем все слеши на пробелы, вытаскиваем имя базы, имя таблицы и дату ее изменения, пересортировываем и готова табличка schema_tables.

Теперь заключительная часть:

# configs - tables
join schema_configs schema_tables | awk '{print $2 " " $3 " " $4}' | ...
... | sort -k 1b,1 | uniq > config_tables

# reports - tables hive dates
join config_reports config_tables | awk '{print $2"#"$3 " " $4}' | ...
... | sort -k 1b,1 > report_table_hive_dates

# final!
join report_table_date report_table_hive_dates | sed 's/#/ /g' | ...
... | awk '{if ($3<$4) print $1}' | sort | uniq > outdated_reports

Сначала джойним schema_configs и schema_tables по полю с именем бд, и получаем табличку config_tables — конфиг, таблица и дата ее последнего изменения. Затем джойним config_reports и config_tables, чтобы наконец-то получить соответствие отчет — таблица в Hive. Причем имя файла с отчетом и имя таблицы объединяем в одно поле с помощью #. Ну и последний штрих — сджойнить report_table_date и report_table_hive_dates, разделить имя файла с отчетом и имя таблицы пробелом, и напечатать те отчеты, где дата создания отчета меньше даты изменения таблицы, затем ищем уникальные отчеты, и работа готова.

Заключение


Девять довольно простых строк на баше оказалось достаточно, чтобы решить данную задачу. Далее этот скрипт запускаем по крону, и вебморда, ориентируясь на файл outdated_reports, может выдать для отчета заголовок "Report is outdated" (или не выдать).

Код тут
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 36
  • 0
    Плюсую.
    Часто приходится обрабатывать таким образом значительные объёмы данных. Раньше загонял данные в SQL таблицы, строил индексы, писал заросы, экспортировал результат. Лет 7 назад открыл для себя возможность разгребать такие файлы при помощи unix команд. Результаты просто фантастические! Скорость обработки увеличилась даже не в разы — на порядки.
    • +1
      Эмм, а скорость «раскуривания» такого скрипта следующим админом (или Вами же, но через полгода-год) как изменилась?

      Что мешает установить язык более высокого уровня (с IDE, отладчиком, автокомплитом, подсветкой синтаксиса и проверкой ошибок «на лету»)?

      Нет, мы откроем ман в соседней консоли и будем отлаживать скрипт стопятьсотый раз прогоняя скрипт…
      • +6
        Если «админ» не знает текстовые утилиты unix он просто не компетентен.
        Автор просто не напиасл самое важное, что выше приведенный скрипт будет работать десятилетиями и не требовать какого либо сопровождения, в отличии от писанины на «языке высокого уровня»
        • –1
          Автор просто не напиасл самое важное, что выше приведенный скрипт будет работать десятилетиями и не требовать какого либо сопровождения, в отличии от писанины на «языке высокого уровня»

          А писанина на языке высокого уровня что? в 12 часов превратится в тыкву? :)

          • 0
            А вы помните все ключи для вызова read? А в мане за сколько найдете что делает ключ -r например?
            И я молчу про постоянный эскейпинг кавычек, неявный субшел, башизмы и прочие IFS'ы.
            Питон предпочтительнее везде, где bash переваливает на 10 строк.
            • +2
              Ну тогда я спокоен, у меня 9)
              • 0
                Да, последнее время занчительно больше делаю на Питоне: заимодействие с API, JSON, HTTP запросы. Но такого рода обработка файлов — это то, с чем башовые утилиты справляются значительно быстрее.
                • 0
                  bash скрипт работает быстрее, чем python? Не верю (с)
                  • 0
                    bash скрипт работает быстрее, чем python? Не верю (с)

                    Ну собственно первая строка этого конкретного баш скрипта проходит по всем файлам и ищет там строки с "table". А вторая строка ещё раз проходит по тем же файлам и ищет в них же строки с "created_date". Эти данные записываются в файлы. И потом содержимое файлов джойнится.


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


                    И чем бы ты не описал эти альтернативные действия — питоном ли, джавой или рубями или другим, более ядрёным, башем — всё равно получится быстрее, чем в данном конкретном случае, за счёт более вменяемого алгоритма.


                    Так что, если речь идёт именно о скорости работы программы, а не о скорости создания скрипта — правильно не верите :)

            • +2
              Как будто IDE, отладчик, автокомплик, подсветка синтаксиса и проверка ошибок на лету — это панацея и не придется раскуривать ни свою реализацию поиска по файлам, ни свою реализацию sed, ни кастомную грепалку…
              • –1
                Как будто IDE, отладчик, автокомплик, подсветка синтаксиса и проверка ошибок на лету — это панацея

                Не панацея, но писать будет легче и быстрее.


                и не придется раскуривать ни свою реализацию поиска по файлам

                Это вы про поиск файлов, содержащия строку? Код будет тривиальным, раскуривать его не придётся.


                ни свою реализацию sed

                sed там в коде используется для того, чтобы делать замены в строках. Код ещё более тривиальный, чем поиск по файлам, там вообще даже думать не придётся, чтобы понять что происходит.


                ни кастомную грепалку

                grep используется либо для поиска файлов (см первый пункт), либо для отсева строк, что в языке программирования высокого уровня тоже очень просто.

                • +2
                  Так и на баше не rocket science. Ну и по опыту, на баше такие штуки пишутся гораздо быстрее, получаются гораздо короче и оказываются гораздо более надежными. Меньше кода — меньше багов (с). Проще поставить пайп, чем написать лишний if или фильтр/замену по регекспу. Если брать support, то да, код write-only, но он таким и задумывался — решить задачу наименьшими силами и забить.
                  • –1
                    Так и на баше не rocket science.

                    Для баша нет IDE, которая тебе подскажет и поможет.


                    Ну и по опыту, на баше такие штуки пишутся гораздо быстрее, получаются гораздо короче и оказываются гораздо более надежными.

                    Из того, что есть в скриптах, навскидку какие-то трудности представляет только join, потому что его, возможно, в стандартной библиотеке не будет.


                    Если брать support, то да, код write-only, но он таким и задумывался

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

                    • 0
                      Для баша нет IDE, которая тебе подскажет и поможет.

                      для баша есть set -x. Для условных однострочников этого хватает за глаза. Люди, которые хотят рефакторинг и go-to-definition для скриптов на баше, явно делают что-то не так.
                      • +1
                        Люди, которые хотят рефакторинг и go-to-definition для скриптов на баше, явно делают что-то не так.

                        Ну да, используют баш там, где лучше использовать язык программирования высокого уровня.

                        • 0
                          Не, ну правда. Не нравится баш — не пишите. Не нравится читать код на баше — не читайте. Начальство заставляет — смените начальство. Как-то так.
                          • 0
                            Не, ну правда. Не нравится баш — не пишите.

                            Вопрос то вроде в том, что потом легче читать и модифицировать — баш или код на языке высокого уровня, а не в том, нравится мне писать на баше, или нет.

                            • +1
                              Вкратце ответ — кому как. Я напишу на баше и уволюсь, придете вы и скажете, что это все г**но и надо на питоне, перепишете на питоне, уволитесь, придет условный Ваня и скажет, что питон г**но и надо на баше, и так далее. И это — нормально.

                              Кто как хочет — тот так и доставляет себе радость и удовольствие.

                              Тем более задача — написать, чтоб работало сейчас — обычно более приоритетна надо задачей — чтобы было всем легко читать потом.
                              • +1
                                Вкратце ответ — кому как. Я напишу на баше и уволюсь, придете вы и скажете, что это все г**но и надо на питоне, перепишете на питоне, уволитесь, придет условный Ваня и скажет, что питон г**но и надо на баше, и так далее. И это — нормально.

                                Вы тут какое-то колесо сансары описали. Бессмысленный и беспощадный джаггернаут. Я против того, чтобы это было нормой :).


                                Кто как хочет — тот так и доставляет себе радость и удовольствие.

                                Спорить не буду, нередко решать проблему лучше не тем инструментом, который её лучше решает, а тем, которым ты лучше умеешь пользоваться. Особенно, когда надо сделать так, чтобы заработало прямо сейчас. Если бы в ветке не подняли вопрос поддерживаемости, в общем-то и разговора бы не было.


                                Хотя, пользуясь случаем, хочу сделать замечание статье. Стиль, используемый в скрипте — не SQL. SQL это декларативный язык, а тут функциональный конвейер.

              • +1
                Вот написал человек на Delphi программульку и слился. В свое время этот IDE был на вершине популярности. Прога была хороша, мы были рады до того момента, когда данных стало много и начались тормоза (что то не оптимально написал, не предвидел).

                Купили крутую программку от уважаемой компании. Делает все, но чуточку не то что нужно. И сделать так как нам надо они немогут — навороты не позволяют. Вынуждены жрать кактус.

                Так что скрипт на bash'е переживет все модные веяния. И понимающий unix команды всегда найдутся.
                • +1
                  Вот написал человек на Delphi программульку и слился.

                  Я так понимаю проблема в том, что нет человека, который может поправить исходники и собрать код заново? Ну, то есть, надо было выбирать другй язык программирования?


                  Так что скрипт на bash'е переживет все модные веяния. И понимающий unix команды всегда найдутся.

                  Программа на java тоже переживёт. Да что там. Мне неприятно это говорить, но даже программа на Питоне будет жить ещё очень долго. Ну и работать не только там, где есть unix tools, конечно :)

                  • 0
                    Ну, то есть, надо было выбирать другой язык программирования?


                    В то время выбор Delphi был естественным. Сама программа 4 года радовала всех.

                    Мне неприятно это говорить, но даже программа на Питоне будет жить ещё очень долго.


                    А вот Питон — г… о. Постоянно имею проблемы с прогами на питоне. Какой нибудь редкий модуль отвалится а из логов никак непонят что отвалилось. А когда разберешься, окажется что нужна версия модуля которая при сборке вываливается с ошибкой. С прогами на других языках столько гемора не имели.

                    Конечно, можно винить руки из ж, но в бизнесе чинить программы — муда. Инструменты должны работать незаметно и исправно.
                    • +1
                      В то время выбор Delphi был естественным.

                      Я не сомневаюсь.


                      Сама программа 4 года радовала всех.

                      Вне всякого сомнения это успешный код. Я даже не совсем понимаю, что мешало поправить его и собрать ещё раз. Врядли обошлось бы дороже покупки стороннего решения. Дельфи то сейчас не то чтобы совсем мёртвая штука .


                      А вот Питон — г… о.

                      Ну, я бы не был так категоричен. Тем не менее я его не люблю и именно поэтому мне неприятно говорить, что код на питоне — таки штука долговечная.


                      Постоянно имею проблемы с прогами на питоне.

                      Сочувствую. У меня немного обратная ситуация — я их писать не люблю. Но питон однозначно лучше баша.

                      • 0
                        Я даже не совсем понимаю, что мешало поправить его и собрать ещё раз. Врядли обошлось бы дороже покупки стороннего решения.


                        Для компании несвязанной с IT найти нужного программиста задача нетривиальная, точнее — лотерея. И в это далекое время мы не знали слова «freelance». Оглядываясь назад, мы многое могли сделать лучше. Был бы тогда теперешний опыт…
                      • 0

                        Если вы не платите за это деньги — то вам никто ничего не должен. А руки да — они из одного места получается

                  • 0
                    Вообще, это скорее повод следующему админу таки раскурить баш. А то мало ли, вдруг воинствующий джавист придет сопроводжать. Давайте для него версию на java напишем, с GOF-ом и интерфейсами.
                    • 0

                      А можно попросить у вас какие-нибудь живые данные? Что там в этих файликах, на которых скрипты бегают?

                      • 0
                        Данные-то дать могу, но вот Hive-кластер, который в статье упоминается, вам самому поднимать приедтся
                        • 0

                          Как я понимаю, от кластера там только xargs -n1 -I dr hdfs dfs -ls dr . Это можно и чем-нибудь другим подменить для целей тестирования.

                • +4
                  Вместо sed 's/:/ /g' | awk '{print $1 " " $3}' можно использовать awk -F: '{print $1 " " $3}'
                  Конструкция упростится и станет более читабельной
                  • 0
                    Да, можно. Спасибо, как-то сразу не догадался)
                    • +1
                      Вот эту конструкцию тоже можно заменить для читабельности:
                      sed 's/[\r\n",:]//g'
                      на
                      tr -d '\r",:'

                      Зачем у вас идёт замена переноса строки (\n)?
                      Во-первых, сэд её так не сможет заменить — надо считать хотя бы две строки для этого (link).
                      Во-вторых, если это удастся сделать (тем же tr), то у вас будет всё в одну строку, что нарушит дальнейшую логику.
                  • +2
                    jq для работы с JSON не пробовали?
                    • +1
                      Посмотрел. Не сильно код укорачивает, если честно) Задача для него слишком примитивна
                    • +3

                      Хотя и сам не прочь крутить подобные портянки, хочу предупредить читателей, этот код — не пример для подражания. Выход за границу 80 символов — сама по себе проблема, и если скрипт "причесать" до читаемого состояния, то в нём будет уже не 9, а без малого сотня строк. И гарантировать, что этот скрипт не развалится от случайно затесавшегося неформатного файлика, в отличие от настоящего парсера, невозможно. А когда он развалится, отладка превратится в ад (если этот факт вообще кто-то заметит).


                      Также для уменьшения времени работы, количества временных файлов да и просто вероятных ошибок, в Bash есть замечательная конструкция:


                      while read -r line; do
                        echo $((line**2))
                      done < <(seq 123)

                      В отличие от command | while ...do ... done она не создаёт новый сабшелл и позволяет избежать ошибок, связанных с потерей переменных.

                      • 0
                        cut -f 1,3 -d ':' вместо sed 's/:/ /g' | awk '{print $1 " " $3}'
                        не работает?
                        А sed 's#\(.*\):.*:\(.*\)#\1 \2#g'?

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.