Next Previous Contents

Unix Review Column 23 -- Редактирование файлов на месте

Randal Schwartz

Декабрь 1998

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

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

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

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

        open IN, "log"
          or die "Cannot open: $!";
        open OUT, ">clean-log"
          or die "Cannot create: $!";
        while (<IN>) {
          print OUT $_ unless /^warning:/i;
        }

В этом коде у нас имеется два вызова функции open, а затем выполняется цикл. Каждый раз проходя через цикл while, новая строка оказывается в переменной $_. Затем она проверяется с помощью регулярного выражения, и если она не соответствует данному выражению, то строка выдается в выходной файл.

Хорошо, этот код работает хорошо, но мы дважды использовали дисковое пространство. Давайте реши эту проблему добавлением оператора переименования в конец блока. Мы можем также избежать использования нового имени файла, используя соглашение, которое добавляет символ (~) в конец имени файла, означая ``временный или резервный файл''.

        my $name = "log";
        open IN, "<$name"
          or die "Cannot open: $!";
        open OUT, ">$name~"
          or die "Cannot create: $!";
        while (<IN>) {
          print OUT $_ unless /^warning:/i;
        }
        close IN;
        close OUT;
        rename "$name~", $name
          or die "Cannot rename: $!";

Здесь я использовал в качестве параметра имя в переменной $name и выходной файл, чье имя равно имени основного файла с добавлением символа тильда. Запомните последние шаги: мы переименовываем временный файл в оригинальный файл, таким образом удаляя оригинальный файл. Это получается лучше. Теперь у меня есть скрипт, который я мог запускать для того, что бы сделать файл короче и чтобы он содержал только то, что мне необходимо!

Хммм. Что, если скрипт сделает ошибку? Было бы хорошо иметь резервную копию исходных данных, на которые я мог бы взглянуть когда необходимо. Чтобы выяснить что изменилось, я мог бы тогда запустить программу diff для старого м нового файлов. Давайте выполним действия в том же порядке: сначала переименуем файл, затем выдадим выбранные строки в новый файл с исходным именем:

        my $name = "log";
        rename $name, "$name~"
          or die "Cannot rename: $!";
        open IN, "<$name~"
          or die "Cannot open: $!";
        open OUT, ">$name"
          or die "Cannot create: $!";
        while (<IN>) {
          print OUT $_ unless /^warning:/i;
        }

Хмм. Это выглядит хорошо. Теперь у меня есть резервный файл (с символом тильда в имени) и новый файл с данными. Давайте сделаем скрипт более удобным; нет никакой причины помещать имя файла в скрипт. Давайте будем считывать имя файла с командной строки (@ARGV) и сделаем возможным задание нескольких файлов из командной строки:

        foreach $name (@ARGV) {
          rename $name, "$name~"
            or die "Cannot rename: $!";
          open IN, "<$name~"
            or die "Cannot open: $!";
          open OUT, ">$name"
            or die "Cannot create: $!";
          while (<IN>) {
            print OUT $_ unless /^warning:/i;
          }
        }

Мы получили даже более мощный скрипт, добавив несколько строк кода. Но возможно я ввел вас в заблуждение. Larry Wall (создатель Perl) вероятно делал эти операции работы с текстом достаточно раз, что он научил Perl делать это сразу. Режим редактирования по месту сразу обрабатывает такие случаи. Если установлена переменная $^I (либо внутри программ, либо ключом командной строки -i), то открытие файла с помощью оператора чтения (<>) автоматически выполняет аналогичную операцию:

        $^I = "~";
        while (<>)
          print unless /^warning:/i;
        }

Намного легче. И небрежно выполняет теже операции, что и код приведенные выше. Заметьте, что мне не нужно было выполнять цикл по списку @ARGV; Это подразумевается оператором чтения. Значение переменной $^I (обычно undef) добавляется к именам файлов, заданным в @ARGV, для того, чтобы создать резервные файлы.

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

Файловая система UNIX позволяет нескольким процессам получить доступ на запись в файл. Но до тех пор, пока у нас не будет способа координации записи в файл, данные в файле будут разрушены.

Наиболее общим способов предотвращения разрушения данных является использование блокировку файла. В Perl, это легко достигается используя оператор flock, названный по имени системного вызова, введенного разработчиками UNIX BSD. Даже, хотя в вариантах System V UNIX такой функции не существует, оператор Perl превращается в соответствующие низкоуровневые операции для выполнения совместимых действий, так что этот оператор достаточно переносим.

Вот базовые правила:

  1. Программы, которые хотят читать из файла, должный открыть файл, а затем использовать flock HANDLE, 1.
  2. Программы, которые хотят читать из файла и записывать в файл должны открыть файл, а затем использовать flock HANDLE, 2.
  3. Вызов flock будет блокировать файл до тех пор пока он доступен, в это время требуемые операции могут быть выполнены с некоторой степенью безопасности.
  4. После выполнения операций уберите блокировку путем закрытия файлового дескриптора. (Вы также можете выполнить отмену блокировку без закрытия файла, но вы должны точно знать, что вы делаете. Самым простым способом является простое закрытие файла).

Заметьте, что блокирование файла только взаимодействует с другими процессами, которые также блокируют файл. Если процесс выбирает такое действие, он может совместно открыть файл на чтение или запись, и таким образом работать с файлом. Поэтому этот метод называется консультативной (advisory) блокировкой. (Hекоторые варианты UNIX реализуют обязательную (mandatory) блокировку, но это еще не так распространено).

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

Первым и быстрым решением является загрузка всех данных в память и запись их без ненужных нам предупреждение:

        my $name = "log";
        open LOG, "+<$name"
          or die "Cannot open $name: $!";
        flock LOG, 2;
        @data = grep !/^warning:/i, <LOG>;
        seek LOG, 0, 0;
        trunc LOG, 0;
        print LOG @data;
        close LOG;

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

После получения блокировки, мы можем изменять файл как захотим. Мы заполняем массив @data строками из файла, которые нас интересуют, а затем переписываем файл перемещаясь в его начало, задавая обрезая его и затем выдавая данные в файл. Завершающий оператор close освобождает файл, так что другие утилиты могут получить новую блокировку для файла и продолжать запись в файл.

Хорошо, это было достаточно остроумно, но заметьте, что теперь мне необходимо все (новые) данные хранить в памяти. Для достаточно большого файла это становится проблемой. Так, что давайте будем редактировать файл ``на месте''. Это немного хитроумно, так что я дам вам программу, а потом объясню как она работает:

        my $name = "log";
        open LOG, "+<$name"
          or die "Cannot open $name: $!";
        flock LOG, 2;
        my $write_pos = 0;
        while (<LOG>) {
          unless (/^warning:/i) {
            my $read_pos = tell LOG;
            seek LOG, $write_pos, 0;
            print LOG $_;
            $write_pos = tell LOG;
            seek LOG, $read_pos, 0;
          }
        }
        trunc LOG, $write_pos;
        close LOG;

Здесь я сопровождаю два указателя на файл: один на позицию чтения и на позицию записи. Позиция чтения отслеживается автоматически в цикле while. Позиция записи в начале начинается с нулевого значения, а затем запоминается в переменной $write_pos. Внутри цикла, когда я вижу запись, которую я хочу сохранить, я вычисляю текущую позицию чтения используя функцию tell, перемещаюсь в позицию записи, сохраняю необходимую запись и затем возвращаюсь в позицию чтения. Однажды пройдя по файлу, я могу сократить его размер до позиции записи, что я и делаю.

Этот код работает только потому, что я делаю файл короче, но он будет работать на огромных файлах, поскольку нам требуется память для хранения только одной строки.

Так что у вас есть программа. Много способов уменьшения количества данных встретятся вам далее. Наслаждайтесь.


Next Previous Contents