grep, sed, awk и jq на практике
Взял файл из шести строк, где три значения повторяются. Посчитал повторы командой подсчёта уникальных - получил шесть строк по одному разу каждая, то есть ничего не посчитал. Отсортировал перед подсчётом - получил честные 3, 2 и 1. Команда склеивает только СОСЕДНИЕ одинаковые строки, и об этом узнают обычно после того, как отчёт уже отправлен.
Стержень: у каждого инструмента есть ровно одно правило, которое ломает результат молча, и знать надо именно его.
// Формулировки: «посчитай топ ошибок в логе», «почему grep -c даёт не то число?», «как достать поле из JSON?»
Три инструмента и три молчаливые ловушки
Первая: подсчёт уникальных работает по соседним строкам, поэтому перед ним всегда идёт сортировка. Без неё результат выглядит правдоподобно и при этом неверен - у меня шесть строк вместо трёх значений.
Вторая: поиск с подсчётом считает СТРОКИ, а не совпадения. В моём файле слово встречалось четыре раза, но в двух строках: подсчёт строк дал 2, а подсчёт совпадений (вывести каждое совпадение отдельной строкой и посчитать их) дал 4. Если считаете события в логе, разница принципиальная.
// Третья: подсчёт строк командой wc считает переводы строки. Файл из трёх строк без перевода в конце даёт результат 2 - последняя строка не посчитана, потому что после неё нет перевода. Такое приезжает из выгрузок и из логов, обрезанных на середине. Честный подсчёт даёт поиск пустого шаблона с ключом подсчёта: он вернул 3.
- подсчёт уникальных
- склеивает только соседние одинаковые строки; нужна сортировка перед ним
- совпадение против строки
- в одной строке слово может встретиться несколько раз
Считать по колонкам
Когда данные разложены по колонкам, дальше сортировки удобнее язык обработки текста. На пятистрочном куске лога с адресом, методом, путём, кодом ответа и временем я посчитал три вещи одной строкой каждая.
Доля ошибок: всего 5 запросов, из них 2 с кодом пятисотой группы, доля 40%. Время ответа: среднее 0,738 секунды, максимум 2,010. Топ адресов: первый адрес встретился трижды, остальные по разу. Всё это - обычные условия и накопление в переменных, никакой магии.
// Полезная привычка при разборе аварии: не тащить лог целиком в глаза, а сводить его к таблице «сколько чего». Три строки обработки отвечают на вопрос «что вообще происходит» быстрее, чем пятнадцать минут листания. И ещё: считайте долю, а не голое количество. «200 ошибок» ничего не значит, пока неизвестно, из скольких запросов.
- колонка
- поле строки, отделённое пробелами; в awk доступно по номеру
- сводка вместо чтения
- свести лог к таблице «сколько чего» до того, как читать глазами
JSON регулярками и правка файлов на месте
Достать поле из структурированных данных регулярным выражением (шаблоном для поиска по тексту) получается ровно до первого настоящего файла. JSON, в котором такие данные обычно и приезжают, - это текстовый формат записи вложенных структур: пары «имя поля - значение», списки, вложенные объекты. Я взял запись, где в имени есть экранированная кавычка: «Анна \"Аня\" Петрова». Регулярка вернула обрезок «Анна \"», потому что честно остановилась на первой кавычке. Разборщик вернул имя целиком. Второй случай в той же записи: рядом лежит поле с текстом, куда попало слово из шаблона поиска - регулярка в другом файле легко утащит именно его.
Вывод не «регулярки плохие», а простой: структуру разбирают разборщиком структуры. Для JSON это отдельный инструмент, который понимает вложенность, экранирование и типы; он же считает длину массивов и собирает выборки.
// Отдельная история - правка файла на месте. Замерил: у файла до правки был внутренний номер 789055, после правки стал 789124. То есть инструмент не изменил файл, а создал новый и переименовал его поверх старого. Последствий два. Первое: жёсткая ссылка на старый файл (второе имя того же файла на диске) остаётся со СТАРЫМ содержимым - проверил, ссылка после правки показывала прежний текст. Второе: программа, которая держала этот файл открытым, продолжит читать старую версию. Именно поэтому конфиг внутри контейнера, примонтированный одним файлом, после такой правки не обновляется.
- разборщик структуры
- инструмент, который понимает вложенность и экранирование, а не ищет по тексту
- правка на месте
- создание нового файла и переименование поверх старого; внутренний номер меняется
Как отвечать: «Посчитай топ ошибок в логе». Как это делается и где тут грабли?
Обычный конвейер: вытащить нужное поле, отсортировать, посчитать повторы, отсортировать по числу и взять верхние строки. Главная граблина в середине: подсчёт повторов склеивает только соседние одинаковые строки, поэтому без сортировки перед ним результат выглядит правдоподобно и при этом неверен - я проверял, шесть строк вместо трёх значений. Вторая граблина рядом: подсчёт с ключом счётчика считает строки, а не совпадения, а в одной строке слово может встретиться несколько раз, у меня было четыре совпадения в двух строках. Если данные разложены по колонкам, я вместо цепочки беру язык обработки текста и сразу считаю долю, а не количество: двести ошибок ничего не значат, пока неизвестно, из скольких запросов.
Ответ даёт рабочую цепочку и сразу называет два места, где она молча врёт. Плюс переход к доле вместо количества - это уже разговор про разбор аварий.
На чём валятся
- −Считают повторы без сортировки и получают правдоподобный неверный результат.
- −Путают число строк с числом совпадений при подсчёте событий.
- −Разбирают структурированные данные регулярками и ломаются на экранировании.
- −Правят файл на месте и удивляются, что жёсткая ссылка и открывший его процесс видят старое.
- −Считают количество вместо доли и не могут сказать, много это или мало.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 7, остальные разбираются в тренажёре.
- Считаем ошибки по коду в логе: awk '{print $9}' access.log | uniq -c. Числа получаются странные. Что не так?A)uniq -c считает строки, а не поля, нужен ключ -fB)uniq схлопывает только соседние повторы — перед ним нужен sortC)awk печатает поле с пробелом, и uniq видит разные строкиD)uniq работает только с отсортированным по числу вводом, нужен sort -n
показать ответ и разбор
+B)uniq схлопывает только соседние повторы — перед ним нужен sort// разбор: uniq сравнивает строку с предыдущей и схлопывает подряд идущие дубли — на неупорядоченном потоке один и тот же код возврата даст десяток отдельных групп. Канон: awk '{print $9}' access.log | sort | uniq -c | sort -rn | head. Второй sort уже по числу, чтобы вытащить топ.
- Скрипт достаёт поле из JSON-ответа API через grep и sed. Что с этим не так на проде?A)grep не умеет работать с потоком от curl без буферизацииB)sed портит юникод в ответе и ломает кодировкуC)Регулярка не переживёт вложенность, порядок полей и переносы строкD)Такой разбор запрещён: API отдают gzip, а grep его не читает
показать ответ и разбор
+C)Регулярка не переживёт вложенность, порядок полей и переносы строк// разбор: JSON — дерево, а не строки: сервер вправе поменять порядок ключей, отдать всё одной строкой или разбить на много, добавить вложенность и экранированные кавычки. Регулярка на этом ломается молча и подсовывает мусор дальше по скрипту. jq разбирает структуру: jq -er '.data.id' и код возврата на отсутствующем поле — тот самый явный отказ, который нужен в автоматизации.
- Конфиг проброшен в контейнер как файл: -v /etc/app/conf.yml:/conf.yml. После sed -i на хосте контейнер видит старое содержимое. Почему?A)sed -i пишет новый файл и подменяет inode, а монтирование держит старыйB)Файл в контейнере кэшируется слоем образа до перезапускаC)Монтирование файла делается копией, правки на хост не влияютD)sed -i меняет права, и контейнер теряет доступ к новой версии
показать ответ и разбор
+A)sed -i пишет новый файл и подменяет inode, а монтирование держит старый// разбор: Монтирование одиночного файла привязано к inode, а sed -i (как и большинство редакторов) создаёт временный файл и переименовывает его поверх — inode меняется. Контейнер продолжает смотреть на старый inode, и даже последующая запись в тот же путь до него не доходит. Лечится правкой на месте (запись в тот же inode) или монтированием каталога вместо файла плюс перезапуск контейнера.
- cut -f2 data.csv на CSV с запятыми возвращает всю строку целиком вместо второго поля. Почему?A)cut считает поля с нуля, так что поле 2 — это на деле третьеB)cut по умолчанию режет по табу, а не по запятой — нужен -d','C)cut не понимает кодировку этого файла и потому берёт строку целикомD)Номер поля надо брать в кавычки: cut -f'2'
показать ответ и разбор
+B)cut по умолчанию режет по табу, а не по запятой — нужен -d','// разбор: У cut разделитель по умолчанию — табуляция; в строке без табов вся она считается одним полем, поэтому -f2 возвращает всё. Для CSV: cut -d','. Но и это ломается на запятых внутри кавычек — настоящий CSV парсят инструментом, понимающим кавычки (csvcut, python, awk с FPAT), а не наивным разбиением.
- В access.log поле $5 — размер ответа в байтах. Как одной командой посчитать суммарный отданный трафик?A)cat access.log | wc -cB)sort -n access.log | tail -1C)awk '{s+=$5} END{print s}' access.logD)grep -o '[0-9]\+' access.log | wc -l
показать ответ и разбор
+C)awk '{s+=$5} END{print s}' access.log// разбор: awk идеально для агрегатов по колонкам: s+=$5 копит сумму построчно, блок END печатает итог после обхода. Легко добавить фильтр (/pattern/{s+=$5}) или условие ($9==200). Для суммы по группам берут ассоциативный массив: s[$key]+=$5. wc/sort такого не умеют — они про строки и сортировку.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.