Программирование на языке Ruby

Фултон Хэл

Глава 10. Ввод/вывод и хранение данных

 

 

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

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

Есть несколько факторов, затрудняющих ввод/вывод. Во-первых, ввод и вывод - совершенно разные вещи, но обычно мы мысленно объединяем их. Во-вторых, операции ввода/вывода столь же разнообразны, как и мир насекомых.

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

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

Ввод/вывод в Ruby сложен, потому что он сложен в принципе. Но мы старались описать его как можно понятнее и показать, где и когда стоит применять различные приемы.

В основе системы ввода/вывода в Ruby лежит класс IO, который определяет поведение всех операций ввода/вывода. С ним тесно связан (и наследует ему) класс File. В класс File вложен класс Stat, инкапсулирующий различные сведения о файле (например, разрешения и временные штампы). Методы stat и lstat возвращают объекты типа File::Stat.

В модуле FileTest также есть методы, позволяющие опрашивать практически те же свойства. Он подмешивается к классу File, но может использоваться и самостоятельно.

Наконец, методы ввода/вывода есть и в модуле Kernel, который подмешивается к классу Object (предку всех объектов, включая и классы). Это простые процедуры, которыми мы пользовались на протяжении всей книги, не думая о том, от имени какого объекта они вызываются. По умолчанию они настроены на стандартный ввод и стандартный вывод.

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

На более высоком уровне Ruby предлагает механизмы, позволяющие сделать объекты устойчивыми. Метод Marshal реализует простую сериализацию объектов; он лежит в основе более изощренной библиотеки PStore. Мы включили в эту главу и библиотеку DBM, хотя она умеет работать только со строками.

На самом высоком уровне возможен интерфейс с системами управления базами данных, например MySQL или Oracle. Эта тема настолько сложна, что ей можно было бы посвятить одну или даже несколько книг. Мы ограничимся лишь кратким введением. В некоторых случаях будут даны ссылки на архивы в сети.

 

10.1. Файлы и каталоги

 

Под файлом мы обычно, хотя и не всегда, понимаем файл на диске. Концепция файла в Ruby, как и в других языках, — это полезная абстракция. Говоря «каталог», мы подразумеваем каталог или папку в смысле, принятом в UNIX и Windows.

Класс File тесно связан с классом IO, которому наследует. Класс Dir связан с ним не так тесно, но мы решили рассмотреть файлы и каталоги вместе, поскольку между ними имеется концептуальная связь.

 

10.1.1.

Открытие и закрытие файлов

Метод класса File.new, создающий новый объект File, также открывает файл. Первым параметром, естественно, является имя файла.

Необязательный второй параметр называется строкой указания режимам он говорит, как нужно открывать файл — для чтения, для записи и т.д. (Строка указания режима не имеет ничего общего с разрешениями.) По умолчанию предполагается режим "r", то есть только чтение. Ниже показано, как открывать файлы для чтения и записи.

file1 = File.new("one")      # Открыть для чтения.

file2 = File.new("two", "w") # Открыть для записи.

Есть также разновидность метода new, принимающая три параметра. В этом случае второй параметр задает начальные разрешения для файла (обычно записывается в виде восьмеричной константы), а третий представляет собой набор флагов, объединенных союзом ИЛИ. Флаги обозначаются константами, например: File::CREAT (создать файл, если он еще не существует) и File::RDONLY (открыть только для чтения). Такая форма используется редко.

file = File.new("three", 0755, File::CREAT|File::WRONLY)

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

out = File.new("captains.log", "w")

# Обработка файла...

out.close

Имеется также метод open. В простейшей форме это синоним new:

trans = File.open("transactions","w")

Но методу open можно также передать блок, и это более интересно. Если блок задан, то ему в качестве параметра передается открытый файл. Файл остается открытым на протяжении всего времени нахождения в блоке и автоматически закрывается при выходе из него. Пример:

File.open("somefile","w") do |file|

 file.puts "Строка 1"

 file.puts "Строка 2"

 file.puts "Третья и последняя строка"

end

# Теперь файл закрыт.

Это изящный способ обеспечить закрытие файла по завершении работы с ним. К тому же при такой записи весь код обработки файла сосредоточен в одном месте.

 

10.1.2. Обновление файла

Чтобы открыть файл для чтения и записи, достаточно добавить знак плюс (+) в строку указания режима (см. раздел 10.1.1):

f1 = File.new("file1", "r+")

# Чтение/запись, от начала файла.

f2 = File.new("file2", "w+")

# Чтение/запись; усечь существующий файл или создать новый.

f3 = File.new("file3", "а+")

# Чтение/запись; перейти в конец существующего файла или создать новый.

 

10.1.3. Дописывание в конец файла

Чтобы дописать данные в конец существующего файла, нужно задать строку указания режима "а" (см. раздел 10.1.1):

logfile = File.open("captains_log", "a")

# Добавить строку в конец и закрыть файл.

logfile.puts "Stardate 47824.1: Our show has been canceled."

logfile.close

 

10.1.4. Прямой доступ к файлу

Для чтения из файла в произвольном порядке, а не последовательно, можно воспользоваться методом seek, который класс File наследует от IO. Проще всего перейти на байт в указанной позиции. Номер позиции отсчитывается от начала файла, причем самый первый байт находится в позиции 0.

# myfile содержит строку: abcdefghi

file = File.new("myfile")

file.seek(5)

str = file.gets # "fghi"

Если все строки в файле имеют одинаковую длину, то можно перейти сразу в начало нужной строки:

# Предполагается, что все строки имеют длину 20.

# Строка N начинается с байта (N-1)*20

file = File.new("fixedlines")

file.seek(5*20) # Шестая строка!

Для выполнения относительного поиска воспользуйтесь вторым параметром. Константа IO::SEEK_CUR означает, что смещение задано относительно текущей позиции (и может быть отрицательным):

file = File.new("somefile")

file.seek(55)                # Позиция 55.

file.seek(-22, IO::SEEK_CUR) # Позиция 33.

file.seek(47, IO::SEEK_CUR)  # Позиция 80.

Можно также искать относительно конца файла, в таком случае смещение может быть только отрицательным:

file.seek(-20, IO::SEEK_END) # Двадцать байтов от конца файла.

Есть еще и третья константа IO::SEEK_SET, но это значение по умолчанию (поиск относительно начала файла).

Метод tell возвращает текущее значение позиции в файле, у него есть синоним pos:

file.seek(20)

pos1 = file.tell # 20

file.seek(50, IO::SEEK_CUR)

pos2 = file.pos  # 70

Метод rewind устанавливает указатель файла в начало. Его название («обратная перемотка») восходит ко временам использования магнитных лент.

Для выполнения прямого доступа файл часто открывается в режиме обновления (для чтения и записи). Этот режим обозначается знаком + в начале строки указания режима (см. раздел 10.1.2).

 

10.1.5. Работа с двоичными файлами

Когда-то давно программисты на языке С включали в строку указания режима символ "b" для открытия файла как двоичного. (Вопреки распространенному заблуждению, это относилось и к ранним версиям UNIX.) Как правило, эту возможность все еще поддерживают ради совместимости, но сегодня с двоичными файлами работать не так сложно, как раньше. Строка в Ruby может содержать двоичные данные, а для чтения двоичного файла не нужно никаких специальных действий.

Исключение составляет семейство операционных систем Windows, в которых различие все еще имеет место. Основное отличие двоичных файлов от текстовых на этой платформе состоит в том, что в двоичном режиме конец строки не преобразуется в один символ перевода строки, а представляется в виде пары «возврат каретки — перевод строки». Еще одно важное отличие — интерпретация символа control-Z как конца файла в текстовом режиме:

# Создать файл (в двоичном режиме).

File.open("myfile","wb") {|f| f.syswrite("12345\0326789\r") }

#Обратите внимание на восьмеричное 032 (^Z).

# Читать как двоичный файл.

str = nil

File.open("myfile","rb") {|f| str = f.sysread(15) )

puts str.size # 11

# Читать как текстовый файл.

str = nil

File.open("myfile","r") {|f| str = f.sysread(15) }

puts str.size # 5

В следующем фрагменте показано, что на платформе Windows символ возврата каретки не преобразуется в двоичном режиме:

# Входной файл содержит всего одну строку: Строка 1.

file = File.open("data")

line = file.readline          # "Строка 1.\n"

puts "#{line.size} символов." # 10 символов,

file.close

file = File.open("data","rb")

line = file.readline          # "Строка 1.\r\n"

puts "#{line.size} символов." # 11 символов.

file.close

Отметим, что упомянутый в коде метод binmode переключает поток в двоичный режим. После переключения вернуться в текстовый режим невозможно.

file = File.open("data")

file.binmode

line = file.readline        # "Строка 1.\r\n"

puts {line.size} символов." # 11 символов.

file.close

При необходимости выполнить низкоуровневый ввод/вывод можете воспользоваться методами sysread и syswrite. Первый принимает в качестве параметра число подлежащих чтению байтов, второй принимает строку и возвращает число записанных байтов. (Если вы начали читать из потока методом sysread, то никакие другие методы использовать не следует. Результаты могут быть непредсказуемы.)

input = File.new("infile")

output = File.new("outfile")

instr = input.sysread(10);

bytes = output.syswrite("Это тест.")

Отметим, что метод sysread возбуждает исключение EOFError при попытке вызвать его, когда достигнут конец файла (но не в том случае, когда конец файла встретился в ходе успешной операции чтения). Оба метода возбуждают исключение SystemCallError при возникновении ошибки ввода/вывода.

При работе с двоичными данными могут оказаться полезны метод pack из класса Array и метод unpack из класса String.

 

10.1.6. Блокировка файлов

В тех операционных системах, которые поддерживают такую возможность, метод flock класса File блокирует или разблокирует файл. Вторым параметром может быть одна из констант File::LOCK_EX, File::LOCK_NB, File::LOCK_SH, File::LOCK_UN или их объединение с помощью оператора ИЛИ. Понятно, что многие комбинации не имеют смысла; чаще всего употребляется флаг, задающий неблокирующий режим.

file = File.new("somefile")

file.flock(File::LOCK_EX) # Исключительная блокировка; никакой другой

                          # процесс не может обратиться к файлу.

file.flock(File::LOCK_UN) # Разблокировать.

file.flock(File::LOCK_SH) # Разделяемая блокировка (другие

                          # процессы могут сделать то же самое).

file.flock(File::LOCK_UN) # Разблокировать.

locked = file.flock(File::LOCK_EX | File::LOCK_NB)

# Пытаемся заблокировать файл, но не приостанавливаем программу, если

# не получилось; в таком случае переменная locked будет равна false.

Для семейства операционных систем Windows эта функция не реализована.

 

10.1.7. Простой ввод/вывод

Вы уже знакомы с некоторыми методами ввода/вывода из модуля Kernel; мы вызывали их без указания вызывающего объекта. К ним относятся функции gets и puts, а также print, printf и p (последний вызывает метод объекта inspect, чтобы распечатать его в понятном для нас виде).

Но есть и другие методы, которые следует упомянуть для полноты. Метод putc выводит один символ. (Парный метод getc не реализован в модуле Kernel по техническим причинам, однако он есть у любого объекта класса IO). Если параметром является объект String, то печатается первый символ строки.

putc(?\n) # Вывести символ новой строки.

putc("X") # Вывести букву X.

Интересный вопрос: куда направляется вывод, если эти методы вызываются без указания объекта? Начнем с того, что в среде исполнения Ruby определены три глобальные константы, соответствующие трем стандартным потокам ввода/вывода, к которым мы привыкли в UNIX. Это STDIN, STDOUT и STDERR. Все они имеют тип IO.

Имеется также глобальная переменная $stdout, именно в нее направляется весь вывод, формируемый методами из Kernel. Она инициализирована значением STDOUT, так что данные отправляются на стандартный вывод, как и следовало ожидать. В любой момент переменной $stdout можно присвоить другое значение, являющееся объектом IO.

diskfile = File.new("foofile","w")

puts "Привет..." # Выводится на stdout.

$stdout = diskfile

puts "Пока!"     # Выводится в файл "foofile".

diskfile.close

$stdout = STDOUT # Восстановление исходного значения.

puts "Это все."  # Выводится на stdout.

Помимо метода gets в модуле Kernel есть методы ввода readline и readlines. Первый аналогичен gets в том смысле, что возбуждает исключение EOFError при попытке читать за концом файла, а не просто возвращает nil. Последний эквивалентен методу IO.readlines (то есть считывает весь файл в память).

Откуда мы получаем ввод? Есть переменная $stdin, которая по умолчанию равна STDIN. Точно так же существует поток стандартного вывода для ошибок ($stderr, по умолчанию равен STDERR).

Еще имеется интересный глобальный объект ARGF, представляющий конкатенацию всех файлов, указанных в командной строке. Это не объект класса File, хотя и напоминает таковой. По умолчанию ввод связан именно с этим объектом, если в командной строке задан хотя бы один файл.

# Прочитать все файлы, а затем вывести их.

puts ARGF.read

# А при таком способе более экономно расходуется память:

while ! ARGF.eof?

 puts ARGF.readline

end

# Пример: ruby cat.rb file1 file2 file3

При чтении из стандартного ввода (stdin) методы Kernel не вызываются. Потому можно обойти (или не обходить) ARGF, как показано ниже:

# Прочитать строку из стандартного ввода.

str1 = STDIN.gets

# Прочитать строку из ARGF.

str2 = ARGF.gets

# А теперь снова из стандартного ввода.

str3 = STDIN.gets

 

10.1.8. Буферизованный и небуферизованный ввод/вывод

В некоторых случаях Ruby осуществляет буферизацию самостоятельно. Рассмотрим следующий фрагмент:

print "Привет... "

sleep 10

print "Пока!\n"

Если запустить эту программу, то вы увидите, что сообщения «Привет» и «Пока» появляются одновременно, после завершения sleep. При этом первое сообщение не завершается символом новой строки.

Это можно исправить, вызвав метод flush для опустошения буфера вывода. В данном случае вывод идет в поток $defout (подразумеваемый по умолчанию для всех методов Kernel, которые занимаются выводом). И поведение оказывается ожидаемым, то есть первое сообщение появляется раньше второго.

print "Привет... "

STDOUT.flush

sleep 10

print "Пока!\n"

Буферизацию можно отключить (или включить) методом sync=, а метод sync позволяет узнать текущее состояние.

buf_flag = $defout.sync # true

STDOUT.sync = false

buf_flag = STDOUT.sync  # false

Есть еще по крайней мере один низкий уровень буферизации, который не виден. Если метод getc возвращает символ и продвигает вперед указатель файла или потока, то метод ungetc возвращает символ назад в поток.

ch = mystream.getc # ?А

mystream.ungetc(?C)

ch = mystream.getc # ?C

Тут следует иметь в виду три вещи. Во-первых, только что упомянутая буферизация не имеет отношения к механизму буферизации, о котором мы говорили выше в этом разделе. Иными словами, предложение sync=false не отключает ее. Во-вторых, вернуть в поток можно только один символ; при попытке вызвать метод ungetc несколько раз будет возвращен только символ, прочитанный последним. И, в-третьих, метод ungetc не работает для принципиально небуферизуемых операций (например, sysread).

 

10.1.9. Манипулирование правами владения и разрешениями на доступ к файлу

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

Для определения владельца и группы файла (это целые числа) класс File::Stat предоставляет методы экземпляра uid и gid:

data = File.stat("somefile")

owner_id = data.uid

group_id = data.gid

В классе File::Stat есть также метод экземпляра mode, который возвращает текущий набор разрешений для файла.

perms = File.stat("somefile").mode

В классе File имеется метод класса и экземпляра chown, позволяющий изменить идентификаторы владельца и группы. Метод класса принимает произвольное число файлов. Если идентификатор не нужно изменять, можно передать nil или -1.

uid = 201

gid = 10

File.chown(uid, gid, "alpha", "beta")

f1 = File.new("delta")

f1.chown(uid, gid)

f2 = File.new("gamma")

f2.chown(nil, gid) # Оставить идентификатор владельца без изменения.

Разрешения можно изменить с помощью метода chmod (у него также есть два варианта: метод класса и метод экземпляра). Традиционно разрешения представляют восьмеричным числом, хотя это и не обязательно.

File.chmod(0644, "epsilon", "theta")

f = File.new("eta")

f.chmod(0444)

Процесс всегда работает от имени какого-то пользователя (возможно, root), поэтому с ним связан идентификатор пользователя (мы сейчас говорим о действующем идентификаторе). Часто нужно знать, имеет ли данный пользователь право читать, писать или исполнять данный файл. В классе File::Stat есть методы экземпляра для получения такой информации.

info = File.stat("/tmp/secrets")

rflag = info.readable?

wflag = info.writable?

xflag = info.executable?

Иногда нужно отличить действующий идентификатор пользователя от реального. На этот случай предлагаются методы экземпляра readable_real?, writable_real? и executable_real?.

info = File.stat("/tmp/secrets")

rflag2 = info.readable_real?

wflag2 = info.writable_real?

xflag2 = info.executable_real?

Можно сравнить владельца файла с действующим идентификатором пользователя (и идентификатором группы) текущего процесса. В классе File::Stat для этого есть методы owned? и grpowned?.

Отметим, что многие из этих методов можно найти также в модуле FileTest:

rflag = FileTest::readable?("pentagon_files")

# Прочие методы: writable? executable? readable_real?

# writable_real? executable_real? owned? grpowned?

# Отсутствуют здесь: uid gid mode.

Маска umask, ассоциированная с процессом, определяет начальные разрешения для всех созданных им файлов. Стандартные разрешения 0777 логически пересекаются (AND) с отрицанием umask, то есть биты, поднятые в маске, «маскируются» или сбрасываются. Если вам удобнее, можете представлять себе эту операцию как вычитание (без занимания). Следовательно, если задана маска 022, то все файлы создаются с разрешениями 0755.

Получить или установить маску можно с помощью метода umask класса File. Если ему передан параметр, то он становится новым значением маски (при этом метод возвращает старое значение).

File.umask(0237) # Установить umask.

current_umask = File.umask # 0237

Некоторые биты режима файла (например, бит фиксации — sticky bit) не имеют прямого отношения к разрешениям. Эта тема обсуждается в разделе 10.1.12.

 

10.1.10. Получение и установка временных штампов

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

Получить эту информацию можно тремя разными способами, хотя все они дают один и тот же результат.

Методы mtime, atime и ctime класса File возвращают временные штампы, не требуя предварительного открытия файла или даже создания объекта File.

t1 = File.mtime("somefile")

# Thu Jan 04 09:03:10 GMT-6:00 2001

t2 = File.atime("somefile")

# Tue Jan 09 10:03:34 GMT-6:00 2001

t3 = File.ctime("somefile")

# Sun Nov 26 23:48:32 GMT-6:00 2000

Если файл, представленный экземпляром File, уже открыт, то можно воспользоваться методами этого экземпляра.

myfile = File.new("somefile")

t1 = myfile.mtime

t2 = myfile.atime

t3 = myfile.ctime

А если имеется экземпляр класса File::Stat, то и у него есть методы, позволяющие получить ту же информацию:

myfile = File.new("somefile")

info = myfile.stat

t1 = info.mtime

t2 = info.atime

t3 = info.ctime

Отметим, что объект File::Stat возвращается методом класса (или экземпляра) stat из класса File. Метод класса lstat (или одноименный метод экземпляра) делает то же самое, но возвращает информацию о состоянии самой ссылки, а не файла, на который она ведет. Если имеется цепочка из нескольких ссылок, то метод следует по ней и возвращает информацию о предпоследней (которая уже указывает на настоящий файл).

Для изменения времени доступа и модификации применяется метод utime, которому можно передать несколько файлов. Время можно создать в виде объекта Time или числа секунд, прошедших с точки отсчета.

today = Time.now

yesterday = today - 86400

File.utime(today, today, "alpha")

File.utime(today, yesterday, "beta", "gamma")

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

mtime = File.mtime("delta")

File.utime(Time.now, mtime, "delta")

 

10.1.11. Проверка существования и получение размера файла

Часто необходимо знать, существует ли файл с данным именем. Это позволяет выяснить метод exist? из модуля FileTest:

flag = FileTest::exist?("LochNessMonster")

flag = FileTest::exists?("UFO")

# exists? является синонимом exist?

Понятно, что такой метод не может быть методом экземпляра File, поскольку после создания объекта файл уже открыт. В классе File мог бы быть метод класса с именем exist?, но его там нет.

С вопросом о том, существует ли файл, связан другой вопрос: а есть ли в нем какие-нибудь данные? Ведь файл может существовать, но иметь нулевую длину — а это практически равносильно тому, что он отсутствует.

Если нас интересует только, пуст ли файл, то в классе File::Stat есть два метода экземпляра, отвечающих на этот вопрос. Метод zero? возвращает true, если длина файла равна нулю, и false в противном случае.

flag = File.new("somefile").stat.zero?

Метод size? возвращает либо размер файла в байтах, если он больше нуля, либо nil для файла нулевой длины. Не сразу понятно, почему nil, а не 0. Дело в том, что метод предполагалось использовать в качестве предиката, а значение истинности нуля в Ruby — true, тогда как для nil оно равно false.

if File.new("myfile").stat.size?

 puts "В файле есть данные."

else

 puts "Файл пуст."

end

Методы zero? и size? включены также в модуль FileTest:

flag1 = FileTest::zero?("file1")

flag2 = FileTest::size?("file2")

Далее возникает следующий вопрос: «Каков размер файла?» Мы уже видели что для непустого файла метод size? возвращает длину. Но если мы применяем его не в качестве предиката, то значение nil только путает.

В классе File есть метод класса (но не метод экземпляра) для ответа на этот вопрос. Метод экземпляра с таким же именем имеется в классе File::Stat.

size1 = File.size("file1")

size2 = File.stat("file2").size

Чтобы получить размер файла в блоках, а не в байтах, можно обратиться к методу blocks из класса File::Stat. Результат, конечно, зависит от операционной системы. (Метод blksize сообщает размер блока операционной системы.)

info = File.stat("somefile")

total_bytes = info.blocks * info.blksize

 

10.1.12. Опрос специальных свойств файла

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

Читая этот раздел (да и большую часть этой главы), помните о двух вещах. Во-первых, так как класс File подмешивает модуль FileTest, то любую проверку, для которой требуется вызывать метод, квалифицированный именем модуля, можно также выполнить, обратившись к методу экземпляра любого файлового объекта. Во-вторых, функциональность модуля FileTest и объекта File::Stat (возвращаемого методом stat или lstat) сильно перекрывается. В некоторых случаях есть целых три разных способа вызвать по сути один и тот же метод. Мы не будем каждый раз приводить все варианты.

В некоторых операционных системах устройства подразделяются на блочные и символьные. Файл может ссылаться как на то, так и на другое, но не на оба сразу. Методы blockdev? и chardev? из модуля FileTest проверяют тип устройства:

flag1 = FileTest::chardev?("/dev/hdisk0")  # false

flag2 = FileTest::blockdev?("/dev/hdisk0") # true

Иногда нужно знать, ассоциирован ли данный поток с терминалом. Метод tty? класса IO (синоним isatty) дает ответ на этот вопрос:

flag1 = STDIN.tty?                  # true

flag2 = File.new("diskfile").isatty # false

Поток может быть связан с каналом (pipe) или сокетом. В модуле FileTest есть методы для опроса этих условий:

flag1 = FileTest::pipe?(myfile)

flag2 = FileTest::socket?(myfile)

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

file1 = File.new("/tmp")

file2 = File.new("/tmp/myfile")

test1 = file1.directory? # true

test2 = file1.file?      # false

test3 = file2.directory? # false

test4 = file2.file?      # true

В классе File есть также метод класса ftype, который сообщает вид потока; одноименный метод экземпляра находится в классе File::Stat. Этот метод возвращает одну из следующих строк: file, directory, blockSpecial, characterSpecial, fifo, link или socket (строка fifо относится к каналу).

this_kind = File.ftype("/dev/hdisk0")   # "blockSpecial"

that_kind = File.new("/tmp").stat.ftype # "directory"

В маске, описывающей режим файла, можно устанавливать или сбрасывать некоторые биты. Они не имеют прямого отношения к битам, обсуждавшимся в разделе 10.1.9. Речь идет о битах set-group-id, set-user-id и бите фиксации (sticky bit). Для каждого из них есть метод в модуле FileTest.

file = File.new("somefile")

info = file.stat

sticky_flag = info.sticky?

setgid_flag = info.setgid?

setuid_flag = info.setuid?

К дисковому файлу могут вести символические или физические ссылки (в тех операционных системах, где такой механизм поддерживается). Чтобы проверить, является ли файл символической ссылкой на другой файл, обратитесь к методу symlink? из модуля FileTest. Для подсчета числа физических ссылок на файл служит метод nlink (он есть только в классе File::Stat). Физическая ссылка неотличима от обычного файла — это просто файл, для которого есть несколько имен и записей в каталоге.

File.symlink("yourfile","myfile")          # Создать ссылку

is_sym = FileTest::symlink?("myfile")      # true

hard_count = File.new("myfile").stat.nlink # 0

Отметим попутно, что в предыдущем примере мы воспользовались методом класса symlink из класса File для создания символической ссылки.

В редких случаях может понадобиться информация о файле еще более низкого уровня. В классе File::Stat есть еще три метода экземпляра, предоставляющих такую информацию. Метод dev возвращает целое число, идентифицирующее устройство, на котором расположен файл. Метод rdev возвращает целое число, описывающее тип устройства, а для дисковых файлов метод ino возвращает номер первого индексного узла, занятого файлом.

file = File.new("diskfile")

info = file.stat

device = info.dev

devtype = info.rdev

inode = info.ino

 

10.1.13. Каналы

Ruby поддерживает разные способы читать из канала и писать в него. Метод класса IO.popen открывает канал и связывает с возвращенным объектом стандартные ввод и вывод процесса. Часто с разными концами канала работают разные потоки, но в примере ниже запись и чтение осуществляет один и тот же поток:

check = IO.popen("spell","r+")

check.puts("'T was brillig, and the slithy toves")

check.puts("Did gyre and gimble in the wabe.")

check.close_write

list = check.readlines

list.collect! { |x| x.chomp }

# list равно %w[brillig gimble gyre slithy toves wabe]

Отметим, что вызов close_write обязателен, иначе мы никогда не достигнем конца файла при чтении из канала. Существует также блочная форма:

File.popen("/usr/games/fortune") do |pipe|

quote = pipe.gets

puts quote

# На чистом диске можно искать бесконечно. - Том Стил.

end

Если задана строка "-", то запускается новый экземпляр Ruby. Если при этом задан еще и блок, то он работает в двух разных процессах, как в результате разветвления (fork); блоку в процессе-потомке передается nil, а в процессе-родителе — объект IO, с которым связан стандартный ввод или стандартный вывод.

IO.popen("-")

do |mypipe|

 if mypipe

  puts "Я родитель: pid = #{Process.pid}"

  listen = mypipe.gets

  puts listen

 else

  puts "Я потомок: pid = #{Process.pid}"

 end

end

# Печатается:

# Я родитель: pid = 10580

# Я потомок: pid = 10582

Метод pipe возвращает также два конца канала, связанных между собой. В следующем примере мы создаем два потока, один из которых передает сообщение другому (то самое сообщение, которое Сэмюэль Морзе впервые послал по телеграфу). Если вы не знаете, что такое потоки, обратитесь к главе 3.

pipe = IO.pipe

reader = pipe[0]

writer = pipe[1]

str = nil

thread1 = Thread.new(reader,writer) do |reader,writer|

 # writer.close_write

 str = reader.gets

 reader.close

end

thread2 = Thread.new(reader,writer) do |reader,writer|

 # reader.close_read

 writer.puts("What hath God wrought?")

 writer.close

end

thread1.join

thread2.join

puts str # What hath God wrought?

 

10.1.14. Специальные операции ввода/вывода

В Ruby можно выполнять низкоуровневые операции ввода/вывода. Мы только упомянем о существовании таких методов; если вы собираетесь ими пользоваться, имейте в виду, что некоторые машиннозависимы (различаются даже в разных версиях UNIX).

Метод ioctl принимает два аргумента: целое число, определяющее операцию, и целое число либо строку, представляющую параметр этой операции.

Метод fcntl также предназначен для низкоуровневого управления файловыми потоками системно зависимым образом. Он принимает такие же параметры, как ioctl.

Метод select (в модуле Kernel) принимает до четырех параметров. Первый из них — массив дескрипторов для чтения, а остальные три необязательны (массив дескрипторов для записи, дескрипторов для ошибок и величина тайм-аута). Если на каком-то из устройств, дескрипторы которых заданы в первом массиве, оказываются новые данные для чтения или какое-то из устройств, дескрипторы которых перечислены во втором массиве, готово к выполнению записи, метод возвращает массив из трех элементов, каждый из которых в свою очередь является массивом, где указаны дескрипторы устройств, готовых к выполнению ввода/вывода.

Метод syscall из модуля Kernel принимает по меньшей мере один целочисленный параметр (а всего до девяти целочисленных или строковых параметров). Первый параметр определяет выполняемую операцию ввода/вывода.

Метод fileno возвращает обычный файловый дескриптор, ассоциированный с потоком ввода/вывода. Это наименее системно зависимый из всех перечислениях выше методов.

desc = $stderr.fileno # 2

 

10.1.15. Неблокирующий ввод/вывод

«За кулисами» Ruby предпринимает согласованные меры, чтобы операции ввода/вывода не блокировали выполнение программы. В большинстве случаев для управления вводом/выводом можно пользоваться потоками — один поток может выполнить блокирующую операцию, а второй будет продолжать работу.

Это немного противоречит интуиции. Потоки Ruby работают в том же процессе, они не являются платформенными потоками. Быть может, вам кажется, что блокирующая операция ввода/вывода должна приостанавливать весь процесс, а значит, и все его потоки. Это не так — Ruby аккуратно управляет вводом/выводом прозрачно для программиста.

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

require 'io/nonblock'

# ...

test = mysock.nonblock? # false

mysock.nonblock = true  # Отключить блокирующий режим.

# ...

mysock.nonblock = false # Снова включить его.

mysock.nonblock { some_operation(mysock) }

# Выполнить some_operation в неблокирующем режиме.

mysock.nonblock(false) { other_operation(mysock) }

# Выполнить other_operation в блокирующем режиме.

 

10.1.16. Применение метода readpartial

Метод readpartial появился сравнительно недавно с целью упростить ввод/вывод при определенных условиях. Он может использоваться с любыми потоками, например с сокетами.

Параметр «максимальная длина» (max length) обязателен. Если задан параметр buffer, то он должен ссылаться на строку, в которой будут храниться данные.

data = sock.readpartial(128) # Читать не более 128 байтов.

Метод readpartial игнорирует установленный режим блокировки ввода/вывода. Он может блокировать программу, но лишь при выполнении следующих условий: буфер объекта IO пуст, в потоке ничего нет и поток еще не достиг конца файла.

Таким образом, если в потоке есть данные, то readpartial не будет блокировать программу. Он читает не более указанного числа байтов, а если байтов оказалось меньше, то прочитает их и продолжит выполнение.

Если в потоке нет данных, но при этом достигнут конец файла, то readpartial немедленно возбуждает исключение EOFError.

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

При вызове метода sysread в блокирующем режиме он ведет себя похоже на readpartial. Если буфер пуст, их поведение вообще идентично.

 

10.1.17. Манипулирование путевыми именами

Основными методами для работы с путевыми именами являются методы класса File.dirname и File.basename; они работают, как одноименные команды UNIX, то есть возвращают имя каталога и имя файла соответственно. Если вторым параметром методу basename передана строка с расширением имени файла, то это расширение исключается.

str = "/home/dave/podbay.rb"

dir = File.dirname(str)          # "/home/dave"

file1 = File.basename(str)       # "podbay.rb"

file2 = File.basename(str,".rb") # "podbay"

Хотя это методы класса File, на самом деле они просто манипулируют строками.

Упомянем также метод File.split, который возвращает обе компоненты (имя каталога и имя файла) в массиве из двух элементов:

info = File.split(str) # ["/home/dave","podbay.rb"]

Метод класса expand_path преобразует путевое имя в абсолютный путь. Если операционная система понимает сокращения ~ и ~user, то они тоже учитываются.

Dir.chdir("/home/poole/personal/docs")

abs = File.expand_path("../../misc") # "/home/poole/misc"

Если передать методу path открытый файл, то он вернет путевое имя, по которому файл был открыт.

file = File.new("../../foobar")

name = file.path # "../../foobar"

Константа File::Separator равна символу, применяемому для разделения компонентов путевого имени (в Windows это обратная косая черта, а в UNIX — прямая косая черта). Имеется также синоним File::SEPARATOR.

Метод класса join использует этот разделитель для составления полного путевого имени из переданного списка компонентов:

path = File.join("usr","local","bin","someprog")

# path равно "usr/local/bin/someprog".

# Обратите внимание, что в начало имени разделитель не добавляется!

Не думайте, что методы File.join и File.split взаимно обратны, — это не так.

 

10.1.18. Класс Pathname

Следует знать о существовании стандартной библиотеки pathname, которая предоставляет класс Pathname. В сущности, это обертка вокруг классов Dir, File, FileTest и FileUtils, поэтому он комбинирует многие их функции логичным и интуитивно понятным способом.

path = Pathname.new("/home/hal")

file = Pathname.new("file.txt")

p2 = path + file

path.directory?     # true

path.file?          # false

p2.directory?       # false

p2.file?            # true

parts = path2.split # [Путевое имя:/home/hal, Путевое имя:file.txt]

ext = path2.extname # .txt

Как и следовало ожидать, имеется ряд вспомогательных методов. Метод root? пытается выяснить, относится ли данный путь к корневому каталогу, но его можно «обмануть», так как он просто анализирует строку, не обращаясь к файловой системе. Метод parent? возвращает путевое имя родительского каталога данного пути. Метод children возвращает непосредственных потомков каталога, заданного своим путевым именем; в их число включаются как файлы, так и каталоги, но рекурсивного спуска не производится.

p1 = Pathname.new("//") # Странно, но допустимо.

p1.root?                # true

р2 = Pathname.new("/home/poole")

p3 = p2.parent          # Путевое имя:/home

items = p2.children     # Массив объектов Pathname

                        # (все файлы и каталоги, являющиеся

                        # непосредственными потомками р2).

Как и следовало ожидать, методы relative и absolute пытаются определить, является ли путь относительным или абсолютным (проверяя, есть ли в начале имени косая черта):

p1 = Pathname.new("/home/dave")

p1.absolute? # true

p1.relative? # false

Многие методы, например size, unlink и пр., просто делегируют работу классам File, FileTest и FileUtils; повторно функциональность не реализуется.

Дополнительную информацию о классе Pathname вы найдете на сайте ruby-doc.org или в любом другом справочном руководстве.

 

10.1.19. Манипулирование файлами на уровне команд

Часто приходится манипулировать файлами так, как это делается с помощью командной строки: копировать, удалять, переименовывать и т.д.

Многие из этих операций реализованы встроенными методами, некоторые находятся в модуле FileUtils из библиотеки fileutils. Имейте в виду, что раньше функциональность модуля FileUtils подмешивалась прямо в класс File; теперь эти методы помещены в отдельный модуль.

Для удаления файла служит метод File.delete или его синоним File.unlink:

File.delete("history")

File.unlink("toast")

Переименовать файл позволяет метод File.rename:

File.rename("Ceylon","SriLanka")

Создать ссылку на файл (физическую или символическую) позволяют методы File.link и File.symlink соответственно:

File.link("/etc/hosts","/etc/hostfile") # Физическая ссылка.

File.symlink("/etc/hosts","/tmp/hosts") # Символическая ссылка.

Файл можно усечь до нулевой длины (или до любой другой), воспользовавшись методом экземпляра truncate:

File.truncate("myfile",1000) # Теперь не более 1000 байтов.

Два файла можно сравнить с помощью метода compare_file. У него есть синонимы cmp и compare_stream:

require "fileutils"

same = FileUtils.compare_file("alpha","beta") # true

Метод copy копирует файл в другое место, возможно, с переименованием. У него есть необязательный флаг, говорящий, что сообщения об ошибках нужно направлять на стандартный вывод для ошибок. Синоним — привычное для программистов UNIX имя cp.

require "fileutils"

# Скопировать файл epsilon в theta с протоколированием ошибок.

FileUtils.сору("epsilon","theta", true)

Файл можно перемещать методом move (синоним mv). Как и сору, этот метод имеет необязательный параметр, включающий вывод сообщений об ошибках.

require "fileutils"

FileUtils.move( "/trap/names", "/etc") # Переместить в другой каталог.

FileUtils.move("colours","colors")     # Просто переименовать.

Метод safe_unlink удаляет один или несколько файлов, предварительно пытаясь сделать их доступными для записи, чтобы избежать ошибок. Если последний параметр равен true или false, он интерпретируется как флаг, задающий режим вывода сообщений об ошибках.

require "fileutils"

FileUtils.safe_unlink("alpha","beta","gamma")

# Протоколировать ошибки при удалении следующих двух файлов:

FileUtils.safe_unlink("delta","epsilon",true)

Наконец, метод install делает практически то же, что и syscopy, но сначала проверяет, что целевой файл либо не существует, либо содержит такие же данные.

require "fileutils"

FileUtils.install("foo.so","/usr/lib")

# Существующий файл foo.so не будет переписан,

# если он не отличается от нового.

Дополнительную информацию о модуле FileUtils см. на сайте ruby-doc.org или в любом другом справочном руководстве.

 

10.1.20. Ввод символов с клавиатуры

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

Это можно сделать и в UNIX, и в Windows, но, к сожалению, совершенно по-разному.

Версия для UNIX прямолинейна. Мы переводим терминал в режим прямого ввода (raw mode) и обычно одновременно отключаем эхо-контроль.

def getchar

 system("stty raw -echo") # Прямой ввод без эхо-контроля.

 char = STDIN.getc

 system("stty -raw echo") # Восстановить режим терминала.

 char

end

На платформе Windows придется написать расширение на С. Пока что альтернативой является использование одной из функций в библиотеке Win32API.

require 'Win32API'

def getchar

 char = Win32API.new("crtdll", "_getch", [], 'L').Call

end

Поведение в обоих случаях идентично.

 

10.1.21. Чтение всего файла в память

Чтобы прочитать весь файл в массив, не нужно даже его предварительно открывать. Все сделает метод IO.readlines: откроет файл, прочитает и закроет.

arr = IO.readlines("myfile")

lines = arr.size

puts "myfile содержит #{lines} строк."

longest = arr.collect {|x| x.length}.max

puts "Самая длинная строка содержит #{longest} символов."

Можно также воспользоваться методом IO.read (который возвращает одну большую строку, а не массив строк).

str = IO.read("myfile")

bytes = arr.size

puts "myfile содержит #{bytes} байтов."

longest=str.collect {|x| x.length}.max # строки - перечисляемые объекты!

puts "Самая длинная строка содержит #{longest} символов."

Поскольку класс IO является предком File, то можно вместо этого писать File.deadlines и File.read.

 

10.1.22. Построчное чтение из файла

Чтобы читать по одной строке из файла, можно обратиться к методу класса IO.foreach или к методу экземпляра each. В первом случае файл не нужно явно открывать.

# Напечатать все строки, содержащие слово "target".

IO.foreach("somefile") do |line|

 puts line if line =~ /target/

end

# Другой способ...

file = File.new("somefile")

file.each do |line|

 puts line if line =~ /target/

end

Отметим, что each_line — синоним each.

 

10.1.23. Побайтное чтение из файла

Для чтения из файла по одному байту служит метод экземпляра each_byte. Напомним, что он передает в блок символ (то есть целое число); воспользуйтесь методом chr, если хотите преобразовать его в «настоящий» символ.

file = File.new("myfile")

e_count = 0

file.each_byte do |byte|

 e_count += 1 if byte == ?e

end

 

10.1.24. Работа со строкой как с файлом

Иногда возникает необходимость рассматривать строку как файл. Что под этим понимается, зависит от конкретной задачи.

Объект определяется прежде всего своими методами. В следующем фрагменте показано, как к объекту source применяется итератор; на каждой итерации выводится одна строка. Можете ли вы что-нибудь сказать о типе объекта source, глядя на этот код?

source.each do |line|

 puts line

end

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

В последних версиях Ruby имеется также библиотека stringio.

Интерфейс класса StringIO практически такой же, как в первом издании этой книги. В нем есть метод доступа string, ссылающийся на содержимое самой строки.

require 'stringio'

ios = StringIO.new("abcdefghijkl\nABC\n123")

ios.seek(5)

ios.puts("xyz")

puts ios.tell        # 8

puts ios.string.dump # "abcdexyzijkl\nABC\n123"

с = ios.getc

puts "с = #{c}"      # с = 105

ios.ungetc(?w)

puts ios.string.dump # "abcdexyzwjkl\nABC\n123"

puts "Ptr = #{ios.tell}"

s1 = ios.gets        # "wjkl"

s2 = ios.gets        # "ABC"

 

10.1.25. Чтение данных, встроенных в текст программы

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

При желании то же самое можно сделать и в Ruby. Директива __END__ в конце программы говорит, что дальше идут встроенные данные. Их можно читать из глобальной константы DATA, которая представляет собой обычный объект IO. (Отметим, что маркер __END__ должен располагаться с начала строки.)

# Распечатать все строки "задом наперед"...

DATA.each_line do |line|

 puts line.reverse

end

__END__

A man, a plan, a canal... Panama!

Madam, I'm Adam.

,siht daer nac uoy fI

.drah oot gnikrow neeb ev'uoy

 

10.1.26. Чтение исходного текста программы

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

Глобальная константа DATA — это объект класса IO, ссылающийся на данные, которые расположены после директивы __END__. Но если выполнить метод rewind, то указатель файла будет переустановлен на начало текста программы.

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

DATA.rewind

num = 1

DATA.each_line do |line|

 puts "#{'%03d' % num} #{line}"

 num += 1

end

__END__

Отметим, что наличие директивы __END__ обязательно — без нее к константе DATA вообще нельзя обратиться.

 

10.1.27. Работа с временными файлами

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

Все эти проблемы решает библиотека Tempfile. Метод new (синоним open) принимает базовое имя в качестве строки-затравки и конкатенирует его с идентификатором процесса и уникальным порядковым номером. Необязательный второй параметр — имя каталога, в котором создается временный файл; по умолчанию оно равно значению первой из существующих переменных окружения tmpdir, tmp или temp, а если ни одна из них не задана, то "/tmp".

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

У метода close есть необязательный флаг; если он равен true, то файл удаляется сразу после закрытия (не дожидаясь завершения программы). Метод path возвращает полное имя файла, если оно вам по какой-то причине понадобится.

require "tempfile"

temp = Tempfile.new("stuff")

name = temp.path # "/tmp/stuff17060.0"

temp.puts "Здесь был Вася"

temp.close

# Позже...

temp.open

str = temp.gets  # "Здесь был Вася"

temp.close(true) # Удалить СЕЙЧАС.

 

10.1.28. Получение и изменение текущего каталога

Получить имя текущего каталога можно с помощью метода Dir.pwd (синоним Dir.getwd). Эти имена уже давно употребляются как сокращения от «print working directory» (печатать рабочий каталог) и «get working directory» (получить рабочий каталог). На платформе Windows символы обратной косой черты преобразуются в символы прямой косой черты.

Для изменения текущего каталога служит метод Dir.chdir. В Windows в начале строки можно указывать букву диска.

Dir.chdir("/var/tmp")

puts Dir.pwd   # "/var/tmp"

puts Dir.getwd # "/var/tmp"

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

Dir.chdir("/home")

Dir.chdir("/tmp") do

 puts Dir.pwd # /tmp

 # Какой-то код...

end

puts Dir.pwd  # /home

 

10.1.29. Изменение текущего корня

В большинстве систем UNIX можно изменить «представление» процесса о том, что такое корневой каталог /. Обычно это делается из соображений безопасности перед запуском небезопасной или непротестированной программы. Метод chroot делает указанный каталог новым корнем:

Dir.chdir("/home/guy/sandbox/tmp")

Dir.chroot("/home/guy/sandbox")

puts Dir.pwd # "/tmp"

 

10.1.30. Обход каталога

Метод класса foreach — это итератор, который последовательно передает в блок каждый элемент каталога. Точно так же ведет себя метод экземпляра each.

Dir.foreach("/tmp") { |entry| puts entry }

dir = Dir.new("/tmp")

dir.each { |entry| puts entry }

Оба фрагмента печатают одно и то же (имена всех файлов и подкаталогов в каталоге /tmp).

 

10.1.31. Получение содержимого каталога

Метод класса Dir.entries возвращает массив, содержащий все элементы указанного каталога:

list = Dir.entries("/tmp") # %w[. .. alpha.txt beta.doc]

Как видите, включаются и элементы, соответствующие текущему и родительскому каталогу. Если они вам не нужны, придется отфильтровать их вручную.

 

10.1.32. Создание цепочки каталогов

Иногда необходимо создать глубоко вложенный каталог, причем промежуточные каталоги могут и не существовать. В UNIX мы воспользовались бы для этого командой mkdir -p.

В программе на Ruby такую операцию выполняет метод FileUtils.makedirs (из библиотеки fileutils):

require "fileutils"

FileUtils.makedirs("/tmp/these/dirs/need/not/exist")

 

10.1.33. Рекурсивное удаление каталога

В UNIX команда rm -rf dir удаляет все поддерево начиная с каталога dir. Понятно, что применять ее надо с осторожностью.

В последних версиях Ruby в класс Pathname добавлен метод rmtree, решающий ту же задачу. В модуле FileUtils есть аналогичный метода rm_r.

require 'pathname'

dir = Pathname.new("/home/poole/")

dir.rmtree

# или:

require 'fileutils'

FileUtils.rm_r("/home/poole")

 

10.1.34. Поиск файлов и каталогов

Ниже мы воспользовались стандартной библиотекой find.rb для написания метода, который находит один или более файлов и возвращает их список в виде массива. Первый параметр — это начальный каталог, второй — либо имя файла (строка), либо регулярное выражение.

require "find"

def findfiles(dir, name)

 list = []

 Find.find(dir) do |path|

  Find.prune if [".",".."].include? Path

  case name

   when String

    list << path if File.basename(path) == name

   when Regexp

    list << path if File.basename(path) =~ name

   else

    raise ArgumentError

  end

 end

 list

end

findfiles "/home/hal", "toc.txt"

# ["/home/hal/docs/toc.txt", "/home/hal/misc/toc.txt"]

findfiles "/home", /^[a-z]+.doc/

# ["/home/hal/docs/alpha.doc", "/home/guy/guide.doc",

# "/home/bill/help/readme.doc"]

 

10.2. Доступ к данным более высокого уровня

 

Часто возникает необходимость хранить и извлекать данные более прозрачным способом. Модуль Marshal предоставляет простые средства сохранения объектов а на его основе построена библиотека PStore. Наконец, библиотека dbm позволяет организовать нечто вроде хэша на диске. Строго говоря, она не относится к теме данного раздела, но уж слишком проста, чтобы рассказывать о ней в разделе, посвященном базам данных.

 

10.2.1. Простой маршалинг

Часто бывает необходимо создать объект и сохранить его для последующего использования. В Ruby есть рудиментарная поддержка для обеспечения устойчивости объекта или маршалинга. Модуль Marshal позволяет сериализовать и десериализовать объекты.

# Массив элементов [composer, work, minutes]

works = [["Leonard Bernstein","Overture to Candide",11],

["Aaron Copland","Symphony No. 3",45],

["Jean Sibelius","Finlandia",20]]

# Мы хотим сохранить его для последующего использования...

File.open("store","w") do |file|

 Marshal.dump(works,file)

end

# Намного позже...

File.open("store") do |file|

 works = Marshal.load(file)

end

Недостаток такого подхода заключается в том, что не все объекты можно сохранить. Для объектов, включающих другие объекты низкого уровня, маршалинг невозможен. К числу таких низкоуровневых объектов относятся, в частности, IO, Proc и Binding. Нельзя также сериализовать синглетные объекты, анонимные классы и модули.

Метод Marshal.dump можно вызывать еще двумя способами. Если он вызывается с одним параметром, то возвращает данные в виде строки, в которой первые два байта — это номер старшей и младшей версии.

s = Marshal.dump(works)

p s[0] # 4

p s[1] # 8

Обычно попытка загрузить такие данные оказывается успешной только в случае, если номера старших версий совпадают и номер младшей версии данных не больше младшей версии метода. Но если при вызове интерпретатора Ruby задан флаг «болтливости» (verbose или v), то версии должны совпадать точно. Эти номера версий не связаны с номерами версий Ruby.

Третий параметр limit (целое число) имеет смысл, только если сериализуемый объект содержит вложенные объекты. Если он задан, то интерпретируется методом Marshal.dump как максимальная глубина обхода объекта. Если уровень вложенности меньше указанного порога, то объект сериализуется без ошибок; в противном случае возбуждается исключение ArgumentError. Проще пояснить это на примере:

File.open("store","w") do |file|

 arr = []

 Marshal.dump(arr,file,0) # Внутри 'dump': превышена пороговая глубина.

                          # (ArgumentError)

 Marshal.dump(arr,file,1)

 arr = [1, 2, 3]

 Marshal.dump(arr,file,1) # Внутри 'dump': превышена пороговая глубина.

                          # (ArgumentError)

 Marshal.dump(arr,file,2) arr = [1, [2], 3]

 Marshal.dump(arr,file,2) # Внутри 'dump': превышена пороговая глубина.

                          # (ArgumentError)

 Marshal.dump(arr,file,3)

end

File.open("store") do |file|

 p Marshal.load(file) # [ ]

 p Marshal.load(file) # [1, 2, 3]

 p Marshal.load(file) # arr = [1, [2], 3]

end

По умолчанию третий параметр равен 1. Отрицательное значение означает, что глубина вложенности не проверяется.

 

10.2.2. Более сложный маршалинг

Иногда мы хотим настроить маршалинг под свои нужды. Такую возможность дают методы _load и _dump. Они вызываются во время выполнения маршалинга, чтобы вы могли самостоятельно реализовать преобразование данных в строку и обратно.

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

class Person

 attr_reader :name

 attr_reader :age

 attr_reader :balance

 def initialize(name,birthdate,beginning)

  @name = name

  @birthdate = birthdate

  @beginning = beginning

  @age = (Time.now - @birthdate)/(365*86400)

  @balance = @beginning*(1.05**@age)

 end

 def marshal_dump

  Struct.new("Human",:name,:birthdate,:beginning)

  str = Struct::Human.new(@name, @birthdate, @beginning)

  str

 end

 def marshal_load(str)

  self.instance_eval do

   initialize(str.name, str.birthdate, str.beginning)

  end

 end

 # Прочие методы...

end

p1 = Person.new("Rudy",Time.now - (14 * 365 * 86400), 100)

p [p1.name, p1.age, p1.balance] # ["Rudy", 14.0, 197.99315994394]

str = Marshal.dump(p1)

p2 = Marshal.load(str)

p [p2.name, p2.age, p2.balance] # ["Rudy", 14.0, 197.99315994394]

При сохранении объекта этого типа атрибуты age и balance не сохраняются. А когда объект восстанавливается, они вычисляются заново. Заметьте: метод marshal_load предполагает, что объект существует; это один из немногих случаев, когда метод initialize приходится вызывать явно (обычно это делает метод new).

 

10.2.3. Ограниченное «глубокое копирование» в ходе маршалинга

В Ruby нет операции «глубокого копирования». Методы dup и clone не всегда работают, как ожидается. Объект может содержать ссылки на вложенные объекты, а это превращает операцию копирования в игру «собери палочки».

Ниже предлагается способ реализовать глубокое копирование с некоторыми ограничениями, обусловленными тем, что наш подход основан на использовании класса Marshal со всеми присущими ему недостатками:

def deep_copy(obj)

 Marshal.load(Marshal.dump(obj))

end

a = deep_copy(b)

 

10.2.4. Обеспечение устойчивости объектов с помощью библиотеки PStore

Библиотека PStore реализует хранение объектов Ruby в файле. Объект класса PStore может содержать несколько иерархий объектов Ruby. У каждой иерархии есть корень, идентифицируемый ключом. Иерархии считываются с диска в начале транзакции и записываются обратно на диск в конце.

require "pstore"

# Сохранить.

db = PStore.new("employee.dat") db.transaction do

 db["params"] = {"name" => "Fred", "age" => 32,

                 "salary" => 48000 }

end

# Восстановить.

require "pstore"

db = Pstore.new("employee.dat")

emp = nil

db.transaction { emp = db["params"] }

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

Эта техника ориентирована на транзакции; в начале блока обрабатываемые данные читаются с диска. А в конце прозрачно для программиста записываются на диск.

Мы можем завершить транзакцию досрочно, вызвав метод commit или abort. В первом случае все изменения сохраняются, во втором отбрасываются. Рассмотрим более длинный пример:

require "pstore"

# Предполагается, что существует файл с двумя объектами.

store = PStore.new("objects")

store.transaction do |s|

 a = s["my_array"] h = s["my_hash"]

 # Опущен воображаемый код, манипулирующий объектами

 # a, h и т. д.

 # Предполагается, что переменная "condition" может

 # принимать значения 1, 2, 3...

 case condition

  when 1

   puts "Отмена."

   s.abort # Изменения будут потеряны.

  when 2

   puts "Фиксируем и выходим."

   s.commit # Изменения будут сохранены.

  when 3

   # Ничего не делаем...

 end

 puts "Транзакция дошла до конца."

 # Изменения будут сохранены.

end

Внутри транзакции можно вызвать метод roots, который вернет массив корней (или метод root?, чтобы проверить принадлежность). Есть также метод delete, удаляющий корень.

store.transaction do |s|

 list = s.roots  # ["my_array","my_hash"]

 if s.root?("my_tree")

  puts "Найдено my_tree."

 else

  puts "He найдено # my_tree."

 end

 s.delete("my_hash")

 list2 = s.roots # ["my_array"]

end

 

10.2.5. Работа с данными в формате CSV

CSV (comma-separated values — значения, разделенные запятыми) — это формат, с которым вам доводилось сталкиваться, если вы работали с электронными таблицами или базами данных. К счастью, Хироси Накамура (Hiroshi Nakamura) написал для Ruby соответствующий модуль и поместил его в архив приложений Ruby.

Имеется также библиотека FasterCSV, которую создал Джеймс Эдвард Грей III (James Edward Gray III). Как явствует из названия, она работает быстрее, к тому же имеет несколько видоизмененный и улучшенный интерфейс (хотя для пользователей старой библиотеки есть «режим совместимости»). Во время работы над книгой велись дискуссии о том, следует ли сделать библиотеку FasterCSV стандартной, заменив старую библиотеку (при этом ей, вероятно, будет присвоено старое имя).

Ясно, что это не настоящая база данных. Но более подходящего места, чем эта глава, для нее не нашлось.

Модуль CSV (csv.rb) разбирает или генерирует данные в формате CSV. О том, что представляет собой последний, нет общепринятого соглашения. Автор библиотеки определяет формат следующим образом:

• разделитель записей: CR + LF;

• разделитель полей: запятая (,);

• данные, содержащие символы CR, LF или запятую, заключаются в двойные кавычки;

• двойной кавычке внутри двойных кавычек должен предшествовать еще один символ двойной кавычки ("→"");

• пустое поле в кавычках обозначает пустую строку (данные,"",данные);

• пустое поле без кавычек означает NULL (данные,,данные).

В настоящем разделе мы рассмотрим лишь часть функциональных возможностей библиотеки. Этого достаточно для введения в предмет, а самую актуальную документацию, как всегда, можно найти в сети (начните с сайта ruby-doc.org).

Начнем с создания файла. Чтобы вывести данные, разделенные запятыми, мы просто открываем файл для записи; метод open передает объект-писатель в блок. Затем с помощью оператора добавления мы добавляем массивы данных (при записи они преобразуются в формат CSV). Первая строка является заголовком.

require 'csv'

CSV.open("data.csv","w") do |wr|

 wr << ["name", "age", "salary"]

 wr << ["mark", "29", "34500"]

 wr << ["joe", "42", "32000"]

 wr << ["fred", "22", "22000"]

 wr << ["jake", "25", "24000"]

 wr << ["don", "32", "52000"]

end

В результате исполнения этого кода мы получаем такой файл data.csv:

"name","age","salary"

"mark",29,34500

"joe",42,32000

"fred",22,22000

"jake",25,24000

"don",32,52000

Другая программа может прочитать этот файл:

require 'csv'

CSV.open('data.csv', ' r') do |row|

 p row

end

# Выводится:

# ["name", "age", "salary"]

# ["mark", "29", "34500"]

# ["joe", "42", "32000"]

# ["fred", "22", "22000"]

# ["jake", "25", "24000"]

# ["don", "32", "52000"]

Этот фрагмент можно было бы записать и без блока, тогда метод open просто вернул бы объект-читатель. Затем можно было бы вызвать метод shift читателя (как если бы это был массив) для получения очередной строки. Но блочная форма мне представляется более естественной.

В библиотеке есть и более развитые средства, а также вспомогательные методы. Для получения дополнительной информации обратитесь к сайту ruby-doc.org или архиву приложений Ruby.

 

10.2.6. Маршалинг в формате YAML

Аббревиатура YAML означает «YAML Ain't Markup Language» (YAML — не язык разметки). Это не что иное, как гибкий, понятный человеку формат хранения данных. Он напоминает XML, но «красивее».

Затребовав директивой require библиотеку yaml, мы добавляем в каждый объект метод to_yaml. Поучительно будет посмотреть на результат вывода в этом формате нескольких простых и более сложных объектов.

require 'yaml'

str = "Hello, world"

num = 237

arr = %w[ Jan Feb Mar Apr ]

hsh = {"This" => "is", "just a"=>"hash."}

puts str.to_yaml

puts num.to_yaml

puts arr.to_yaml

puts hsh.to_yaml

# Выводится:

# --- "Hello, world"

# --- 237

# ---

# - Jan

# - Feb

# - Mar

# - Apr

# ---

# just a: hash.

# This: is

Обратным по отношению к to_yaml является метод YAML.load, который принимает в качестве параметра строку или поток.

Предположим, что имеется такой файл data.yaml:

---

- "Hello, world"

- 237

-

  - Jan

  - Feb

  - Mar

  - Apr

-

 just a: hash.

 This: is

Это те же четыре элемента данных, которые мы видели раньше, только они сгруппированы в единый массив. Если загрузить этот поток, то получим массив-

require 'yaml'

file = File.new("data.yaml")

array = YAML.load(file)

file.close

p array

# Выводится:

# ["Hello, world", 237, ["Jan", "Feb", "Mar", "Apr"],

# {"just a"=>"hash.", "This"=>"is"} ]

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

YAML позволяет и многое другое, о чем мы не можем здесь рассказать. Дополнительную информацию вы найдете на сайте ruby-doc.org или в справочном руководстве.

 

10.2.7. Преобладающие объекты и библиотека Madeleine

В некоторых кругах популярна идея преобладающих объектов (object prevalence). Смысл ее в том, что память дешева и продолжает дешеветь, а базы данных в большинстве своем невелики, поэтому о них можно вообще забыть и хранить все объекты в памяти.

Классической реализацией является пакет Prevayler, написанный на языке Java. Версия для Ruby называется Madeleine.

Madeleine годится не для всех приложений. У методики преобладающих объектов есть собственные правила и ограничения. Все объекты должны, во-первых, помещаться в памяти; во-вторых, быть сериализуемы.

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

Объекты должны быть по возможности изолированы от ввода/вывода (файлов и сети). Обычно весь ввод/вывод выполняется вне системы преобладающих объектов.

Наконец, любая команда, которая изменяет состояние системы преобладающих объектов, должна иметь вид объекта-команды (то есть для таких объектов тоже должна иметься возможность сериализации и сохранения).

Madeleine предлагает два основных метода доступа к системе объектов. Метод execute_query позволяет выполнить запрос или получить доступ для чтения. Метод execute_command инкапсулирует любую операцию, которая изменяет состояние объектов в системе.

Оба метода принимают в качестве параметра объект Command. По определению такой объект должен иметь метод execute.

Работа системы состоит в том, что во время исполнения приложения она периодически делает моментальные снимки всей системы объектов. Команды сериализуются наравне с другими объектами. В настоящее время не существует способа «откатить» набор транзакций.

Трудно привести содержательный пример использования этой библиотеки. Если вы знакомы с Java-версией, рекомендую изучить API для Ruby и освоить ее таким образом. Хороших руководств нет — может быть, вы напишете первое.

 

10.2.8. Библиотека DBM

DBM — платформенно-независимый механизм для хранения строк в файле, как в хэше. И ключ, и ассоциированные с ним данные должны быть строками. Интерфейс dbm включен в стандартный дистрибутив Ruby.

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

require 'dbm'

d = DBM.new("data")

d["123"] = "toodle-oo!"

puts d["123"] # "toodle-oo!"

d.close

puts d["123"] # RuntimeError: закрытый DBM-файл.

e = DBM.open("data")

e["123"]      # "toodle-oo!"

w=e.to_hash   # {"123"=>"toodle-oo!"}

e.close

e["123"]      # RuntimeError: закрытый DBM-файл.

w["123"]      # "toodle-oo!

Интерфейс к DBM реализован в виде одного класса, к которому подмешан модуль Enumerable. Два метода класса (синонимы) new и open являются синглетами, то есть в любой момент времени можно иметь только один объект DBM, связанный с данным файлом.

q=DBM.new("data.dbm")  #

f=DBM.open("data.dbm") # Errno::EWOULDBLOCK:

                       #  Try again - "data.dbm"

Всего есть 34 метода экземпляра, многие из которых являются синонимами или аналогичны методам хэша. Почти все операции с настоящим хэшем применимы и к объекту dbm.

Метод to_hash создает представление файла в виде хэша в памяти, а метод close закрывает связь с файлом. Остальные методы по большей части аналогичны методам хэшам, однако дополнительно есть методы rehash, sort, default, default=. Метод to_s возвращает строковое представление идентификатора объекта.

 

10.3. Библиотека KirbyBase

KirbyBase — небольшая библиотека, с которой должен освоиться каждый программист на Ruby. В настоящее время она не входит в стандартный дистрибутив, а если бы входила, то была бы еще полезнее.

KirbyBase — плод трудов Джейми Криббса (Jamey Cribbs), названный, к слову, в честь его собаки. Во многих отношениях это полноценная база данных, но есть причины, по которым мы рассматриваем ее здесь, а не вместе с MySQL и Oracle.

Во-первых, это не автономное приложение. Это библиотека для Ruby, и без Ruby ее использовать нельзя. Во-вторых, она вообще не знает, что такое язык SQL. Если вам без SQL не обойтись, то эта библиотека не для вас. В-третьих, если приложение достаточно сложное, то функциональных возможностей и быстродействия KirbyBase может не хватить.

Но несмотря на все это, есть немало причин любить KirbyBase. Это написанная целиком на Ruby библиотека, состоящая из единственного файла, которую не нужно ни устанавливать, ни конфигурировать. Она работает на всех платформах, и созданные с ее помощью файлы можно переносить с одной платформы на другую. Это «настоящая» база данных в том смысле, что данные не загружаются целиком в память.

Библиотекой легко пользоваться, а ее интерфейс выдержан в духе Ruby с легким налетом DBI. В общем, база данных соответствует каталогу, а каждая таблица — одному файлу. Формат данных в таблицах таков, что человек может их читать (и редактировать). Дополнительно таблицы можно зашифровать — но только для того, чтобы затруднить редактирование. База знает об объектах Ruby; допускается их хранение и извлечение без потери информации.

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

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

Чтобы создать таблицу, вызывается метод create_table объекта, представляющего базу данных; ему передается имя таблицы (объект Symbol); имя файла на диске образуется из этого имени. Затем передается последовательность пар символов, описывающих имена и типы полей.

require 'kirbybase'

db = KirbyBase.new(:local, nil, nil, "mydata")

books = db.create_table(:books, # Имя таблицы.

         :title, :String,       # Поле, тип, ...

         :author, :String)

В текущей версии KirbyBase распознает следующие типы полей: String, Integer, Float, Boolean, Time, Date, DateTime, Memo, Blob и YAML. К тому моменту, когда вы будете читать эту главу, возможно, появятся и новые типы.

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

books.insert("The Case for Mars","Robert Zubrin")

books.insert(:title => "Democracy in America",

             :author => "Alexis de Tocqueville")

Book = Struct.new(:title, :author)

book = Book.new("The Ruby Way","Hal Fulton")

books.insert(book)

В любом случае метод insert возвращает идентификатор строки, соответствующей новой записи (вы можете использовать его или игнорировать). Это «скрытое» автоинкрементное поле, присутствующее в каждой записи любой таблицы. Для выборки записей служит метод select. Без параметров он выбирает все поля всех записей таблицы. Набор полей можно ограничить, передав в качестве параметров символы. Если задан блок, то он определяет, какие записи отбирать (примерно так же, как работает метод find_all для массивов).

list1 = people.select             # Все люди, все поля.

list2 = people.select(:name,:age) # Все люди, только имя и возраст.

list3 = people.select(:name) {|x| x.age >= 18 && x.age < 30 }

# Имена всех людей от 18 до 30 лет.

В блоке допустимы любые операции. Это означает, например, что можно формулировать запрос с помощью регулярных выражений (в отличие от типичной SQL-базы).

Результирующий набор, возвращаемый KirbyBase, можно сортировать по нескольким ключам в порядке возрастания или убывания. Для сортировки по убыванию перед именем ключа ставится минус. (Это работает, потому что в класс Symbol добавлен метод, соответствующий унарному минусу.)

sorted = people.select.sort(:name,-:age)

# Отсортировать в порядке возрастания name и в порядке убывания age.

У результирующего набора есть одно интересное свойство: он может предоставлять массивы, «срезающие» результат. С первого раза это довольно трудно понять.

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

list = people.select(:name,:age,:heightweight)

p list[0]         # Вся информация о человеке 0.

p list[1].age     # Только возраст человека 1.

p list[2].height  # Рост человека 2.

ages = list.age   # Массив: возрасты всех людей.

names = list.name # Массив: имена всех людей.

В KirbyBase есть ограниченные средства печати отчетов; достаточно вызвать метод to_report для любого результирующего набора. Пример:

rpt = books.select.sort(:title).to_report

puts rpt

# Выводится:

# recno | title                | author

# -----------------------------------------------------------

#     2 | Democracy in America | Alexis de Tocqueville

#     1 | The Case for Mars    | Robert Zubrin

#     3 | The Ruby Way         | Hal Fulton

Атрибут таблицы encrypt можно установить в true — тогда данные нельзя будет читать и редактировать, как обычный текст. Но имейте в виду, что для этого применяется шифр Вигенера — не «игрушечный», но и не являющийся криптографически безопасным. Так что пользоваться шифрованием имеет смысл только для того, чтобы помешать редактированию, но никак не для сокрытия секретных данных. Обычно режим шифрования устанавливается в блоке при создании таблицы:

db.create_table(:mytable, f1, :String, f2, :Date) {|t| t.encrypt = true }

Поскольку удаленный доступ — интересное средство, уделим ему немного внимания. Вот пример сервера:

require 'kirbybase'

require 'drb'

host = 'localhost'

port = 44444

db = KirbyBase.new(:server) # Создать экземпляр базы данных.

DRb.start_service("druby://#{host} :#{port)", db)

DRb.thread.join

Это прямое применение интерфейса dRuby (см. главу 20). На стороне клиента следует при подключении к базе данных задать символ :client вместо обычного :local.

db = KirbyBase.new(:client,'localhost',44444)

# Весь остальной код не изменяется.

Можно также выполнять обычные операции: обновлять и удалять записи, удалять таблицы и т.д. Есть и более сложные механизмы, о которых я не буду рассказывать подробно: связи один-ко-многим, вычисляемые поля и нестандартные классы записей. Подробнее см. документацию по KirbyBase на сайте RubyForge.

 

10.4. Подключение к внешним базам данных

 

Благодаря усилиям многих людей Ruby может взаимодействовать с разными базами данных, от монолитных систем типа Oracle до более скромного MySQL. Для полноты описания мы включили в него также текстовые файлы в формате CSV.

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

 

10.4.1. Интерфейс с SQLite

SQLite — популярная база данных для тех, кто ценит программное обеспечение, которое не нужно конфигурировать. Это небольшая автономная исполняемая программа, написанная на языке С, которая хранит всю базу данных в одном файле. Хотя обычно она используется для небольших баз, но теоретически способна управиться с терабайтными объемами.

Привязка Ruby к SQLite довольно прямолинейна. API, написанный на С, обернут в класс SQLite::API. Поскольку при этом методы отображаются один в один и интерфейс не назовешь образцом объектной ориентированности, пользоваться этим API стоит только в случае острой необходимости.

В большинстве ситуаций вам будет достаточно класса SQLite::Database. Вот пример кода:

require 'sqlite'

db = SQLite::Database.new("library.db")

db.execute("select title,author from books") do |row|

 p row

end

db.close

# Выводится:

# ["The Case for Mars", "Robert Zubrin"]

# ["Democracy in America", "Alexis de Tocqueville"]

# ...

Если блок не задан, то метод execute возвращает объект ResultSet (по сути, курсор, который можно перемещать по набору записей).

rs = db.execute("select title,author from books")

rs.each {|row| p row } # Тот же результат, что и выше.

rs.close

Если получен объект ResultSet, то программа должна будет рано или поздно закрыть его (как показано в примере выше). Если нужно обойти список записей несколько раз, то с помощью метода reset можно вернуться в начало. (Это экспериментальное средство, которое в будущем может измениться.) Кроме того, можно производить обход в духе генератора с помощью методов next и eof?.

rs = db.execute("select title,author from books")

while ! rs.eof?

 rec = rs.next

 p rec # Тот же результат, что и выше.

end

rs.close

Методы библиотеки могут возбуждать различные исключения. Все они являются подклассами класса SQLite::Exception, так что легко перехватываются поодиночке или целой группой.

Отметим еще, что библиотека написана так, что может работать совместно с библиотекой ArrayFields Ары Ховарда (Ara Howard). Она позволяет получать доступ к элементам массива по индексу или по имени. Если перед sqlite затребована библиотека arrayfields, то объект ResultSet можно индексировать как числами, так и именами полей. (Но можно задать и такую конфигурацию, что вместо этого будет возвращаться объект Hash.)

Хотя библиотека sqlite вполне развита, она не покрывает всех мыслимых потребностей просто потому, что сама база данных SQLite не полностью реализует стандарт SQL92. Дополнительную информацию об SQLite и привязке к Ruby ищите в сети.

 

10.4.2. Интерфейс с MySQL

Интерфейс Ruby с MySQL — один из самых стабильных и полнофункциональных среди всех интерфейсов с базами данных. Это расширение, которое должно устанавливаться после инсталляции Ruby и MySQL.

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

require 'mysql'

m = Mysql.new("localhost","ruby","secret","maillist")

r = m.query("SELECT * FROM people ORDER BY name")

r.each_hash do |f|

 print "#{f['name']} - #{f['email']}"

end

# Выводится что-то вроде:

# John Doe - [email protected]

# Fred Smith - [email protected]

Особенно полезны методы класса Mysql.new и MysqlRes.each_hash, а также метод экземпляра query.

Модуль состоит из четырех классов (Mysql, MysqlRes, MysqlField и MysqlError), описанных в файле README. Мы приведем сводку некоторых наиболее употребительных методов, а дополнительную информацию вы сможете найти сами в официальной документации.

Метод класса Mysql.new принимает несколько строковых параметров, которые по умолчанию равны nil, и возвращает объект, представляющий соединение. Параметры называются host, user, passwd, db, port, sock и flag. У метода new есть синонимы real_connect и connect.

Методы create_db, select_db и drop_db принимают в качестве параметров имя базы данных и используются, как показано ниже. Метод close закрывает соединение с сервером.

m=Mysql.new("localhost","ruby","secret")

m.create_db("rtest")  # Создать новую базу данных.

m.select_db("rtest2") # Выбрать другую базу данных.

in.drop_db("rtest")   # Удалить базу данных.

m.close               # Закрыть соединение.

В последних версиях методы create_db и drop_db объявлены устаревшими. Но можно «воскресить» их, определив следующим образом:

class Mysql

 def create_db(db)

  query("CREATE DATABASE #{db}")

 end

 def drop_db(db)

  query("DROP DATABASE #{db}")

 end

end

Метод list_dbs возвращает список имен доступных баз данных в виде массива.

dbs = m.list_dbs # ["people","places","things"]

Метод query принимает строковый параметр и по умолчанию возвращает объект MysqlRes. В зависимости от заданного значения свойства query_with_result может также возвращаться объект Mysql.

Если произошла ошибка, то ее номер можно получить, обратившись к методу errno. Метод error возвращает текст сообщения об ошибке.

begin

 r=m.query("create table rtable

 (

  id int not null auto_increment,

  name varchar(35) not null,

  desc varchar(128) not null,

  unique id(id)

 )")

# Произошло исключение...

rescue

 puts m.error

  # Печатается: You have an error in your SQL syntax

  # near 'desc varchar(128) not null ,

  # unique id(id)

  # )' at line 5"

 puts m.errno

  # Печатается 1064

  # ('desc' is reserved for descending order)

end

Ниже перечислено несколько полезных методов экземпляра, определенных в классе MysqlRes:

• fetch_fields возвращает массив объектов MysqlField, соответствующих полям в следующей строке;

• fetch_row возвращает массив значений полей в следующей строке;

• fetch_hash(with_table=false) возвращает хэш, содержащий имена и значения полей в следующей строке;

• num_rows возвращает число строк в результирующем наборе;

• each — итератор, последовательно возвращающий массив значений полей;

• each_hash(with_table=false) — итератор, последовательно возвращающий хэш вида {имя_поля => значение_поля} (пользуйтесь нотацией x['имя_поля'] для получения значения поля).

Вот некоторые методы экземпляра, определенные в классе MysqlField:

• name возвращает имя поля;

• table возвращает имя таблицы, которой принадлежит поле;

• length возвращает длину поля, заданную при определении таблицы;

• max_length возвращает длину самого длинного поля в результирующем наборе;

• hash возвращает хэш с именами и значениями следующих элементов описания: name, table, def, type, length, max_length, flags, decimals.

Если изложенный здесь материал противоречит онлайновой документации, предпочтение следует отдать документации. Более подробную информацию вы найдете на официальном сайте MySQL (http://www.mysql.com) и в архиве приложений Ruby.

 

10.4.3. Интерфейс с PostgreSQL

В архиве RAA есть также расширение, реализующее доступ к СУБД PostgreSQL (работает с версиями PostgreSQL 6.5/7.0).

В предположении, что PostgreSQL уже установлена и сконфигурирована (и в базе данных есть таблица testdb), нужно лишь выполнить те же шаги, что и для всех остальных интерфейсов Ruby с базами данных: загрузить модуль, установить соединение с базой данных и начать работу. Надо полагать, вам понадобится способ послать запрос, получить результаты и работать с транзакциями.

require 'postgres'

conn = PGconn.connect("", 5432, "", "", "testdb")

conn.exec("create table rtest ( number integer default 0 );")

conn.exec("insert into rtest values ( 99 )")

res = conn.query("select * from rtest")

# res id [["99"]]

В классе PGconn есть метод connect, который принимает обычные параметры для установления соединения: имя хоста, номер порта, имя базы данных, имя и пароль пользователя. Кроме того, третий и четвертый параметры — соответственно, флаги и параметры терминала. В приведенном примере мы установили соединение через сокет UNIX от имени привилегированного пользователя, поэтому не указывали ни имя пользователя, ни пароль, а имя хоста, флаги и параметры терминала оставили пустыми. Номер порта должен быть целым числом, а остальные параметры — строками. У метода connect есть синоним new.

Для работы с таблицами нужно уметь выполнять запросы. Для этого служат методы PGconn#exec и PGconn#query.

Метод exec посылает переданную ему строку — SQL-запрос — серверу PostgreSQL и получает ответ в виде объекта PGresult, если выполнение завершилось успешно. В противном случае он возбуждает исключение PGError.

Метод query также посылает свой строковый параметр в виде SQL-запроса. Но в случае успеха получает массив кортежей. В случае ошибки возвращается nil, а подробности можно получить, вызвав метод error.

Имеется специальный метод insert_table для вставки записи в указанную таблицу. Вопреки названию он не создает новую таблицу, а добавляет данные в существующую. Этот метод возвращает объект PGconn.

conn.insert_table("rtest",[[34]])

res = conn.query("select * from rtest")

res равно [["99"], ["34"]]

В этом примере в таблицу rtest вставляется одна строка. Для простоты мы указали только одну колонку. Отметим, что объект res класса PGresult после обновления возвращает массив из двух кортежей. Чуть ниже мы рассмотрим методы, определенные в классе PGresult.

В классе PGconn определены также следующие полезные методы:

• db возвращает имя базы, с которой установлено соединение;

• host возвращает имя сервера, с которым установлено соединение;

• user возвращает имя аутентифицированного пользователя;

• error возвращает сообщение об ошибке;

• finish, close закрывают соединение;

• loimport(file) импортирует файл в большой двоичный объект (BLOB), в случае успеха возвращает объект PGlarge, иначе возбуждает исключение PGError;

• loexport(oid, file) выгружает BLOB с идентификатор oid в указанный файл;

• locreate([mode]) возвращает объект PGlarge в случае успеха, иначе возбуждает исключение PGError;

• loopen(oid, [mode]) открывает BLOB с идентификатором oid. Возвращает объект PGlarge в случае успеха. Аргумент mode задает режим работы с открытым объектом: "INV_READ" или "INV_WRITE" (если этот аргумент опущен, по умолчанию предполагается "INV_READ");

• lounlink(oid) удаляет BLOB с идентификатором oid.

Отметим, что пять последних методов (loimport, loexport, locreate, loopen и lounlink) работают с объектами класса PGlarge. У этого класса есть собственные методы для доступа к объекту и его изменения. (BLOB'ы создаются в результате выполнения методов loimport, locreate, loopen экземпляра.)

Ниже перечислены методы, определенные в классе PGlarge:

• open([mode]) открывает BLOB. Аргумент mode задает режим работы с объектом, как и в случае с методом PGconn#loopen);

• close закрывает BLOB (BLOB'ы также закрываются автоматически, когда их обнаруживает сборщик мусора);

• read([length]) пытается прочитать length байтов из BLOB'a. Если параметр length не задан, читаются все данные;

• write(str) записывает строку в BLOB и возвращает число записанных байтов;

• tell возвращает текущую позицию указателя;

• seek(offset, whence) перемещает указатель в позицию offset. Параметр whence может принимать значения SEEK_SET, SEEK_CUR и SEEK_END (равные соответственно 0,1,2);

• unlink удаляет BLOB;

• oid возвращает идентификатор BLOB'a;

• size возвращает размер BLOB'a;

• export(file) сохраняет BLOB в файле с указанным именем.

Более интересны методы экземпляра, определенные в классе PGresult (перечислены ниже). Объект такого класса возвращается в результате успешного выполнения запроса. (Для экономии памяти вызывайте метод PGresult#clear по завершении работы с таким объектом.)

• result возвращает массив кортежей, описывающих результат запроса;

• each — итератор;

• [] — метод доступа;

• fields возвращает массив описаний полей результата запроса;

• num_tuples возвращает число кортежей в результате запроса;

• fieldnum(name) возвращает индекс поля с указанным именем;

• type(index) возвращает целое число, соответствующее типу поля;

• size(index) возвращает размер поля в байтах. 1 означает, что поле имеет переменную длину;

• getvalue(tup_num, field_num) возвращает значение поля с указанным порядковым номером; tup_num — номер строки;

• getlength(tup_num, field_num) возвращает длину поля в байтах;

• cmdstatus возвращает строку состояния для последнего запроса;

• clear очищает объект PGresult.

 

10.4.4. Интерфейс с LDAP

Для Ruby есть по меньшей мере три разных библиотеки, позволяющих работать с протоколом LDAP. Ruby/LDAP, написанная Такааки Татеиси (Takaaki Tateishi), — это довольно «тонкая» обертка. Если вы хорошо знакомы с LDAP, то ее может оказаться достаточно; в противном случае вы, наверное, сочтете ее слишком сложной. Пример:

conn = LDAP::Conn.new("rsads02.foo.com")

conn.bind("CN=username,CN=Users,DC=foo,DC=com", "password") do |bound|

 bound.search("DC=foo,DC=com", LDAP::LDAP_SCOPE_SUBTREE,

  "(&(name=*) (objectCategory=person))", ['name','ipPhone'])

 do |user|

  puts "#{user['name']} #{user['ipPhone']}"

 end

end

Библиотека ActiveLDAP организована по образцу ActiveRecord. Вот пример ее использования, взятый с домашней страницы:

require 'activeldap'

require 'examples/objects/user'

require 'password'

# Установить соединение Ruby/ActiveLDAP и т. д.

ActiveLDAP::Base.connect(:password_block

 => Proc.new { Password.get('Password: ') },

  :allow_anonymous => false)

# Загрузить запись с данными о пользователе

# (ее класс определен в примерах).

wad = User.new('wad')

# Напечатать общее имя.

р wad.cn

# Изменить общее имя.

wad.cn = "Will"

# Сохранить в LDAP.

wad.write

Есть также сравнительно недавняя библиотека, написанная Фрэнсисом Чианфрокка (Francis Cianfrocca), многие предпочитают именно ее:

require 'net/ldap'

ldap = Net::LDAP.new :host => server_ip_address,

 :port => 389,

 :auth => {

  :method => :simple,

  :username => "cn=manager,dc=example,dc=com",

  :password => "opensesame"

 }

filter = Net::LDAP::Filter.eq( "cn", "George*" )

treebase = "dc=example,dc=com"

ldap.search( :base => treebase, :filter => filter ) do |entry|

 puts "DN: #{entry.dn}"

 entry.each do |attribute, values|

  puts " #{attribute}:"

  values.each do |value|

   puts " --->#{value}"

  end

 end

end

p ldap.get_operation_result

Какая из этих библиотек лучше — дело вкуса. Я рекомендую познакомиться со всеми и сформировать собственное мнение.

 

10.4.5. Интерфейс с Oracle

Oracle — одна из наиболее мощных и популярных СУБД в мире. Понятно, что было много попыток реализовать интерфейс с этой базой данных из Ruby. На сегодняшний день лучшей считается библиотека OCI8, которую написал Кубо Такехиро (Kubo Takehiro).

Вопреки названию, библиотека OCI8 работает и с версиями Oracle младше 8. Но она еще не вполне зрелая, поэтому не позволяет воспользоваться некоторыми средствами, появившимися в последних версиях.

API состоит из двух уровней: тонкая обертка (низкоуровневый API, довольно точно повторяющий интерфейс вызовов Oracle — Call Level Interface). Но в большинстве случаев вы будете работать с высокоуровневым API. Не исключено, что в будущем низкоуровневый API станет недокументированным.

Модуль OCI8 включает классы Cursor и Blob. Класс OCIException служит предком всех классов исключений, которые могут возникнуть при работе с базой данных: OCIError, OCIBreak и OCIInvalidHandle.

Чтобы установить соединение с сервером, вызывается метод OCI8.new, которому нужно передать как минимум имя и пароль пользователя. В ответ возвращается описатель, который можно использовать для выполнения запросов. Пример:

require 'oci8'

session = OCI8.new('user', 'password')

query = "SELECT TO_CHAR(SYSDATE, 'YYYY/MM/DD') FROM DUAL"

cursor = session.exec(query)

result = cursor.fetch # В данном случае всего одна итерация.

cursor.close

session.logoff

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

query = 'select * from some_table'

cursor = session.exec(query)

while row = cursor.fetch

 puts row.join(",")

end

cursor.close

# Или с помощью блока:

nrows = session.exec(query) do |row|

 puts row.join(",")

end

Связанные переменные в запросе напоминают символы. Есть несколько способов связать переменные со значениями:

session = OCI8.new("user","password")

query = "select * from people where name = :name"

# Первый способ...

session.exec(query,'John Smith')

# Второй...

cursor = session.parse(query)

cursor.exec('John Smith')

# Третий...

cursor = session.parse(query)

cursor.bind_param(':name','John Smith') # Связывание по имени.

cursor.exec

# И четвертый.

cursor = session.parse(query)

cursor.bind_param(1,'John Smith')       # Связывание по номеру.

cursor.exec

Для тех, кто предпочитает интерфейс DBI, имеется соответствующий адаптер. Дополнительную информацию можно найти в документации по OCI8

 

10.4.6. Обертка вокруг DBI

Теоретически интерфейс DBI обеспечивает доступ к любым базам данных. Иными словами, один и тот же код должен работать и с Oracle, и с MySQL, и с PostgreSQL, и с любой другой СУБД, стоит лишь изменить одну строку, в которой указан нужный адаптер. Иногда эта идеология не срабатывает для сложных операций, специфичных для конкретной СУБД, но для рутинных задач она вполне годится.

Пусть имеется база данных под управлением Oracle и используется драйвер (он же адаптер), поставляемый вместе с библиотекой OCI8. Методу connect следует передать достаточно информации для успешного соединения с базой данных. Все более или менее интуитивно очевидно.

require "dbi"

db = DBI.connect("dbi:OCI8:mydb", "user", "password")

query = "select * from people"

stmt = db.prepare(query)

stmt.execute

while row = stmt.fetch do

 puts row.join(",")

end

stmt.finish

db.disconnect

Здесь метод prepare — это некий вариант компиляции или синтаксического анализа запроса, который позже исполняется. Метод fetch извлекает одну строку из результирующего набора и возвращает nil, если строк не осталось (поэтому мы и воспользовались циклом while). Метод finish можно считать вариантом закрытия или освобождения ресурсов.

Полную информацию обо всех возможностях DBI можно найти в любом справочном руководстве. Список имеющихся драйверов приведен на сайте RubyForge и в архиве приложений Ruby.

 

10.4.7. Объектно-реляционные отображения (ORM)

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

Повсеместная распространенность обеих моделей (РСУБД и ООП) и «несогласованный импеданс» между ними побудил многих людей попытаться перебросить мост. Этот программный мост получил название «объектно-реляционное отображение» (Object-Relational Mapper — ORM).

К этой задаче существуют разные подходы. У каждого есть свои достоинства и недостатки. Ниже мы рассмотрим два популярных ORM: ActiveRecord и Og (последняя аббревиатура обозначает «object graph» — граф объектов).

Библиотека ActiveRecord для Ruby названа в честь предложенного Мартином Фаулером (Martin Fowler) паттерна проектирования «Active Record» (активная запись). Смысл его в том, что таблицам базы данных сопоставляются классы, в результате чего данными становится возможно манипулировать без привлечения SQL. Точнее говоря, «она (активная запись) обертывает строку таблицы или представления, инкапсулирует доступ к базе данных и наделяет данные логикой, присущей предметной области» (см. книгу Martin Fowler «Patterns of Enterprise Application Architecture», Addison Wesley, 2003 [ISBN: 0-321-12742-0e]).

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

require 'active_record'

ActiveRecord::Base.establish_connection(:adapter => "oci8",

 :username => "username",

 :password => "password",

 :database => "mydb",

 :host => "myhost")

class SomeTable < ActiveRecord::Base

 set_table_name "test_table"

 set_primary_key "some_id"

end

SomeTable.find(:all).each do |rec|

 # Обработать запись...

end

item = SomeTable.new

item.id = 1001

item.some_column = "test"

item.save

Библиотека предлагает богатый и сложный API. Я рекомендую ознакомиться со всеми руководствами, которые вы сможете найти в сети или в книгах. Поскольку эта библиотека составляет неотъемлемую часть системы «Ruby on Rails», то мы еще вернемся к ней в главе, посвященной этой теме.

Og отличается от ActiveRecord тем, что в центре внимания последней находится база данных, а первая делает упор на объекты, Og может сгенерировать схему базы данных, имея определения классов на языке Ruby (но не наоборот).

При работе с Og нужен совсем другой стиль мышления; она не так распространена, как ActiveRecord. Но мне кажется, что у этой библиотеки есть свои «изюминки», и ее следует рассматривать как мощный и удобный механизм ORM, особенно если вы проектируете базу данных исходя из структуры имеющихся объектов.

Определяя подлежащий хранению класс, мы пользуемся методом property, который похож на метод attr_accessor, только с ними ассоциирован тип (класс).

class SomeClass

 property :alpha, String

 property :beta, String

 property :gamma, String

end

Поддерживаются также типы данных Integer, Float, Time, Date и пр. Потенциально возможно связать со свойством произвольный объект Ruby.

Соединение с базой данных устанавливается так же, как в случае ActiveRecord или DBI.

db = Og::Database.new(:destroy => false,

 :name => 'mydb',

 :store => :mysql,

 :user => 'hal9000',

 :password => 'chandra')

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

obj = SomeClass.new

obj.alpha = "Poole"

obj.beta = "Whitehead"

obj.gamma = "Kaminski"

obj.save

Имеются также методы для описания связей объекта в терминах классической теории баз данных:

class Dog

 has_one    :house

 belongs_to :owner

 has_many   :fleas

end

Эти, а также другие методы, например many_to_many и refers_to, помогают создавать сложные связи между объектами и таблицами.

Библиотека Og слишком велика, чтобы ее документировать на страницах этой книги. Дополнительную информацию вы можете найти в онлайновых источниках (например, на сайте http://oxyliquit.de).

 

10.5. Заключение

В данной главе был представлен обзор ввода/вывода в Ruby. Мы рассмотрели сам класс IO и его подкласс File, а также связанные с ними классы, в частности Dir и Pathname. Мы познакомились с некоторыми полезными приемами манипулирования объектами IO и файлами.

Также было уделено внимание вопросам хранения данных на более высоком уровне, точнее, на внешних носителях в виде сериализованных объектов. Наконец, мы дали краткий обзор решений, которые Ruby предлагает для интерфейса с настоящими базами данных, а кроме того, познакомились с некоторыми объектно-ориентированными подходами к взаимодействию с реляционными СУБД.

Ниже мы еще вернемся к вводу/выводу в контексте сокетов и сетевого программирования. Но предварительно рассмотрим некоторые другие темы.