Next Previous Contents

Unix Review Column 3 -- Регулярные выражения

Randal Schwartz

Июль 1995

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

Одной из многих сильных сторон Perl является возможность легкого манипулирования текстовыми строками. Это особенно полезно, так как манипуляции со строками являются ядром большинства писанных на коленке программ в типичной среде UNIX-инструментария.

В Perl многие операции с текстовыми строками построены на использовании ``регулярных выражений''. Многие люди, с которыми я разговаривал, считают, регулярные выражения пугающими --- тёмным, колдовским языком на котором говорят наиболее посвящённые адепты и то, только шёпотом. К счастью, в следующих нескольких параграфах я вынесу искусство регулярных выражений на свет. (Конечно, после этого моя карточка посвящённого будет аннулирована, но я должен рискнуть.)

Регулярное выражение есть ни что иное, как шаблон, который выбирает класс подходящих под него строек, отличая их от строек, которые не подходят под этот шаблон. Например, регулярное выражение /abc/ соответствует всем строкам, в которых есть подряд буквы ``a'', ``b'' и ``c'' именно в этом порядке. (Видите, это не так уж страшно). Указание последовательности из букв для поиска подстроки довольно обычно в программах на Perl:

        foreach $i (@somelist) {
                if ($i =~ /abc/) {
                        print "$i contains abc\n";
                }
        }

Здесь каждый элемент @somelist проверяется один раз. Для каждого элемента, который соответствует регулярному выражению ``abc'' печатается, что он содержит подстроку ``abc'' где-то внутри строки.

Регулярные выражения могут делать больше, чем просто проверять на точное совпадение подстроки. Зачастую, они содержат метасимволы, которые позволяют регулярным выражениям подходить к любому из возможного списка символов в определённой позиции. Например, символ ``.'' в регулярном выражении соответствует любому строковому символу, исключая символ новой строки. То есть, регулярное выражение ``a.c'' соответствует не только abc, но и acc, adc, afc, azc, a+c, ... вот такая картина.

Если вы хотите быть более разборчивым, вы можете использовать ``класс символов''. Класс символов заключается в квадратные скобки. Класс ``[a-z]'' в регулярном выражении соответствует любой строчной букве латинского алфавита. То есть, ``[b-f].d'' соответствует bed, fed, cad, но не kid. Класс ``[aeiou]'' соответствует всем строчным гласным латинским буквам.

Класс символов может так же описывать, что вы не хотели бы видеть в соответствии, просто включите знак ``стрелка вверх'' -- ``^'' первым символом внутри скобок. Например, класс ``[\^a-z]'' говорит, что приемлимо всё, кроме строчных символов латинского алфавита. Таким образом, ``a[^a-z]'' соответствует a+c, a*c, a=c, но не abc или agc.

Но, даже с символами и классами символов вы всё ещё не можете описывать соответствия с изменяющейся длиной. Если вы хотите, чтобы между ``a'' и ``c'' могло быть любое количество символов, то вы должны использовать квантификаторы. Квантификатор пишется после регулярного выражения (или его части) и позволяет выражению повторяться так много раз, как позволяет квантификатор.

Один из самых часто используемых квантификаторов, это ``звёздочка'' -- ``*''. Этот квантификатор (используемый почти во всех утилитах UNIX, которые используют регулярные выражения) означает, ``непосредственно предшествующее выражение может встретиться ноль или больше раз''. Так, ``fre*d'' соответствует frd, fred, freed, freeed, .... Заметьте, что это отличается от того shell делает с ``*'', shell подразумевает, в терминах регулярных выражений, ``.*''.

Немного менее частый, но не менее полезный квантификатор, это ``плюс'' -- ``+'', который означает ``непосредственно предшествующее выражение может встретиться один или больше раз'' . Так, ``fre+d'' соответствует fred, freed, freeed, ..., но не frd.

Существует ещё ``знак вопроса'' -- ``?'', означающий ``непосредственно предшествующее выражение может встретиться ноль или один раз''. Так, ``ab?c'' соответствует ac или abc, но не abbc. (И Ab?a никогда не будет соответствовать старой шведской группе ``Abba''.) Обратите внимание, что знак вопроса в регулярном выражении означает совершенно не то, как его интерпретирует shell. Знак вопроса в shell эквивалентен ``.'' в регулярном выражении.

Уже даже с такими возможностями регулярных выражений мы могли бы делать очень много работы, выбирая одни строки и игнорируя другие. Но даже более мощные операции становятся возможными, когда мы можем запомнить ту часть строки, которая соответствует части регулярного выражения. Эта часть заключается в круглые скобки и помещается в память. Вы можете поместить любое количество правильно сбалансированных скобок в регулярном выражении. Когда строка подходит под регулярное выражение, части строки, соответствующие частям регулярного выражения, заключённых в пары круглых скобок, запоминаются в ``памяти''. Эта память доступна двумя способами: через такое же регулярное выражение используя нотацию обратной косой черты, или позже в программе (до следующего регулярного выражения), используя нумерованные скалярные переменные, доступные только для чтения.

Для примера, скажем, мы разбираем вывод UNIX команды who:

        merlyn   tty14   May 01 14:42 (netserver5.omni.net)

Мы хотим выделить имя пользователя (первая колонка) и имя терминала (вторая колонка), но нас не интересует остаток строки. Если данные в переменной $wholeline, у нас получается что-то вроде:

   
        if ($wholine =~ /([a-z]+) +([a-z0-9]+)/) {
                $user = $1;
                $tty = $2;
        }

Первая пара скобок выделяет имя пользователя (соответствует [a-z]+), которое будет сохранено в $1, в случае соответствия строки регулярному выражению. Это значение впоследствии сохраняется в переменной $user. Таким же образом, имя терминала, сохранённое в $2, запоминается в $tty.

Эти символьные классы выглядят жутковато и, на самом деле, сломаются на именах пользователей, содержащих цифры. Perl предоставляет несколько общих аббревиатур классов символов, чтобы помочь уменьшить уровень шума. \s соответствует любому пробельному символу (пробел, табуляция, перевод строки), а \S соответствует любому непробельному символу (всё, что не соответствует \s).

В общем, можно написать так:

        if ($wholine =~ /(\S+)\s+(\S+)/) {
                $user = $1;
                $tty = $2;
        }

Таким образом, мы достигли обобщённости и улучшили читабельность одновременно. Можно даже проще, $1 и $2 могут составить список, и быть присвоенными напрямую $user и $tty как список:

        if ($wholine =~ /(\S+)\s+(\S+)/) {
                ($user,$tty) = ($1,$2);
        }

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

  
        if (/(\S+)\s+(\S+)/) {
                ($user,$tty) = ($1,$2);
        }

Это немного укоротило программу. Но подождите ещё! Результатом оператора соответствия в списковом контексте является список как у нас ($1,$2). Таким образом, мы можем поставить присвоение как часть выражения соответствия:

  
        if (($user,$tty) = /(\S+)\s+(\S+)/) {
                # (it matched)
        }

Теперь у нас ничего не осталось даже в теле if! Мы готовы использовать $user и $tty!

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

  
        foreach $_ (`who`) { # one line each
                ($user,$tty) = /(\S+)\s+(\S+)/;
                $count{$user}++; # note count
                $where{$user}{$tty}++; # note ttys
        }
        foreach (sort keys %count) {
                if ($count{$_} > 1) {
                        print "user $_ is logged in at: ";
                        @where = sort keys %{$where{$_}};
                        print "@where\n";
                }
        }

Первый цикл собирает данные. Каждая строка, выводимая who (представляет одну сессию), разбирается на имя пользователя и имя терминала. Имена пользователей считаются в ассоциативном массиве %count, чтобы мы могли учесть пользователей, имеющих больше одной сессии. Также, $user и $tty сохраняются в ассоциативном массиве ассоциативных массивов, называемом %where. (Такая возможность недоступна в старых версиях Perl, так что, если вы попробовали и у вас не заработало, обновите версию Perl!)

После того, как первый цикл закончится, у нас будет счётчик сессий в %count и подробная информация в %where. Второй цикл сканирует массив %count используя сортированые ключи из %count (имена пользователей). Если значение счётчика сессий пользователя больше, чем один, значит пользователь вошёл в систему больше, чем один раз. В этом случае, имя пользователя печатается вместе со всеми именами терминалов, где он вошёл в систему. (Запись %{$where{$_}} означает ассоциативный массив, на который ссылается элемент ассоциативного массива $where{$_} --- да, это жуткий синтаксис, но не хуже, чем всё остальное в Perl.)

В следующих выпусках я продолжу рассказ о интересных использованиях и особенностях регулярных выражений. Следите за рекламой!


Next Previous Contents