16-й час

Объекты в деле

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

Создание приложения

  Если Вы ещё помните, в главе 6 мы начали работу над программой учёта записей телепередач на кассетах видеомагнитофона. У меня скопилось более 5000 таких записей, поэтому я действительно остро нуждался в такой программе. Все кассеты были пронумерованы, как Вы помните, шестнадцатеричными цифрами. К проблеме учёта записей на кассетах мы ещё раз вернулись в главе 13, когда рассматривали концепции объектно-ориентированного программирования, а именно концепцию отношений между объектом-контейнером (кассетой) и объектом-членом контейнера (записью). Видеокассета, как и контейнер, может быть пустой, содержать несколько записей и быть переполненной — три возможных состояния класса. Действительно, кассеты и видеозаписи в нашем приложении будут реализованы с помощью двух соответствующих классов.

   Свою коллекцию видеозаписей я начал давно, задолго до того, как познакомился с Python. Но уже в те времена не мог обойтись без помощи компьютера. Моя первая "база данных" представляла собой обычный текстовый файл с записями в опредёленном формате. Эта база данных считывалась утилитой на языке С, которая позволила мне автоматизировать выполнение многих рутинных задач. Впрочем, с её помощью нельзя было обновлять текстовые файлы, так что мне приходилось вносить изменения в базу данных с помощью обычного текстового редактора. Но программа на языке С сортировала записи, должным образом располагая их внутри кассет, затем выводила на печать списки записей в алфавитном порядке или по номерам, в зависимости от выбранной опции. Я добавил довольно сложные функции поиска записей, но затем понял, что впустую тратил время. Все необходимые операции поиска можно было выполнять быстрее и более эффективно с помощью утилиты grep (программа для UNIX, осуществляющая поиск образцов текста внутри текстовых файлов). А после того как я перешел на Windows, мне потребовалась программа, отличающаяся лучшей переносимостью, чем мои ранее созданные утилиты на языке С. Лучшего выбора, чем Python, для этих целей трудно себе было даже представить.

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

Модуль shelve

  В целом архитектура программы довольно проста. Видеокассеты содержатся внутри словаря, в котором шестнадцатеричные номера играют роль ключей. Каждая кассета, в свою очередь, содержит свой словарь записей, ключами которого выступают порядковые номера. На основе исходного текстового файла в оперативной памяти компьютера будет создаваться база данных. Чтобы затем сохранить эту базу данных на диске, воспользуемся модулем shelve. Сохранение баз данных таким путём называется персистингом (persisting).

  Модуль shelve позволяет сохранять любой объект в Python в двоичном формате и обрабатывать его затем как словарь. В обычных словарях в качестве ключей могут использоваться данные любых неизменяемых типов. Но в словаре, сохранённом на диске (иногда говорят — положенном на полку), ключами могут быть только строки. Больше информации о модуле shelve можно почерпнуть на Web-странице по адресу http://www.python.org/doc/current/lib/module-shelve.html. Я не стану описывать интерфейс этого модуля, но Вы во многом сможете разобраться вo время анализа листингов далее в этой главе.

Формат записей для ввода

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

0001;0;sp,T120;89;89:

Masterpiece Theatre:

Last Place on Earth, The:

1: Poles Apart

  Вся часть строки с самого начала до первого двоеточия представляла собой заголовок, в котором указывался номер кассеты, порядковый номер записи на кассете, скорость, размер записи, чистое и реальное время воспроизведения. За первым двоеточием следовали поля с описанием записи: категория (Masterpiece Theatre), название (Last Place on Earth), номер эпизода (1), название эпизода (Poles Apart). Поля были разделены комбинацией символов :/t (двоеточие с символом табуляции). Любая запись должна быть представлена хотя бы одним из этих полей. Кроме того, полезным может оказаться ещё ряд дополнительных полей. Например, для записей фильмов следует указать год, например (1999). Комментарии обычно заключают в фигурные скобки {}, а ключевые слова для поиска — в квадратные []. Кроме того, нужно предусмотреть возможность указать, что данная запись заменена более качественной или поздней версией. Ниже показан пример записи с дополнительными необязательными полями:

0045;2;ер,Т120;139,139:

Hidden Fortress, The [Japanes] {Xd} (1958): ->119

  Эта строка означает следующее: вторая запись на кассете № 45, фильм The Hidden Fortress ("Тайная крепость", Акиро Куросава (Akira Kurosawa)) японского производства 1958 г. К сожалению, окончание этого фильма было испорчено (символ Xd), поэтому новая версия была записана на кассету № 119.

  В действительности формат записей не родился сразу, а постепенно дорабатывался мною. Так, например, за значением скорости ер раньше не было значения размера ленты Т120. Вместо двух полей времени воспроизведения указывалось только реальное время записи на кассете, включая как фильм, так и сопровождающую его рекламу. В более позднем варианте уже указывались два значения времени: чистое время фильма и реальное время воспроизведения всей записи. Запись 59,46 означала, что воспроизведение всей записи занимает 59 минут, тогда как чистое время фильма — 46 минут, а остальное время идёт реклама. Нужно помнить, что все дополнения следует делать так, чтобы они были совместимы с предыдущим форматом записей. Для нашего приложения нужно создать функцию, которая будет считывать строки исходного текстового файла, и для каждой новой кассеты создавать объект класса cassette, который должен "знать", как создавать, добавлять и удалять объекты записей телепередач. Функцию, обслуживающую ввод данных, мы рассмотрим позже. А пока сосредоточимся на конструировании двух классов кассет и записей.

Классы

  В нашем приложении, назовем его vcr (video cassette records — записи на видеокассетах), используются два класса. Как мы уже знаем, это классы cassette и program. В листинге 16.1 показан класс program (нумерация строк соответствует положению этого класса в файле программы vcr.py).

Листинг 16.1. Класс program

class program

  def __init__(self, s="",n=0):

    if s=="" and n==0:

      self.seq=-1

      self.year=None

      self.comments=None

      self.keywords=None

      self.rplcd=None

      self.tape=n

    else:

      self.year=None

      self.comments=None

      self.keywords=None

      self.rplcd=None

      self.tape=n

 

    i=string.index(s,":")

    h=header(s[0:i])

    self.seq=string.atoi(h[0])

    sp, tlen=parts(h[1],n)

    self.sp=tspeed(sp)

    if self.sp==-1:

      print "UNDEFINED Tape speed:",n,sp

      self.sp=1 # По умолчанию sp

    self.tlen=tsize(tlen)

    if self.tlen==-1:

      print "UNDEFINED Tape size:",n,tlen

    self.tlen=_tsizes["T120"]

 

    self.ttime, self.ctime=tuple(map(string.atoi, list(parts(h[2],n)))

    stuff=s[i+2:]

    self.fields=[]

    q=string.splitfields(stuff, ":\t")

    for k in q:

      if alldigits(k):

        if k=="":

          print h,"empty string",n

          k=0

        self.fields.append(string.atoi(k))

      else:

        r=FindReplaced(k)*Возвращает[list,]

        if r!=None:

          self.rplcd=r

      else:

        e=Extract(k)

        if e==None:

          print "TAPE %04X Hosed."%(n)

          raise ValueError

        if e[1]!=None:

          self.year=e[1]

        if e[2]!=None:

          self.comments=e[2]

        if e[3]!=None:

          self.keywords=e[3]

          self.fields.append(e[0])

 

  def __len__(self):

    return self.ttime

 

  def speed(self):

    return speedname(self.sp)

 

  def size(self):

    return sizename(self.tlen)

 

  def __repr__(self):

    s="%02d: "%(self.seq)

    n=0

    for i in self.fields:

      if type(i)==type(0):

        s=s+'i'

      else:

        s=s+SwapThe(i)

        if n==0:

          if self.year!=None:

            s=s+" (%4d)"*(self.year)

          if self.comments!=None:

            s=s+" {%s}"%(self.comments)

          if self.keywords!=None:

            s=s+" [%s]"%(self.keywords)

            s=s+','

            n=n+1

    return s

  Метод __init__(), наиболее длинный в этом классе, подразделён на два блока. Первый выполняется в случае, если функция program() вызывается без аргументов. Соответственно второй блок активизируется при передаче в класс вводной строки с номером кассеты. Без аргументов создаётся пустая заготовка объекта, в которую впоследствии можно внести информацию о записи. В случае передачи с аргументом вводной строки программа анализирует её содержимое и присваивает значения переменным-членам класса, представляющим все допустимые поля базы данных. Класс program содержит следующие переменные-члены:

  Комментарий: назначения большинства функций, вызываемых внутри метода __init__(), вполне очевидны или станут таковыми по мере дальнейшего рассмотрения кода программы. Единственная функция, с которой может возникнуть сложность, — это string.splitfields() (строка 315). Это то из немногих мест, где Python "стесняется" показать своё лицо. Данная функция принимает вводную строку (аргумент stuff) и разбивает её на элементы списка, используя в качестве границ комбинацию символов ':/t'. Все поля исходной записи теперь представлены соответствующими элементами списка. Поскольку комбинация символов-разделителей достаточно уникальна, данные в полях могут быть представлены практически любыми наборами символов. Содержимое полей анализируется далее. Например, выражение 'if alldigits(k)' (строка 317) определяет, содержит ли текущее поле только числовые значения, что будет соответствовать номеру эпизода, и т.д. Если название фильма состоит только из цифр, чтобы не спутать его с номером эпизода, перед названием следует установить обратную косую черту, например \2001.

  Следующий класс cassette показан в листинге 16.2 (и вновь нумерация строк соответствует положению класса в файле vcr.py).

Листинг 16.2. Класс cassette

class cassette:

  def __init__(self, n=0, s=None):

    self.num=n

    self.l={}

    if s==None:

      pass

    else:

      t=program(s[5:], self.num)

    self.set(t)

 

  def key(self):

    return "%04Х"%(self.num)

 

  def keys(self):

    if len(self.l)==0:

      return None

    k=self.l.keys()

    k.sort()

    return k

 

  def cat(self, s):

    if s!=None: ,

      t=program(s[5:], self.num)

      self.set(t)

 

  def __len__(self):

    t=0

    k=self.l.keys()

    k.sort()

    for 1 in k:

      i=self.l[1]

      if i!=None:

        t=t+i.ttime

    return t

 

  def speed(self):

    s=tapespeed(self)

    return speedname(s[0])

 

  def size(self):

    return tapesize(self)

 

  def programs(self):

    return len(self.l)

 

  def programlist(self):

    if isdummy(self):

      return None

    rslt=[]

    for i in self.l.keys():

      rslt.append(self.l[i])

    return rslt

 

  def sequence(self, n):

    if type(n)==type(0):

      k="%02d"%(n)

    elif type(n)==type(""):

      k=n

    else:

      k="%s"%(n)

    if self.l.has_key(k):

      return self.l[k]

    return None

 

  def set(self, p):

    try:

      n=p.seq

    except AttributeError:

      return

    k=self.sequence(n)

    if k==None:

      self.l["%02d"*(n)]=p

    else:

      self.l["%02d"%(n)]=p

 

  def __repr__(self):

    s="%04X\n"*(self.num)

    if len(self.l)==0:

      s=s+"*No Tape*"

    else:

      j=self.l.keys()

      j.sort()

      for k in j:

        i=self.l[k]

        s=s+'i'+"\n"

    return s

 

  def nrelplaced(self):

    if len(self.l)==0:

      return 0

    n=0

    k=self.keys()

    for i in k:

      p=self.l[i]

      if p.rplcd!=None:

        n=n+1

    return n

 

  def getreplaced(self):

    if len(self.l)==0:

      return None

    d={}

    k=self.keys()

    for i in k:

      p=self.l[i]

      if p.rplcd!=None:

        d[i]=p.rplcd

    if len(d)==0:

      return None

    return d

 

  def numeric(self, fid=0):

    x=self.programlist()

    return x

 

  def alphabetic(self, fid=0):

    x=self.programlist()

    return x

Этот класс содержит только две переменные-члена:

  Да, так оно и есть. Записи телепередач в действительности хранятся в списке l. Функции нашего класса-контейнера сводятся не совсем к тому, о чем Вы могли подумать. Класс содержит только методы, ничего более сложного. Самый важный среди них — метод set(), который помещает объект записи передачи в соответствующую позицию в списке l. Эта позиция определяется по порядковому номеру записи в кассете. Порядковый номер будет ключом в конечном словаре, причём, как Вы помните, чтобы сохранить этот словарь в файле на диске, ключ должен быть строкой. Процесс сохранения базы данных на диске в Python, между прочим, называют pickling(фаршировка). Поэтому не удивительно, что все функции, необходимые для сохранения файлов, хранятся в модуле pickle. А как же упомянутый выше модуль shelve? В действительности он является лишь интерфейсом модуля pickle, избавляя нас от необходимости разбираться в достаточно сложных функциях последнего.

Служебные функции

  Для работы приложения нам необходим набор функций, которые будут обслуживать работу двух центральных классов. Сюда входят считывание исходных данных, построение базы данных в оперативной памяти компьютера, обработка строк, вводимых пользователем, и многое другое. На данном этапе мы пока не имеем ничего, кроме двух абстрактных объектов, которые сами по себе ничего не умеют делать. В листинге 16.3 показана оставшаяся часть кода модуля vcr.py. (Обратите внимание на разрывы в нумерации строк, где находятся блоки определений классов, которые мы рассмотрели раньше.)

Листинг 16.3. Служебные функции программы vcr.py

  *Прим. В. Шипкова: здесь был длиннющий листинг описываемой программы. Я его убрал, зато положил ссылку на файл-оригинал, если кому действительно надо. %)

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

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

Интерфейс командной строки

  Поскольку программа на языке С была прообразом модуля vcr.py, нелишне посмотреть на распечатку опций командной строки этой программы, показанную в табл. 16.1.

Таблица 16.4. Опции командной строки программы vcr на языке С

Опция Описание
-h Вывод справки на stderr
-? Вывод справки на stdout
-V Напечатать версию и умереть
-i<файл> Ввести файл, если это не '-/VcrList'
-о<файл> Вывести файл (по умолчанию на stdout)
Сортировать по заголовкам записей
-r Обратный порядок записей
-S<строка> Выбрать по совпадению регулярного выражения
-+ Расширить область поиска-выделения дополнительными полями
Ограничить область поиска-выделения только ключевыми словами
-k Вывести ключевые слова на печать
Не выводить описание
-F<строка> Выводить строку с использованием оператора форматирования %
Справка по поводу форматирования выводимых строк
-d<строка> Отладка на уровне функций
Отключение чувствительности к регистру во время поиска
-N<crpoкa> Выбор кассет по номерам в диапазоне: [х, х-х, -х, х-]
-Т<строка> Создание файла в формате HTML для выбранных записей
-I<строка> Создание указателя в формате HTML для выбранных записей
Создание ярлыков PostScript для выбранных записей
PostScript: только граничные ярлыки
-f PostScript: только лицевые ярлыки
-R Отчёт о суммарном времени воспроизведения всех лент
-р<строка> Использование файла -.pro вместо заданных по умолчанию
-n##t Установка ширины колонки в символах
-l Выбор и показ самого длинного поля записей
-L Исключительный выбор и показ самого длинного поля записей
-V Отмена подстановки слов a, an и the в начале строки во время вывода её на печать
-0 Вывод файла в формате исходного файла

  В нашем модуле базы данных vcr мы попытаемся воссоздать лишь часть описанных опций, хотя бы потому, что многие из них уже потеряли свой смысл или не существенны. Так, например, записи теперь хранятся не в сортируемом списке, как было в С, а в базе данных. Поэтому отпала необходимость в опциях управления списком. Определение аргументов командной строки лучше выполнить не в самом модуле vcr, а в отдельной программе, в которую предварительно импортируем модуль. Первая программа, которую мы создадим, будет просто посылать запрос в базу данных для нахождения в ней определённых кассет. Код этой программы показан в листинге 16.4.

Листинг 16.4. Программа vcr.py

#!C:\PYTHON\PYTHON.EXE

 

import os

import sys

import shelve

import string

import time

from vcr import cassette

from vcr import program

import vcr

 

try:

  _vcrfile=os.environ["VCRLIST"]

except:

  print "$VCRLIST not set in environment!"

  sys.exit(0)

 

if len(sys.argv)>1:

  vdb=vcr.openfiledb()

  for i in range(1, len(sys.argv)):

    t=vcr.findtape(sys.argv[i], vdb)

    if t!=None:

      print t, '"Length:", len(t), "speed", t.speed(), \

        "size", t.size(), print "minutestleft", \

         vcr.minutesleft(t)

      print ""

  k=t.keys()# Автосортировка

  for j in k:

    у=t.sequence(j)

  # Функция принимает либо целые числа, либо строки.

 

  if y.fields[0]!=None:

    for z in у.fields:

      if type(z)!=type(0):

        print vcr.SwapThe(z)

      else:

        print z

      if y.year!=None:

        print "(%04d)" % ( y.year ),

      if y.comments!=None:

        print "{%s}" % ( y.comments ),

      if y.keywords!=None:

        print "[%s]" % ( y.keywords ),

  print "%S~~~~~~~~"% ( j )

  vcr.closedb(vdb)

  Обратите внимание, что мы не только импортировали в программу модуль vcr (инструкция import vcr), но также явно импортировали классы этого модуля (инструкция from vcr import cassette и аналогичная инструкция для класса program). Это необходимо было сделать, так как модулю shelve для оперирования файлами базы данных нужны имена исходных классов объектов. В программе предполагается, что аргументами командной строки могут быть только номера кассет, которые нужно отыскать в базе данных. Поиск производится с помощью функции findtape() нашего модуля vcr. В исходной версии на языке С допускался ввод диапазонов номеров в формате 1000-1FFF или -1FFF. Я планировал добавить такую возможность поиска в модуль vcr, но не успел. Может быть, Вы это сделаете сами.

  Следующая программа, также работающая в режиме командной строки, просматривает все кассеты в базе данных и выбирает те из них, на которых свободного места осталось больше, чем указано в аргументе. Такая утилита будет весьма полезной, например, когда Вам нужно записать новую 1-часовую программу и Вы ищете подходящую для этого кассету. Код такой программы показан в листинге 16.5.

Листинг 16.5. Программа vcrleft.py

#!/usr/local/bin/python

import os
import sys
import shelve
import string
from vcr import cassette
from vcr import program
import vcr

try :
_vcrfile = os.environ[ "VCRLIST" ]
except :
  print "$VCRLIST not set in environment!"
sys.exit(0)

spd=0

howmuch=28.0
lessthan=9999.0
tlist=[]
mlist=[]

def prlist(list):
tpr=0
n=len(list)
z=0
i=0
  for z in range(n):
    print list[z],
tpr=tpr+1
i=z%3
    if i==2:
      print ""
    else:
      print "\t",
    if i!=2:
    print ""
    print "--------------------------------------------------------------------------------"

if len(sys.argv)>1:
howmuch=float(string.atoi(sys.argv[1]))
if len(sys.argv)>2:
  try:
lessthan=float(string.atoi(sys.argv[2]))
  except :
    if sys.argv[2]=="sp":
spd=1
    elif sys.argv[2]=="lp":
spd=2
    elif sys.argv[2]=="ep" or sys.argv[2]=="slp":
spd=3
    else:
      print "I don't understand", sys.argv[ 2 ]
      raise ValueError

if len(sys.argv)>3:
  if sys.argv[3]=="sp":
spd=1
  elif sys.argv[3]=="lp":
spd=2
  elif sys.argv[3]=="ep" or sys.argv[3]=="slp":
spd=3
  else:
    print "I don't understand", sys.argv[3]
    raise ValueError
vdb=vcr.openfiledb()
lst=vcr.getkeys(vdb)
lst.sort()
print "Looking for", howmuch, "<", lessthan, "minutes, in", len ( lst ), "Keys"
print "--------------------------------------------------------------------------------"
for j in lst :
i=vcr.findtape(j, vdb)
t=vcr.minutesleft(i)
  if spd!=0:
    if not vcr.onlyspeed(i, spd):
continue
if howmuch<t<lessthan:
x, y=vcr.tapespeed(i)
hl, ml=vcr.tupleize(t)

st="T%04X%s -- %03dm %02d:%02d"%(i.num, vcr.speedname(x ), t, hl, ml)
mlist.append(st)
st="%03dm %02d:%02d -- T%04X %s"%( t, hl, ml, i.num, vcr.speedname(x))
tlist.append(st)

tlist.sort()
mlist.sort()

prlist(mlist)
prlist(tlist)

vcr.closedb(vdb)

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

Резюме

  В этой главе, последней в части II, мы впервые столкнулись с длинными кодами программ, подразделенными на модули, классы и служебные функции. Некоторые части кода, например, там, где используются регулярные выражения, остались для Вас непонятнымй. Но Вы можете разобраться в них самостоятельно, почерпнув дополнительную информацию из источников, рекомендованных ниже, в разделе "Примеры и задания". Целью, которую я ставил перед собой, выбирая программный код для демонстрации, было наглядно показать Вам, как взаимодействуют разные части программы. Все рассмотренные программы можно загрузить с Web-страницы этой книги. Там же Вы найдёте небольшой подготовленный мной файл базы данных в текстовом формате. Только предупреждаю, это всего лишь демонстрационная база данных, не просите меня в письмах одолжить ту или иную кассету для просмотра.

  *Прим. В. Шипкова: в приведённой базе данных, есть как ковбойские вестерны, так и классическое японское кино. 80% этих фильмов я не видел, ничего существенного добавить не могу.

Практикум

Вопросы и ответы

  Всё, что мы делали, изучая материал данной главы, — это рассматривали длиннющие программные коды. Надо ли нам теперь садиться за испытание этих программ?

  Не обязательно. Вес, что Вам сейчас надо, — это немножко отдохнуть перед следующей частью.

Примеры и задания

  Лучшая книга, описывающая использование регулярных выражений, написана Jeffrey Е. F. Friedl, Mastering Regular Expressions, O'Reilly & Associates. Давид Ашер (David Ascher) так сказал о ней: "Книга Джефри Фриедла одна из немногих, заслуживающих ту цену, которую я за неё заплатил, даже при том, что представления Джефри о Python несколько устарели, а я довольно редко использую на практике регулярные выражения. Это лишний раз доказывает стремление человеческого сознания познать непознанное, даже если оно не нужно". От себя добавлю, если Вам всё же нужны регулярные выражения, то лучшего источника данных о них Вы не найдёте.

  Сделайте возможным поиск кассет в базе данных vcr по указанным диапазонам. Подсказка: воспользуйтесь в качестве образца модулем getargs, который можно найти и загрузить по адресу http://www.pauahtun.org/ftp.html. Не сомневайтесь, этот модуль хорошо документирован (http://www.pauahtun.org/getargs.html).