Сведения о документе

История одного скрипта

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

Важным свойством таких программ является, на мой взгляд, возможность быстрого получения первых полезных результатов. Нередко 2–3-строчный скрипт способен сэкономить десятки минут монотонной ручной деятельности.

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

Я увлекаюсь фотографией. Последнее время — цифровой. Довольно быстро я столкнулся с тем, что просто сложить всё отснятое в один каталог — не самый оптимальный вариант размещения материала, так как по мере роста количества снимков найти нужный становится непросто. А хотелось бы также показывать, например, тематические подборки снимков гостям, легко записывать для них тематические компакт-диски.

Сначала я поискал среди готовых программ поддержки фотографических коллекций.

Первое решение было очень простым: надо разложить снимки по каталогам в соответствии с датой съёмки. Поскольку часть снимков подвергалась редактированию, дата модификации файла в качестве даты не годилась, и я использовал дату из EXIF (во многих графических форматах существует возможность записывать те или иные сведения об изображении, так большинство цифровых фотокамер сохраняет данные о параметрах съёмки, включая и дату).

На тот момент (2004 год) в результатах поиска apt-cache search exif нашлась лишь одна очевидная программа, могущая мне помочь, но тогда мне и не требовалось большего. Вот первый вариант скрипта:


#!/bin/bash
for n in *.jpg;
  do
    date=`exif $n | grep 'Date and Time  '| cut -d'|' -f2| cut -d' ' -f1| sed 's/:/-/g'`
    if [ -L ../days/$date/$n ]; then true;
    else
	mkdir -p ../days/$date
	ln -s `pwd`/$n ../days/$date/$n
    fi
done

Пример 1. Скрипт, сортирующий фотографии по дате съёмки

Первая строка указывает операционной системе на тот интерпретатор, которому следует передать для выполнения данную программу. Далее всё достаточно прозрачно, за исключением, пожалуй, волшебной строки: date=`exif $n | grep 'Date and Time '| cut -d'|' -f2| cut -d' ' -f1|sed 's/:/-/g'`. В этой строке производится извлечение из файла и преобразование даты с последующим помещением её в переменную оболочки $date.

Получена она была примерно следующим путём (для понимания смысла используемых команд и их аргументов рекомендую заглянуть в соответствующие man-страницы):


[avb@boyarsh-book Animals]$ exif dsc_7622.jpg | grep 'Date and Time  '
Date and Time       |2006:01:21 22:06:34
[avb@boyarsh-book Animals]$ exif dsc_7622.jpg | grep 'Date and Time  '| cut -d'|' -f2
2006:01:21 22:06:34
[avb@boyarsh-book Animals]$ exif dsc_7622.jpg | grep 'Date and Time  '| cut -d'|' -f2|cut -d' ' -f1
2006:01:21
[avb@boyarsh-book Animals]$ exif dsc_7622.jpg | grep 'Date and Time  '| cut -d'|' -f2|cut -d' ' -f1| sed 's/:/-/g'
2006-01-21

Пример 2. Команды для сортировки фотографий по дате

Однако через некоторое время этот скрипт перестал меня устраивать: я стал фотографировать в формат RAW, преобразовывая результат в JPEG при помощи программы ufraw. Доступная на тот момент её версия (а также версия, входящая в Compact 3.0) не поддерживала перенос данных EXIF из RAW-файла в получаемый из него JPEG. Пришлось искать иной способ переноса. Несмотря на то что RAW-файлы от моей фотокамеры являются по сути TIFF-файлами специального вида, и дополнительная информация в них хранится стандартным образом и доступна для ряда программ, работающих с TIFF, применить их для переноса информации не удалось. Поиск в Сети тоже не сразу дал результат, так как мне не удалось придумать хороший запрос. Однако через некоторое время подходящий инструмент был найден. Он содержится в пакете perl-Image-ExifTool.

После нескольких проб и ошибок скрипт приобрёл примерно такой вид:


#!/bin/bash
NEFPATH=$NEFPATH./
for n in *.jpg;
  do
    date=`exif $n | grep 'Date and Time  '| cut -d'|' -f2| cut -d' ' -f1| sed 's/:/-/g'`
   if [ $date ]; then true;
   else
	nef=`basename $n .jpg`   
	nef=$NEFPATH$nef.nef
	   if [ -r $nef ]; then 
             exiftool -overwrite_original -TagsFromFile $nef $n
	     echo $n
	     date=`exif $n | grep 'Date and Time  '| cut -d'|' -f2| cut -d' ' -f1| sed 's/:/-/g'`
	     if [ -w $nef ];then
	     mv $nef ../nef/
	     fi
	   fi
   fi
    if [ -L ../days/$date/$n ]; then true;
    else
	mkdir -p ../days/$date
	ln -s `pwd`/$n ../days/$date/$n
    fi
done

Пример 3. Скрипт для сортировки фотографий, при необходимости получающий информацию из RAW

Идея добавленного фрагмента состоит в том, что если в данных EXIF не содержится дата, и в текущем каталоге присутствует файл с таким же именем, но имеющий расширение. nef (RAW-файлы от камер Nikon имеют именно такое расширение), производится копирование метаданных из RAW-файла в соответствующий JPEG, после чего RAW-файл, если возможно, переносится в другой каталог. Для того чтоб перенести метаданные EXIF с RAW-файлов, выгруженных на оптические носители либо находящихся в другом каталоге, я реализовал возможность указать каталог, содержащий RAW-файлы, при помощи переменной командной оболочки.

Вскоре я понял, что простое раскладывание фотографий по каталогам в соответствии с датой съёмки — не идеальное решение. Куда удобнее иметь архив, отсортированный по месту съёмки. Правда, место съёмки в EXIF не содержится. Таким образом, нужен некий интерфейс для классификации фотографий по месту съёмки. Эта задача тоже нашла простое решение. Для просмотра фотографий я использую программу gqview, в которой имеется возможность вызывать горячими клавишами до 10 разных редакторов для обработки выбранных файлов. Надо только написать соответствующий редактор, позволяющий указать место съёмки и установить его для заданных файлов. Написание такого редактора на языке командной оболочки оказалось элементарным:


#!/bin/bash
location=`Xdialog --stdout --inputbox Location 0x0`
exiftool -overwrite_original -Location=$location $@

Пример 4. Редактор для сортировки фотографий по месту съёмки

А вот сортирующий скрипт с поддержкой обработки поля Location:


#!/bin/bash
NEFPATH=$NEFPATH./

for n in *.jpg;
  do
    echo -n .
    date=`exif $n | grep 'Date and Time  '| cut -d'|' -f2| cut -d' ' -f1| sed 's/:/-/g'`
   if [ $date ]; then true;
   else
	nef=`basename $n .jpg`   
	nef=$NEFPATH$nef.nef
	   if [ -r $nef ]; then 
             exiftool -overwrite_original -TagsFromFile $nef $n
	     echo $n
	     date=`exif $n | grep 'Date and Time  '| cut -d'|' -f2| cut -d' ' -f1| sed 's/:/-/g'`
	     if [ -w $nef ];then
	     mv $nef ../nef/
	     fi
	   fi
   fi
   if [ -r pics/$n ]; then true;
      else convert -resize 1024x768 -quality 90 $n pics/$n;
   fi

  location=`exiftool -s -s -s -Location $n`
  year_month=`exiftool -s -s -s -d %Y-%m -CreateDate $n`
    if [ $location != '-' ]; then
	if [ -L ../locations/$location/$year_month/$n ]; then true;
	else
		mkdir -p ../locations/$location/$year_month
		ln -s `pwd`/pics/$n ../locations/$location/$year_month/$n
	fi
  fi
done

Пример 5. Скрипт с поддержкой сортировки по месту съёмки

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

Всё бы хорошо, но работать этот скрипт стал ужасно медленно. Ещё бы — на обработку каждого изображения приходится 2–3 инициализации интерпретатора Perl (exiftool написан именно на нём) и масштабирование изображения. Впрочем, масштабирование производится для каждого файла только один раз, а вот 2–3 запуска exiftool производятся при каждом запуске скрипта. Через некоторое время мне надоело каждый раз ждать результатов обработки по несколько минут, и я предпринял очевидную оптимизацию: переписал скрипт на Perl. Заодно с оптимизацией я добавил в него довольно много новой функциональности: обработка нескольких JPEG-файлов, полученных из одного RAW (с именами dsc_1234-1.jpg, dsc_1234-final.jpg и т. п.), извлечение полноразмерного изображения низкого качества с целью отбора при помощи утилиты nefextract и др. Ниже приводится окончательный на сегодняшний день вариант скрипта. Он, разумеется, не является примером хорошего кода, но он решает свою задачу и экономит время. Хотя, возможно, со временем, я оптимизирую в нём хотя бы самые вопиющие места.


#!/usr/bin/perl

use strict;

use Image::ExifTool;

my $nefpath = $ENV{'NEFPATH'}?$ENV{'NEFPATH'}:'./';

opendir DIR,'.';
my @all = readdir(DIR);
my @nefs = grep { /\.(nef)$/ } @all;
closedir DIR;

foreach ( @nefs ) {
    my $name =$_;
    my $jpg=$name;
    $jpg =~ s/\.nef$/\.jpg/;
    if(! -r $jpg && ! -r $name.".jpg") {
    	`nefextract $name > $name.jpg`;
	print "$name\n";
    	}
    }

opendir DIR,'.';
@all = readdir(DIR);
my @files = grep { /\.(jpg)|(tif)$/ } @all;
closedir DIR;
if( my $mask=shift )
{
	@files = grep {/$mask/} @files;
}

foreach ( @files ) {
    my $name =$_;
    my $full_name = $name;
    my $info = Image::ExifTool::ImageInfo( $name );
    $name =~ /^([a-zA-Z]+_[0-9]+)/;
    my $nef = $nefpath.$1.'.nef';

    unless( $info->{'DateTimeOriginal'} ) {
	if( -r $nef ) {
	    `exiftool -overwrite_original -TagsFromFile $nef $name`;
	    $info = Image::ExifTool::ImageInfo( $name );
	    if ( -w $nef && !( $full_name =~ /nef/) ){
		`mv $nef ../nef/`;
	    }
	}
	else
	{
	    my $jpg = $nefpath.$1.'.jpg';
	    if( './'.$name ne $jpg && -r $jpg )
	    {
		`exiftool -overwrite_original -TagsFromFile $jpg $name`;
		$info = Image::ExifTool::ImageInfo( $name );
	    }
	}
    }
    if( -r $nef ) {
	if ( -w $nef && !( $full_name =~ /nef/) ){
	    `mv $nef ../nef/`;
	}
    }

    unless ( -r  "pics/$name" ) {
	`convert -resize 1024x768 -quality 90 $name pics/$name`;
    }

    if( $info->{'DateTimeOriginal'} ) {
	my @ps = split /:/,$info->{'DateTimeOriginal'};
	my $y_m = $ps[0].'-'.$ps[1];
	my $loc =  $info->{'Location'};
	my $qual = $info->{'Quality'};
	my $pwd = `pwd`;
	$pwd =~ s/\n//;
	if( $loc ) {
	    if( $qual eq 'Best' ) {
		unless( -l "../locations/$loc/$qual/$name") {
		    `mkdir -p ../locations/$loc/$qual/`;
		    `ln -s $pwd/pics/$name ../locations/$loc/$qual/$name`;
		}
	    }
	    unless( -l "../locations/$loc/$y_m/$name") {
		`mkdir -p ../locations/$loc/$y_m/`;
		`ln -s $pwd/pics/$name ../locations/$loc/$y_m/$name`;
	    }
	}

    }
    print '.';
}

Пример 6. Скрипт для сортировки фотографий, переписанный на Perl

Сведения о документе