Sed в примерах. Часть 3

Поговорим о sed в новом качестве: переработка данных в стиле sed.

Мускулистый sed

Во второй статье этого цикла я на примерах показал как работает sed. Но только немногие из них действительно полезны. В последней статье пришло время изменить подход и правильно использовать sed. Я покажу вам несколько отличных примеров, которые не только демонстрируют мощь sed, но и выполняют несколько действительно искусных и удобных вещей. Я покажу как разработал sed скрипт, который конвертирует .QIF файл из программы для управления финансами Intuit`s Quicken в хорошо форматированный текстовый файл. Но сначала мы рассмотрим несколько более простых, но не менее полезных, скриптов sed.

Перевод текста

Наш первый практический скрипт будет конвертировать текст отформатированный в UNIX стиле в DOS/Windows формат. Как вы, наверное, знаете, текстовые файлы DOS/Windows имеют в конце каждой строки символ CR (carriage return - возврат каретки) и LF (line feed - перевод строки), а текстовые файлы UNIX содержат только символ LF. Иногда вам может понадобиться перенести немного UNIX текста в ОС Windows, поэтому этот скрипт выполнит необходимые операции по смене форматирования:

$ sed -e 's/$/\r/' myunix.txt > mydos.txt


В этом скрипте символ '$' внутри регулярного выражения обозначает конец строки, а символ возврата каретки '\r' указывает sed, что его нужно вставлять перед символом перевода строки. Итак, вставляем символ возврата каретки перед символом перевода строки и, вуаля, имеем пару CR/LF в конце каждой строки. Важно: символ '\r' будет заменен на CR только тогда, когда используется GNU sed 3.02.80 или более поздняя версия. Если у вас до сих пор не установлен GNU sed 3.02.80, то смотрите мою первую статью о sed, чтобы понять как это сделать (Sed в примерах. Часть 1).

Я не могу сказать, сколько раз я скачивал примеры скриптов или исходных кодов на C, чтобы обнаружить потом, что они отформатированны в DOS/Windows формате. В то время как многие программы не обращают внимание на текстовые файлы формата DOS/Windows с символами CR/LF, но некоторые программы определенно делают это -- наиболее заметной среди них выглядит командный интерпретатор bash, которого клинит сразу же, как только он встречает символ возврата каретки. Следующий пример использования sed выполнит преобразование текстового файла в формате DOS/Windows настоящий формат UNIX:

$ sed -e 's/.$//' mydos.txt > myunix.txt


Принцип работы этого sed скрипта прост: регулярное выражение замены совпадает с последним символом строки, которым оказывается символ возврата каретки. Мы заменяем его на "ничто", т.е. просто удаляем этот символ из результирующего вывода. Если вы используете этот скрипт и заметили, что последний символ каждой строки в выводе был удален, то полученный файл уже оказывается в формате UNIX. Тогда с ним ничего не придется делать!


Изменение порядка следования строк

Вот еще один маленький и удобный скрипт. Он изменяет порядок следования строк в файле на обратный, аналогично команде "tac", которая часто имеется в большинстве Linux дистрибутивов. Название "tac" может вызывать некоторое недоумение потому, что "tac" не меняет порядок следования символов в словах в строке (слева и справа), но меняет положение самих строк в файле (верх и низ). Применим утилиту "tac" к файлу:

foo
bar
oni
...

получим результат такого вида:

oni
bar
foo

Мы можем сделать то же самое с помощью вот такого вот скрипта sed:

$ sed -e '1!G;h;$!d' forward.txt > backward.txt


Этот скрипт особенно полезен в ОС FreeBSD: где нет утилиты "tac". Этот скрипт удобен. Давайте проанализируем его чтобы понять, как и что он делает.

Объяснение перестановки

Прежде всего, скрипт содержит три отдельных команды sed, разделенных символом ";": '1!G', 'h' и '$!d'. Пришло время разобраться, какие адреса используются в первой и третьей команде. Если в первой команде '1G' содержится адрес "1", то 'G' будет применена только к первой строке. Однако, за единицей стоит дополнительный символ '!', который означает отрицание адреса. Таким образом, команда 'G' будет применена ко всем строкам за исключением первой. Похожая ситуация и с третьей командой '$!d'. Если команда содержит адрес '$', то 'd' будет применена к последней строке файла (символ '$' - это простой способ адресовать последнюю строку). Однако, за символом '$' стоит '!', который означает отрицание адреса. Таким образом команда 'd' будет применена ко всем строкам за исключением последней. Теперь единственное, что нам осталось -- понять, что эти команды делают сами по себе.

Когда мы применяем наш скрипт перестановки к вышеуказанному файлу, первая выполняемая команда это 'h'. Эта команда говорит sed копировать содержимое области шаблона (буфер, хранящий строку, над которой производятся вычисления) в удерживаемую область (временный буфер). Затем выполняется команда 'd', которая удаляет слово "foo" из области шаблона так, что оно не выводится на экран после того, как все команды над текущей строкой будут выполнены.

Теперь вторая строка. После того, как слово "bar" будет скопировано в область шаблона, исполнится команда 'G', которая добавит содержимое удерживаемой области ("foo\n") в область шаблона ("bar\n"). В итоге в области шаблона у нас будет содержаться "bar\nfoo\n". Затем команда 'h' возвращает этот текст для сохранности обратно в удерживаемую область, а 'd' удаляет текст из области шаблона, поэтому он не выводится.

Над последней строкой ("oni") производятся те же действия, за исключением того, что содержимое области шаблона не удаляется (из-за '$!', находящихся перед командой 'd'), а выводится в стандартный поток вывода (stdout).

Настало время сделать воистину сложное преобразование с помощью sed.

Магия sed QIF

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

Я разработал хорошую маленькую программу для сведения баланса чековой книжки (с использованием awk), которая считала баланс, разбирая все мои транзакции, записанные обычным текстом. Позже я ее слегка доработал, т.е улучшил ее настолько, что смог с ее помощью отслеживать различные категории дебета и кредита, так же, как это делает Quicken. Однако, была еще одна возможность, которую я хотел добавить. Недавно я перевел свои счета в банк c online доступом. В один прекрасный день я получил уведомление, что теперь у меня есть возможность скачивать информацию о моих счетах в формате .QIF программы Quicken (Описание формата QIF можно найти пройдя по ссылке http://ru.wikipedia.org/wiki/QIF. Прим. пер. ). Недолго думая, я решил, что будет действительно здорово, если я смогу конвертировать эти данные в текстовый формат.

История двух форматов

Сначала я расскажу вам о том, как выглядит формат моего текстового файла checkbook.txt, а затем вы увидите содержимое файлов в формате .QIF.
Checkbook.txt:

28 Aug 2000 food - - Y Supermarket 30.94
25 Aug 2000 watr - 103 Y Check 103 52.86

В файле моего формата все поля разделены одним или более символом табуляции и каждая транзакция занимает одну строку. После даты следует поле, определяющие тип расхода (или "-" если это был доход). Третьим полем идет тип дохода (или "-" если это был расход). Далее следует поле с номером чека (снова "-" если поле пустое). Строку транзакции завершают поле ("Y" или "N"), комментарий и количество потраченных/полученных средств в долларах. Теперь мы готовы к тому, чтобы взглянуть на формат .QIF файла. Когда я посмотрел содержимое моего загруженного файла в текстовом редакторе, то я увидел следующее:

!Type:Bank
D08/28/2000
T-8.15
N
PCHECKCARD SUPERMARKET
^
D08/28/2000
T-8.25
N
PCHECKCARD PUNJAB RESTAURANT
^
D08/28/2000
T-17.17
N
PCHECKCARD SUPERMARKET
<u>Замечание:</u>

Очевидно, что существует несколько разных версий формата QIF, или ваш банк может использовать сильно отличающийся формат для транзакций. Если вы хотите следовать действиям, описываемым в статье, можете взять вышеописанный кусочек QIF файла и сохранить его как текст.

После просмотра файла не составило особой сложности описать его формат -- игнорируя первую строку файла, получается следующее:
<code>
D<дата>
T<расход/приход по транзакции>
N<номер чека>
P<описание>
^ (разделитель поля)

Начинаем процесс

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

Чтобы начать, я создал файл и назвал его qiftrans.sed и начал "массировать" данные:

1d
/^^/d
s/<<:cntrl:>>//g

Первая команда '1d' удаляет первую строку, а вторая команда удаляет эти назойливые символы '^' из вывода. Последняя строка скрипта удаляет любые управляющие символы, которые могут существовать в файле. Поскольку я имею дело с чужим форматом файла, я решил исключить всякий риск любых встреченных в файле управляющих символов. Пока что все идет хорошо. Усиливаем действие скрипта:

1d
/^^/d
s/<<:cntrl:>>//g
/^D/ {
s/^D\(.*\)/\1\tOUTY\tINNY\t/
s/^01/Jan/
s/^02/Feb/
s/^03/Mar/
s/^04/Apr/
s/^05/May/
s/^06/Jun/
s/^07/Jul/
s/^08/Aug/
s/^09/Sep/
s/^10/Oct/
s/^11/Nov/
s/^12/Dec/
s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
}

Первым делом я добавил адрес '/^D/', которому в файле формата QIF соответствует поле даты 'D'. Как только sed найдет совпадение с адресом, то над буфером шаблона будут выполняться команды, заключенные в фигурные скобки в порядке их расположения. И так с каждой прочитанной строкой.

Первая же строка, которая соответствует адресу и будет обработана командами в фигурных скобках, выглядит так:

D08/28/2000

А результат обработки выглядит так:

08/28/2000 OUTY INNY

Конечно, этот формат пока не идеален, но это уже хорошо. Мы улучшим содержимое буфера шаблона в дальнейшем. Следующие 12 строк создают сетевой эффект трансформации даты в трех-буквенный формат, а три последний строки удаляют три слэша из даты. Таким образом в результате обработки получим такую строку:

28 Aug 2000 OUTY INNY

Поля OUTY и INNY зарезервированы как метки, которые будут заменены на полезные данные позже. Я не могу записать на их место корректные данные прямо сейчас потому, что, если сумма будет отрицательной, то я захочу заменить OUTY и INNY на "misc" и "-" соответственно. Но если сумма будет положительной, я захочу заменить эти метки на "-" и "inco". Но поскольку сумма еще не прочитана, то мне приходится использовать эти метки до поры до времени.

Усовершенствование

Настало время улучшить скрипт:

1d
/^^/d
s/<<:cntrl:>>//g
/^D/ {
s/^D\(.*\)/\1\tOUTY\tINNY\t/
s/^01/Jan/
s/^02/Feb/
s/^03/Mar/
s/^04/Apr/
s/^05/May/
s/^06/Jun/
s/^07/Jul/
s/^08/Aug/
s/^09/Sep/
s/^10/Oct/
s/^11/Nov/
s/^12/Dec/
s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
N
N
N
s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/
s/NUMNUM/-/
s/NUM\(<0-9>*\)NUM/\1/
s/\(<0-9>\),/\1/
}

Следующие несколько строк немного сложнее, поэтому рассмотрим то, что они делают подробно. Первое, что у нас есть --три команды 'N', записанных по одной в отдельной строке. Команда 'N' указывает sed, что нужно прочитать следующую строку и добавить ее в буфер шаблона. Поскольку нам нужно прочитать следующие три строки и добавить их в буфер у нас и используется три команды 'N'. После отработки строки данных этими командами результат будет такой:

28 Aug 2000 OUTY INNY \nT-8.15\nN\nPCHECKCARD SUPERMARKET

Ну вот, теперь буфер шаблона sed стал выглядеть безобразно -- нужно удалить лишние символы перехода на новую строку и выполнить некоторое дополнительное форматирование. Вот регулярное выражение:

'\nT.*\nN.*\nP.*'

Этот шаблон будет совпадать с символом новой строки, за которым следует символ 'T', затем следует ноль или более любых символов, затем снова идет символ новой строки, потом символ 'N', и любое количество любых символов, за тем снова (и в последний раз) идет символ перехода на новую строку, за которым следует символ 'P' и за ним уже следует любое количество любых символов. Ух! Это регулярное выражение будет совпадать со всем содержимым тех трех строк, которые мы только что добавили в буфер шаблона. Но мы же хотим еще и переформатировать эту часть, не заменяя ее целиком. Сумма в долларах, номер чека (если есть) и описание должны оказаться в строке замены. Чтобы сделать это, мы окружили такие "интересные места" круглыми скобками. Они в свою очередь экранированы обратными слэшами, поэтому теперь мы может ссылаться на них в нашей строке замены (используя '\1', '\2' и '\3' для того, чтобы указать sed где необходимо вставить их). Вот завершенная команда:

s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/


Эта команда превратит нашу строку в это:

28 Aug 2000 OUTY INNY NUMNUM Y CHECKCARD SUPERMARKET AMT-8.15AMT

Хотя теперь эта строка выглядит лучше, есть несколько вещей, которые на первый взгляд кажутся... хм... интересными. Для начала это глупая строка "NUMNUM" -- для чего она? Вы поймете это, если просмотрите следующие две строки скрипта sed, которые заменят "NUMNUM" на "-", а "NUM""NUM" на . Как видите, обрамление номера чека глупым тегом позволяет нам удобно вставить символ "-" если поле пустое.


Последние штрихи

Последняя строка удаляет запятую, за которой идет число. Например, таким образом я преобразую строку с суммой, которая может выглядеть как "3,231.00" в строку "3231.00". Последний вариант я и использую. Теперь можно посмотреть на окончательный вариант скрипта sed:

1d
/^^/d
s/<<:cntrl:>>//g
/^D/ {
s/^D\(.*\)/\1\tOUTY\tINNY\t/
s/^01/Jan/
s/^02/Feb/
s/^03/Mar/
s/^04/Apr/
s/^05/May/
s/^06/Jun/
s/^07/Jul/
s/^08/Aug/
s/^09/Sep/
s/^10/Oct/
s/^11/Nov/
s/^12/Dec/
s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
N
N
N
s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/
s/NUMNUM/-/
s/NUM\(<0-9>*\)NUM/\1/
s/\(<0-9>\),/\1/
/AMT-<0-9>*.<0-9>*AMT/b fixnegs
s/AMT\(.*\)AMT/\1/
s/OUTY/-/
s/INNY/inco/
b done
:fixnegs
s/AMT-\(.*\)AMT/\1/
s/OUTY/misc/
s/INNY/-/
:done
}

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

/AMT-<0-9>*.<0-9>*AMT/b fixnegs


Эта строка содержит команду ветвления, которая соответствует формату "/регулярное выражение/b метка". Если буфер шаблона совпадает с регулярным выражением, то sed переходит к метке fixnegs. Вы должны легко найти эту метку, которая выглядит в тексте скрипта как ":fixnegs". Если регулярное выражение не совпадет с содержимым буфера, то команды будут выполняться в обычном порядке.

Поскольку теперь вы понимаете, как работает команда сама по себе, давайте посмотрим на ответвления. Если взглянуть на регулярное выражение, относящееся к ответвлению, то вы увидите, что оно совпадает со строкой 'AMT', за ней следует символ '-', за которой следует любое количество цифр, далее идет любой символ, затем любое количество цифр и снова строка 'AMT'. Уверен, что вы поняли, что это регулярное выражение относится исключительно к отрицательной сумме. Ранее мы окружили нашу сумму строками 'AMT' и поэтому легко сможем найти ее позднее. Поскольку это регулярное выражение совпадет только с отрицательной суммой, которая начинается со знака '-' соответственно, то ответвление выполнения команд происходит только тогда, когда мы имеем дело с дебетом. А если у нас дебет, то OUTY должно быть заменено на 'misc', INNY должно быть заменено на '-', а знак минус перед суммой дебета должен быть удален. Если вы просмотрите текст скрипта, то вы увидите, что это действительно так. Если же ответвления не происходит, то OUTY заменяется на '-' и INNY заменяется на 'inco'. Мы закончили! Теперь результат обработки выглядит отлично:

28 Aug 2000 misc - - Y CHECKCARD SUPERMARKET 8.15

Избегайте путаницы

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

Источник: welinux.ru
Оригинал: funtoo.org/wiki




Вас также может заинтересовать:

Sed в примерах. Часть 2
Sed в примерах. Часть 1. Или знакомство с мощным текстовым редактором Sed из мира UNIX
Всё что нам надо - это мозг! Часть 2!
Регулярные выражения, Часть I
Регулярные выражения, Часть II
Отчет по работе с Я.Вебмастер - часть Два.