← Blog of oldlisper

cookbook:file-parsing

· 04.08.2010 04:08
· old wiki

Обработка файлов

Этот раздел посвящён автоматной обработке текстовых файлов - по линиям, с простыми заменами шаблонов в виде регулярных выражений. На ум сразу приходят иероглифы на shell или итераторы на python, но почему бы не делать такие вещи на CL? Посмотрим что получится, и ещё - я тут не пытаюсь быть кратким :) на sed всё это будет гора*здо короче :))

Итерация по строкам

Итак, для работы с файлами в CL существует макрос with-open-file о котором подробнее можно почитать в PCL, в нашем случае у нас будет два файла - исходный файл и видоизменённый. Поэтому сразу делаем макрос "с-двумя-файлами-делай-то-то":

(defmacro with-two-files ((stream-1 stream-2 file-1 file-2 &key (if-second-exists :overwrite)
                                                                (if-second-not-exist :create)
)

                          &body body
)

  `(with-open-file (,stream-1 ,file-1)
     (with-open-file (,stream-2 ,file-2 :direction :output
                                        :if-exists ,if-second-exists
                                        :if-does-not-exist ,if-second-not-exist
)

       ,@body
)
)
)

Тут всё так же как и с with-open-file - мы называем два файлы, и объявляем потоки которыми будем пользоваться в теле макроса:

(with-two-files (s1 s2 #p"my-file" #p"my-new-file")
  ;; работаем с потоками s1 и s2 - пишем/читаем туда/сюда
)

Можно было бы сделать названия потоков анафорическими (контекстными) переменными, это бы сократило код, но потребовывало бы от нас помнить какие именно переменные неявно (без всякого объявления) появляются связанными в теле with-two-files:

(defmacro with-two-files ((file-1 file-2 &key (if-second-exists :overwrite)
                                              (if-second-not-exist :create)
)

                          &body body
)

  `(with-open-file (^stream-1^ ,file-1)
     (with-open-file (^stream-2^ ,file-2 :direction :output
                                         :if-exists ,if-second-exists
                                         :if-does-not-exist ,if-second-not-exist
)

       ,@body
)
)
)

Теперь with-two-files требует только имена файлов и автоматически создает связь переменных ^stream-1^ и ^stream-2^ с потоками:

(with-two-files (#p"my-file" #p"my-new-file")
  ;; работаем с потоками ^stream-1^ и ^stream-2^
)

Такой получается интерфейс, макрос-класс. Никаких style-warning нет, но в данном случае такой способ не оправдан. Тогда смотрим дальше.

Теперь, когда у нас есть общий макрос для работы с двумя файлами, сделаем макрос следующего уровня - конвертации одного файла в другой. Первый вариант:

(defmacro convert-file (file-in file-out &key (filter #'(lambda (line)
                                                        line
)
)
)

  `(with-two-files (stream-in stream-out ,file-in ,file-out)
       (loop for line = (read-line stream-in nil)
                 while line do (format stream-out
                                       "~a~%"
                                       (funcall ,filter line)
)
)
)
)
)

Используем with-two-files и итерируем поток первого файла по линиям используя стандартный макрос loop; для каждой линии вызываем функцию фильтр, которая передаётся в конвертер в качестве аргумента. По-умолчанию функция фильтр - прямое отображение, - ничего не меняет, поэтому

(convert-file #p"my-file" #p"my-new-file")

работает как простое копирование.

Задачка 1 - перевод простых строк в другую фому

Попробуем вызвать convert-file с различными функциями фильтрации.

Пока неохота :)

Задачка 2 - удалить комментарий в начале файла

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

(defmacro convert-file (file-in file-out &key (at-begining-p t)
                                              (print-line-p  t)
                                              (filter #'(lambda (line)
                                                           line
)
)
)

  "Транслятор файл -> файл. Функция filter транслирует текущую строку и имеет доступ
к параметрам трансляции."

  `(with-two-files (stream-in stream-out ,file-in ,file-out)
     (let ((^at-begining-p^ ,at-begining-p)
           (^print-line-p^  ,print-line-p)
)

           ;; и т.д.
      (loop for line = (read-line stream-in nil)
                 while line do (let ((filtered (funcall ,filter line)))
                                 (if ^print-line-p^
                                     (format stream-out "~a~%" filtered)
)
)
)
)
)
)

И пример использования, функция delete-disclaimer удаляющая начальный комментарий в файле:

(defmacro delete-disclaimer (file-in file-out)
  `(flet ((is-comment-p (string)
            (if (< 0 (length string))
              (string= (subseq string 0 1) ";")
)
)
)

     (convert-file ,file-in ,file-out
                   ;; Определяем начальное состояние КА!
                  :at-begining-p t
                   :print-line-p  nil
                   :filter (lambda (line)
                             ;; Функция КА смотрит вокруг:
                            (if ^at-begining-p^
                                 (unless (is-comment-p line)
                                   ;; Функция КА меняет состояние транслятора:
                                  (setq ^print-line-p^ t
                                         ^at-begining-p^ nil
)

                                   line
)

                                 line
)
)
)
)
)