Обработка текста с помощью textutils с примерами
Набор утилит для потоковой обработки текста появился уже в первых версиях Unix и доступен практически везде. Это такие команды как cat/tac, head/tail, cut, grep, sed, sort, uniq, wc, nl, fmt. Каждая из этих утилит выполняет свою простую обработку текста, но комбинируя их в конвейере, т.е. передавая стандартный вывод одной команды на вход следующей, можно обрабатывать тексты произвольного размера или быстро решить некоторые задачи.
Для этого нужно усвоить несколько простых приёмов.
Потоковое редактирование списка файлов
Текстовый файл - это по сути множество букв, цифр, специальных символов, разбитых на строки. Пустая строка в тексте - частный случай, состоящий только из символа конца строки, начнём с него. Во-первых, можно сделать невидимые символы, такие как конец строки или знак табуляции, видимыми, напр:
head -n 3 textutils.md |cat -ET
###^Iобработка текста с помощью textutils с примерами$
$
Набор утилит для потоковой обработки текста появился уже в первых $
Ключи/опции к командам объяснять не буду - всегда можно использовать опцию `--help`. А мы видим, что в первой строке у нас не пробелы, а символ табуляции ^I, в третьей в конце строки 2 пробела. Уберём эти пробелы, многие style-guides требуют не оставлять подобный невидимый мусор в коде.
sed 's/\s*$//' textutils.md > textutils-clean.md
diff textutils.md textutils-clean.md |cat -ET
3c3$
< Набор утилит для потоковой обработки текста появился уже в первых $
---$
> Набор утилит для потоковой обработки текста появился уже в первых$
sed - т.н. потоковый редактор, выполняет заданное регулярным выражением редактирование для каждой строки. Регулярные выражения, regex'ы, тема достойная отдельного тьюториала, я буду давать только минимальные пояснения. В приведенной команде sed это:
s - substitute, команда sed
/ - ограничители для regex /что искать/на что заменить/
звездочка * - метасимвол, вхождение предыдущего символа 0 или более раз
\s - пробел, символ табуляции, т.н. пробельные символы
$ - привязка regex к концу строки
Будьте внимательны - вывод sed надо перенаправлять в новый файл, иначе файл обнулится, или использовать ключ -i (in place). Команда diff не совсем относится к текстовым утилитам, но, как видите, весьма удобна для поиска различий между файлами.
Более интересно, конечно, уметь выполнять подобное редактирование для многих файлов. Исторически язык perl возник как попытка добавить в sed, awk и другие утилиты новые возможности. Одна из таких возможностей - редактирование in place, т.е не затирая содержимое файла до его редактирования. Вот как будет выглядить аккуратная очистка мусора в конце строк для всего каталога:
find -type f|grep -v \./\.git/|xargs -r grep -P ' +$'|head|cat -ET
find -type f|grep -v \./\.git/|xargs -r grep -Pl ' +$'|xargs -r ls -l
find -type f|grep -v \./\.git/|xargs -r grep -Pl ' +$'|xargs -r perl -i -wpe's/ +$//'
Пара могучих команд find/xargs также заслуживают отдельного тьюториала, ограничусь только минимальными пояснениями. Первая команда по очереди делает: находит все файлы в текущем каталоге и во всех вложенных, убирает из списка всё в каталоге .git(его редактировать нельзя!), для всех оставшихся файлов ищет концевые пробелы, отрезает верхние 10 строк, показывает результат поиска с невидимыми символами. Эта команда нужна только для проверки, убедиться, что мы правильно составили регулярное выражение. Вторая команда похожа на первую, но вместо печати содержимого файла, показывает список файлов - ещё одна проверка что найдены только нужные файлы. И, наконец, третья команда выполняет заданное потоковое редактирование in place. Пояснения к использованным опциям:
grep -v - найти строки НЕ содержащие указанный контекст
\. - искать точку буквально, в regex точка имеет особый смысл
grep -P - использовать т.н. perl-compatible regex
grep -l - вывести только имя файла без его содержимого
perl -i - выполнить редактирование in place
perl -wpe's/ +$//' - почти то же самое что sed, см. выше
Предупреждение! Указанные выше действия выполнены опытным профессионалом, перед их повторением внимательно выполните все проверки, начинающим рекомендуется использовать опцию perl -i.bak для сохранения старых файлов с расширением .bak. Полезно также проверить работу замены на одном файле. Мне пришлось пару раз ревёртить коммиты в гитхабе.
Автор не несёт ответственности за возможную порчу файлов.
Запросы к текстовому структурированному файлу как к БД
Следующий пример - использование textutils и специально структурированного текстового файла как простой БД. Для наших запросов выберем файл CREDITS из репозитария свежего ядра Линукс:
curl https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/CREDITS?h=v5.15.6 -o CREDITS
В самом начале файла пояснение от некоего Линуса:
head CREDITS
This is at least a partial credits-file of people that have contributed to the Linux project. It is sorted by name and formatted to allow easy grepping and beautification by scripts. The fields are: name (N), email (E), web-address (W), PGP key ID and fingerprint (P), description (D), and snail-mail address (S). Thanks,
Linus
----------
Давайте посмотрим, что о Linus'е есть в файле:
grep -A 5 '^N: Linus' CREDITS
N: Linus Torvalds
E: torvalds@linux-foundation.org
D: Original kernel hacker
S: Portland, Oregon 97005
S: USA
О, да это сам великий Линус Торвальдс, скромно позволивший себе одну строку описания. Опция -A 5 показала нам 5 строк после найденного контекста. Интересно, а есть ли там наши люди?
grep -iEB 10 '^S: (Russia|Ukraine)' CREDITS|grep '^N: '
N: Oleg Drokin
N: Yuri Per
N: Stas Sergeev
Есть! Выше я использовал вывод 10 строк до контекста, опция -B, опцию -i игнорировать регистр, а также т.н. extended regex, опция -E, в данном выражении имеющая смысл Россия _ИЛИ_ Украина. Второй grep вывел только поле name, аналог _И_ в настоящем языке запросов.
Можно подсчитать, сколько всего человек упомянуты в этом почётном списке:
grep '^N: ' CREDITS|wc -l
569
Команда wc умеет считать строки, слова и символы, ключ -l показал только строки. Можно более аккуратно посчитать только уникальные строки:
grep '^N: ' CREDITS|sort -u|wc -l
568
Неожиданно на одно имя меньше, т.е. кто-то записан дважды. Найдём ошибку, можно будет послать патч Линусу:
grep '^N: ' CREDITS|sort|uniq -c|sort -k1nr|head -n 1
2 N: Inaky Perez-Gonzalez
В этих двух примерах просканированный (в смысле grep) список сортируется командой sort, затем оставляются только уникальные строки - команда uniq, её ключ -c может также подсчитать совпадающие строки, ещё один sort использует аж 4 ключа:
-k1 - сортировать по полю №1
-n - считать сортируемое поле как число
-r - обратный порядок сортировки, искомый двойник оказался вверху
Ещё один пример запросов к похожей текстовой БД есть в давней статье https://ophilon.github.io/gomelug/articles/sketch_o_heroes.html
Создание отчёта из логов
Если у нас есть большой файл с однотипными строками, со стандартными разделителями между полями, можно, применяя уже опробованные приёмы, работать с текстом как с таблицей. Подходящим файлом для примера может быть лог какого-то сервиса, или .csv файл с разделителями. Нашёл старый лог достаточно большой приблизительно такого содержания:
[10/20/18 5:32:02:404 EDT] 0000006b NGUtil$Server I ASND0002I: Detected server nodeagent started on node b01cxnp11090Node01
[10/20/18 5:32:13:388 EDT] 0000006c NGUtil$Server I ASND0002I: Detected server MRP-W3-Stage-03 started on node b01cxcp11065Node01
[10/20/18 5:36:47:625 EDT] 000000ae WebContainer E com.ibm.ws.webcontainer.internal.WebContainer handleRequest SRVE0255E: A WebGro
[10/20/18 5:41:47:682 EDT] 000000ae WebContainer E com.ibm.ws.webcontainer.internal.WebContainer handleRequest SRVE0255E: A WebGro
[10/20/18 6:06:47:491 EDT] 000000ae WebContainer E com.ibm.ws.webcontainer.internal.WebContainer handleRequest SRVE0255E: A WebGro
[10/20/18 6:11:47:488 EDT] 000000af WebContainer E com.ibm.ws.webcontainer.internal.WebContainer handleRequest SRVE0255E: A WebGro
[10/20/18 6:31:29:177 EDT] 000000bd HardwareInfoC W ASPS0023W: The logical partition on which this node resides has a mode of sha
Обращаем внимание - лог упорядочен по времени, ошибки обозначены буквой ' E ' с пробелами в 6-м поле. Найдём сначала уникальные строки об ошибке:
grep ' E ' SystemOut.log |sed 's/^\[10.*] //'|cut -c 9-120|sort|uniq -c
231 RequestProces E org.apache.wink.server.internal.RequestProcessor handleRequest An unhandled exception occurred
231 ServletWrappe E com.ibm.ws.webcontainer.servlet.ServletWrapper service SRVE0014E: Uncaught service() exception
231 webapp E com.ibm.ws.webcontainer.webapp.WebApp logServletError SRVE0293E: [Servlet Error]-[JAX-RS Servlet]: jav
221 WebContainer E com.ibm.ws.webcontainer.internal.WebContainer handleRequest SRVE0255E: A WebGroup/Virtual Host t
Выше мы выбрали только строки с ошибками, затем удалили поле timestamp, затем убрали командой cut -c 9-120 символы в строке, оставив только диапазон с 9 по 120, далее отсортировали и посчитали уникальные. Ошибок всего 4 типа, причём, судя по числам, 3 из них связаны и по сути это одна ошибка с 3 строками в логе.
Из этого же лога сделаем отчёт по времени ошибок. На этот раз уберём всё кроме timestamp c точностью до часа:
grep ' E ' SystemOut.log |cut -c2-12|uniq -c|tail -n 5
168 10/22/18 2:
48 10/22/18 3:
56 10/22/18 4:
130 10/22/18 5:
121 10/22/18 6:
У нас получился отчёт по количеству ошибок в час за последние 5 часов.
Переводчик для азбуки Морзе
Когда-то давно я увидел похожую задачку у школьников, готовящихся к олимпиаде по программированию. Сразу решил, что она хорошо подходит для sed - кроме контекстной замены, для решения ничего не надо. Ещё, чтобы не утруждать пальцы, решил сделать регулярки для замены прямо из текста задачи.
Из солидарности с нашими добрыми соседями белорусами код Морзе возьмём на стандартный, а тюремный. У них за полтора последних года через тюрьмы и СИЗО прошли десятки тысяч народа, много программистов, думаю они оценят. От стандартного он отличается дополнительной точкой в конце каждой буквы. Стук в стену не имеет длительности, есть только паузы между ударами, для определения был ли предпоследний звук длинным или коротким без ещё одного удара не обойтись. Естественно, это точка.
Даже с усложнением, такая азбука будет эффективнее более простых кодов в подобных условиях. Давно известен т.н. шифр Полибия :
| |1 |2 |3 |4 |5 |
|---|---|---|---|---|---|
|1 |А |Б |В |Г |Д |
|2 |Е |Ж |З |И |К |
|3 |Л |М |Н |О |П |
|4 |Р |С |Т |У |Ф |
|5 |Х |Ц |Ч |Ш |Щ |
|6 |Ы |Ю |Я |АЛТ|ОК |
Используется он так. На стене рисуется таблица 6 строк на 5 столбцов, и заполняется по горизонтали по алфавиту, жертвуя ради экономии буквами Ё, Й, Ъ, Ь, Э. Далее стуком передают сначала номер строки, затем после паузы номер столбца. Последние 2 символа я придумал сам. ОК - для окончания сообщения, наподобие "перехожу на приём" у радистов. АЛТ можно было бы использовать для переключения на альтернативную таблицу, например, добавить цифры:
| |1 |2 |3 |
|---|---|---|---|
|1 |1 |2 |3 |
|2 |4 |5 |6 |
|3 |7 |8 |9 |
|4 |0 |АЛТ|ОК |
Код Морзе более эффективен т.к. он с самого начала составлялся как бинарное дерево, как на заглавной картинке вверху или ниже в виде текста:
......=5;....-.=4;...--.=3;..-...=э;..---.=2;.----.=1;-.....=6;--....=7;---...=8;----..=9;-----.=0;
.....=х;...-.=ж;..-..=ф;..--.=ю;.-...=л;.-.-.=я;.--..=п;.---.=й;-....=б;-..-.=ь;-.-..=ц;-.--.=ы;--...=з;--.-.=щ;---..=ч;----.=ш;
....=с;..-.=у;.-..=р;.--.=в;-...=д;-.-.=к;--..=г;---.=о;
...=и;.-.=а;-..=н;--.=м;
..=е;-.=т;
В стандартной азбуке Морзе ветви из корня растут вверх: влево всегда добавляются точки, вправо тире. Собственно, это двоичный код. Более короткие - чаще используемые буквы. Надеюсь, это видно из текста выше - в корне 2 буквы, потом 4, 8, 16. Пятый ряд заполнен частично, кроме одной буквы "э" там только цифры со своей мнемоникой.
Ой, извините, заболтался. Скрипт то крошечный, что про него долго рассказывать. Я покажу весь скрипт, потом поясню что ещё не проходили.
grep '^\.\.' textutils.md|\
sed 's/\([.-]*\)=/s\/\1 \//g;
s/;/\/g;\n/g;
s/\./\\\./g'|\
grep -v '^$'>morse-sed
Команда grep извлекает из текста статьи код азбуки. Во второй строке 1-я замена: [.-]* - множество точек/тире до знака "=" группируется ч/з скобочки \(группа\), и заменяется на "s/группа /". Скобки и / экранируются обратной наклонной "\". "\1" - использование группы при подстановке, "/g;" - выполнять замену глобально, для каждого вхождения. Точка с запятой завершают формулу и позволяют перейти на новую строку.
3-я строка - вторая замена, без формул, текст в текст: ";" -> "/g;\n"
4-я строка, снова без формул . -> \.
Обилие обратных косых стало следствием замысла - построить через sed формулу для самоё себя. Мы коды алфавита превратили в формулы контекстной замены. Поэтому последней командой убираем пустые строки и сохраняем длинный, по строке на букву, список замен в файл morse-sed.
Показанный скрипт представляет по сути команды редактирования текста описания кода Морзе. Если бы описание было другим, скрипт пришлось бы также поправить. Например, в оригинальном задании буквы шли в алфавитном порядке. В том случае пришлось ещё добавить сортировку - длинные коды должны заменяться первыми, это понятно.
Для запуска переводчика подаём на вход текст из нашего специфического алфавита, буквы разделяем пробелами. Пример:
echo '.. -. ... .-. -.. --. .... ..-. .. ..-... ...... .-.. ! text'|sed -f morse-sed
етианмсуеэ5р! text