|
||||
|
Глава 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, а также 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. Работа с данными в формате CSVCSV (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. Библиотека DBMDBM— платформенно-независимый механизм для хранения строк в файле, как в хэше. И ключ, и ассоциированные с ним данные должны быть строками. Интерфейс 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. Библиотека KirbyBaseKirbyBase — небольшая библиотека, с которой должен освоиться каждый программист на 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. Интерфейс с SQLiteSQLite — популярная база данных для тех, кто ценит программное обеспечение, которое не нужно конфигурировать. Это небольшая автономная исполняемая программа, написанная на языке С, которая хранит всю базу данных в одном файле. Хотя обычно она используется для небольших баз, но теоретически способна управиться с терабайтными объемами. Привязка 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 - jdoe@rubynewbie.com # Fred Smith - smithf@rubyexpert.com Особенно полезны методы класса 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. Интерфейс с OracleOracle — одна из наиболее мощных и популярных СУБД в мире. Понятно, что было много попыток реализовать интерфейс с этой базой данных из 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 предлагает для интерфейса с настоящими базами данных, а кроме того, познакомились с некоторыми объектно-ориентированными подходами к взаимодействию с реляционными СУБД. Ниже мы еще вернемся к вводу/выводу в контексте сокетов и сетевого программирования. Но предварительно рассмотрим некоторые другие темы. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|