• 10.1. Файлы и каталоги
  • 10.1.1. Открытие и закрытие файлов
  • 10.1.2. Обновление файла
  • 10.1.3. Дописывание в конец файла
  • 10.1.4. Прямой доступ к файлу
  • 10.1.5. Работа с двоичными файлами
  • 10.1.6. Блокировка файлов
  • 10.1.7. Простой ввод/вывод
  • 10.1.8. Буферизованный и небуферизованный ввод/вывод
  • 10.1.9. Манипулирование правами владения и разрешениями на доступ к файлу
  • 10.1.10. Получение и установка временных штампов
  • 10.1.11. Проверка существования и получение размера файла
  • 10.1.12. Опрос специальных свойств файла
  • 10.1.13. Каналы
  • 10.1.14. Специальные операции ввода/вывода
  • 10.1.15. Неблокирующий ввод/вывод
  • 10.1.16. Применение метода readpartial
  • 10.1.17. Манипулирование путевыми именами
  • 10.1.18. Класс Pathname
  • 10.1.19. Манипулирование файлами на уровне команд
  • 10.1.20. Ввод символов с клавиатуры
  • 10.1.21. Чтение всего файла в память
  • 10.1.22. Построчное чтение из файла
  • 10.1.23. Побайтное чтение из файла
  • 10.1.24. Работа со строкой как с файлом
  • 10.1.25. Чтение данных, встроенных в текст программы
  • 10.1.26. Чтение исходного текста программы
  • 10.1.27. Работа с временными файлами
  • 10.1.28. Получение и изменение текущего каталога
  • 10.1.29. Изменение текущего корня
  • 10.1.30. Обход каталога
  • 10.1.31. Получение содержимого каталога
  • 10.1.32. Создание цепочки каталогов
  • 10.1.33. Рекурсивное удаление каталога
  • 10.1.34. Поиск файлов и каталогов
  • 10.2. Доступ к данным более высокого уровня
  • 10.2.1. Простой маршалинг
  • 10.2.2. Более сложный маршалинг
  • 10.2.3. Ограниченное «глубокое копирование» в ходе маршалинга
  • 10.2.4. Обеспечение устойчивости объектов с помощью библиотеки PStore
  • 10.2.5. Работа с данными в формате CSV
  • 10.2.6. Маршалинг в формате YAML
  • 10.2.7. Преобладающие объекты и библиотека Madeleine
  • 10.2.8. Библиотека DBM
  • 10.3. Библиотека KirbyBase
  • 10.4. Подключение к внешним базам данных
  • 10.4.1. Интерфейс с SQLite
  • 10.4.2. Интерфейс с MySQL
  • 10.4.3. Интерфейс с PostgreSQL
  • 10.4.4. Интерфейс с LDAP
  • 10.4.5. Интерфейс с Oracle
  • 10.4.6. Обертка вокруг DBI
  • 10.4.7. Объектно-реляционные отображения (ORM)
  • 10.5. Заключение
  • Глава 10. Ввод/вывод и хранение данных

    На чистом диске можно искать бесконечно.

    (Томас Б. Стил младший)

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

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

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

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

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

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

    В основе системы ввода/вывода в Ruby лежит класс

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

    В модуле

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

    Наконец, методы ввода/вывода есть и в модуле

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

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

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

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

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

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

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

    Класс

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

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

    Метод класса

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

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

    "r"
    , то есть только чтение. Ниже показано, как открывать файлы для чтения и записи.

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

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

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

    File::CREAT
    (создать файл, если он еще не существует) и
    File::RDONLY
    (открыть только для чтения). Такая форма используется редко.

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

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

    close
    :

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

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

    out.close

    Имеется также метод

    open
    . В простейшей форме это синоним
    new
    :

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

    Но методу

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

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

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

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

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

    end

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

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

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

    Чтобы открыть файл для чтения и записи, достаточно добавить знак плюс (

    +
    ) в строку указания режима (см. раздел 10.1.1):

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

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


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

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

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

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

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

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

    "а"
    (см. раздел 10.1.1):

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

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

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

    logfile.close

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

    Для чтения из файла в произвольном порядке, а не последовательно, можно воспользоваться методом

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

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

    file = File.new("myfile")

    file.seek(5)

    str = file.gets # "fghi"

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

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

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

    file = File.new("fixedlines")

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

    Для выполнения относительного поиска воспользуйтесь вторым параметром. Константа

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

    file = File.new("somefile")

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

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

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

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

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

    Есть еще и третья константа

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

    Метод

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

    file.seek(20)

    pos1 = file.tell # 20

    file.seek(50, IO::SEEK_CUR)

    pos2 = file.pos  # 70

    Метод

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

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

    +
    в начале строки указания режима (см. раздел 10.1.2).

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

    Когда-то давно программисты на языке С включали в строку указания режима символ

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

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

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

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

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


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

    str = nil

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

    puts str.size # 11


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

    str = nil

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

    puts str.size # 5

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

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

    file = File.open("data")

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

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

    file.close


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

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

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

    file.close

    Отметим, что упомянутый в коде метод

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

    file = File.open("data")

    file.binmode

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

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

    file.close

    При необходимости выполнить низкоуровневый ввод/вывод можете воспользоваться методами

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

    input = File.new("infile")

    output = File.new("outfile")

    instr = input.sysread(10);

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

    Отметим, что метод

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

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

    pack
    из класса
    Array
    и метод
    unpack
    из класса
    String
    .

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

    В тех операционных системах, которые поддерживают такую возможность, метод

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

    file = File.new("somefile")

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

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

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


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

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

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


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

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

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

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

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

    Вы уже знакомы с некоторыми методами ввода/вывода из модуля

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

    Но есть и другие методы, которые следует упомянуть для полноты. Метод

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

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

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

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

    STDIN
    ,
    STDOUT
    и
    STDERR
    . Все они имеют тип
    IO
    .

    Имеется также глобальная переменная

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

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

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

    $stdout = diskfile

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

    diskfile.close

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

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

    Помимо метода

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

    Откуда мы получаем ввод? Есть переменная

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

    Еще имеется интересный глобальный объект

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

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

    puts ARGF.read

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

    while ! ARGF.eof?

     puts ARGF.readline

    end

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

    При чтении из стандартного ввода (

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

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

    str1 = STDIN.gets

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

    str2 = ARGF.gets

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

    str3 = STDIN.gets

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

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

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

    sleep 10

    print "Пока!\n"

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

    sleep
    . При этом первое сообщение не завершается символом новой строки.

    Это можно исправить, вызвав метод

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

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

    STDOUT.flush

    sleep 10

    print "Пока!\n"

    Буферизацию можно отключить (или включить) методом

    sync=
    , а метод
    sync
    позволяет узнать текущее состояние.

    buf_flag = $defout.sync # true

    STDOUT.sync = false

    buf_flag = STDOUT.sync  # false

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

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

    ch = mystream.getc # ?А

    mystream.ungetc(?C)

    ch = mystream.getc # ?C

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

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

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

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

    Для определения владельца и группы файла (это целые числа) класс

    File::Stat
    предоставляет методы экземпляра
    uid
    и
    gid
    :

    data = File.stat("somefile")

    owner_id = data.uid

    group_id = data.gid

    В классе

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

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

    В классе

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

    uid = 201

    gid = 10

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

    f1 = File.new("delta")

    f1.chown(uid, gid)

    f2 = File.new("gamma")

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

    Разрешения можно изменить с помощью метода

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

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

    f = File.new("eta")

    f.chmod(0444)

    Процесс всегда работает от имени какого-то пользователя (возможно,

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

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

    rflag = info.readable?

    wflag = info.writable?

    xflag = info.executable?

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

    readable_real?
    ,
    writable_real?
    и
    executable_real?
    .

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

    rflag2 = info.readable_real?

    wflag2 = info.writable_real?

    xflag2 = info.executable_real?

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

    File::Stat
    для этого есть методы
    owned?
    и
    grpowned?
    .

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

    FileTest
    :

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

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

    # writable_real? executable_real? owned? grpowned?

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

    Маска

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

    Получить или установить маску можно с помощью метода

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

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

    current_umask = File.umask # 0237

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

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

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

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

    Методы

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

    t1 = File.mtime("somefile")

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

    t2 = File.atime("somefile")

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

    t3 = File.ctime("somefile")

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

    Если файл, представленный экземпляром

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

    myfile = File.new("somefile")

    t1 = myfile.mtime

    t2 = myfile.atime

    t3 = myfile.ctime

    А если имеется экземпляр класса

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

    myfile = File.new("somefile")

    info = myfile.stat

    t1 = info.mtime

    t2 = info.atime

    t3 = info.ctime

    Отметим, что объект

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

    Для изменения времени доступа и модификации применяется метод

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

    today = Time.now

    yesterday = today - 86400

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

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

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

    mtime = File.mtime("delta")

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

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

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

    exist?
    из модуля
    FileTest
    :

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

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

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

    Понятно, что такой метод не может быть методом экземпляра

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

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

    Если нас интересует только, пуст ли файл, то в классе

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

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

    Метод

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

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

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

    else

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

    end

    Методы

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

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

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

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

    size?
    возвращает длину. Но если мы применяем его не в качестве предиката, то значение
    nil
    только путает.

    В классе

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

    size1 = File.size("file1")

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

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

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

    info = File.stat("somefile")

    total_bytes = info.blocks * info.blksize

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

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

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

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

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

    blockdev?
    и
    chardev?
    из модуля
    FileTest
    проверяют тип устройства:

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

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

    Иногда нужно знать, ассоциирован ли данный поток с терминалом. Метод

    tty?
    класса
    IO
    (синоним
    isatty
    ) дает ответ на этот вопрос:

    flag1 = STDIN.tty?                  # true

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

    Поток может быть связан с каналом (pipe) или сокетом. В модуле

    FileTest
    есть методы для опроса этих условий:

    flag1 = FileTest::pipe?(myfile)

    flag2 = FileTest::socket?(myfile)

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

    FileTest
    :

    file1 = File.new("/tmp")

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

    test1 = file1.directory? # true

    test2 = file1.file?      # false

    test3 = file2.directory? # false

    test4 = file2.file?      # true

    В классе

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

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

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

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

    FileTest
    .

    file = File.new("somefile")

    info = file.stat

    sticky_flag = info.sticky?

    setgid_flag = info.setgid?

    setuid_flag = info.setuid?

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

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

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

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

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

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

    symlink
    из класса
    File
    для создания символической ссылки.

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

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

    file = File.new("diskfile")

    info = file.stat

    device = info.dev

    devtype = info.rdev

    inode = info.ino

    10.1.13. Каналы

    Ruby поддерживает разные способы читать из канала и писать в него. Метод класса

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

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

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

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

    check.close_write

    list = check.readlines

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

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

    Отметим, что вызов

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

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

    quote = pipe.gets

    puts quote

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

    end

    Если задана строка

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

    IO.popen("-")

    do |mypipe|

     if mypipe

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

      listen = mypipe.gets

      puts listen

     else

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

     end

    end


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

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

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

    Метод

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

    pipe = IO.pipe

    reader = pipe[0]

    writer = pipe[1]


    str = nil

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

     # writer.close_write

     str = reader.gets

     reader.close

    end


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

     # reader.close_read

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

     writer.close

    end


    thread1.join

    thread2.join


    puts str # What hath God wrought?

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

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

    Метод

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

    Метод

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

    Метод

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

    Метод

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

    Метод

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

    desc = $stderr.fileno # 2

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

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

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

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

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

    require 'io/nonblock'


    # ...


    test = mysock.nonblock? # false


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

    # ...

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


    mysock.nonblock { some_operation(mysock) }

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


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

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

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

    Метод

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

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

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

    Метод

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

    Таким образом, если в потоке есть данные, то

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

    Если в потоке нет данных, но при этом достигнут конец файла, то

    readpartial
    немедленно возбуждает исключение
    EOFError
    .

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

    EOFError
    .

    При вызове метода

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

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

    Основными методами для работы с путевыми именами являются методы класса

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

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

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

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

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

    Хотя это методы класса

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

    Упомянем также метод

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

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

    Метод класса

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

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

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

    Если передать методу

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

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

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

    Константа

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

    Метод класса

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

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

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

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

    Не думайте, что методы

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

    10.1.18. Класс Pathname

    Следует знать о существовании стандартной библиотеки

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

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

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

    p2 = path + file

    path.directory?     # true

    path.file?          # false

    p2.directory?       # false

    p2.file?            # true


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

    ext = path2.extname # .txt

    Как и следовало ожидать, имеется ряд вспомогательных методов. Метод

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

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

    p1.root?                # true

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

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

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

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

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

    Как и следовало ожидать, методы

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

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

    p1.absolute? # true

    p1.relative? # false

    Многие методы, например

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

    Дополнительную информацию о классе

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

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

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

    Многие из этих операций реализованы встроенными методами, некоторые находятся в модуле

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

    Для удаления файла служит метод

    File.delete
    или его синоним
    File.unlink
    :

    File.delete("history")

    File.unlink("toast")

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

    File.rename
    :

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

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

    File.link
    и
    File.symlink
    соответственно:

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

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

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

    truncate
    :

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

    Два файла можно сравнить с помощью метода

    compare_file
    . У него есть синонимы
    cmp
    и
    compare_stream
    :

    require "fileutils"


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

    Метод

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

    require "fileutils"


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

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

    Файл можно перемещать методом

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

    require "fileutils"

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

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

    Метод

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

    require "fileutils"


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

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

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

    Наконец, метод

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

    require "fileutils"


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

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

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

    Дополнительную информацию о модуле

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

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

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

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

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

    def getchar

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

     char = STDIN.getc

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

     char

    end

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

    Win32API
    .

    require 'Win32API'


    def getchar

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

    end

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

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

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

    IO.readlines
    : откроет файл, прочитает и закроет.

    arr = IO.readlines("myfile")

    lines = arr.size

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


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

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

    Можно также воспользоваться методом

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

    str = IO.read("myfile")

    bytes = arr.size

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


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

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

    Поскольку класс

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

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

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

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

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

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

     puts line if line =~ /target/

    end


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

    file = File.new("somefile")

    file.each do |line|

     puts line if line =~ /target/

    end

    Отметим, что

    each_line
    — синоним
    each
    .

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

    Для чтения из файла по одному байту служит метод экземпляра

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

    file = File.new("myfile")

    e_count = 0

    file.each_byte do |byte|

     e_count += 1 if byte == ?e

    end

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

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

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

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

    source.each do |line|

     puts line

    end

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

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

    stringio
    .

    Интерфейс класса

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

    require 'stringio'


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

    ios.seek(5)

    ios.puts("xyz")


    puts ios.tell        # 8


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


    с = ios.getc

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


    ios.ungetc(?w)


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


    puts "Ptr = #{ios.tell}"


    s1 = ios.gets        # "wjkl"

    s2 = ios.gets        # "ABC"

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

    Когда подростком вы учили язык BASIC, копируя программы из журналов, то, наверное, для удобства часто пользовались предложением

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

    При желании то же самое можно сделать и в Ruby. Директива

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

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

    DATA.each_line do |line|

     puts line.reverse

    end

    __END__

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

    Madam, I'm Adam.

    ,siht daer nac uoy fI

    .drah oot gnikrow neeb ev'uoy

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

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

    Глобальная константа

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

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

    DATA.rewind

    num = 1

    DATA.each_line do |line|

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

     num += 1

    end

    __END__

    Отметим, что наличие директивы

    __END__
    обязательно — без нее к константе
    DATA
    вообще нельзя обратиться.

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

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

    Все эти проблемы решает библиотека

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

    Возвращаемый объект

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

    У метода

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

    require "tempfile"


    temp = Tempfile.new("stuff")

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

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

    temp.close


    # Позже...

    temp.open

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

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

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

    Получить имя текущего каталога можно с помощью метода

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

    Для изменения текущего каталога служит метод

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

    Dir.chdir("/var/tmp")

    puts Dir.pwd   # "/var/tmp"

    puts Dir.getwd # "/var/tmp"

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

    Dir.chdir("/home")

    Dir.chdir("/tmp") do

     puts Dir.pwd # /tmp

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

    end

    puts Dir.pwd  # /home

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

    В большинстве систем UNIX можно изменить «представление» процесса о том, что такое корневой каталог

    /
    . Обычно это делается из соображений безопасности перед запуском небезопасной или непротестированной программы. Метод
    chroot
    делает указанный каталог новым корнем:

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

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

    puts Dir.pwd # "/tmp"

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

    Метод класса

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

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


    dir = Dir.new("/tmp")

    dir.each { |entry| puts entry }

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

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

    Метод класса

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

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

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

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

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

    mkdir -p
    .

    В программе на Ruby такую операцию выполняет метод

    FileUtils.makedirs
    (из библиотеки
    fileutils
    ):

    require "fileutils"

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

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

    В UNIX команда

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

    В последних версиях Ruby в класс

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

    require 'pathname'

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

    dir.rmtree


    # или:


    require 'fileutils'

    FileUtils.rm_r("/home/poole")

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

    Ниже мы воспользовались стандартной библиотекой

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

    require "find"


    def findfiles(dir, name)

     list = []

     Find.find(dir) do |path|

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

      case name

       when String

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

       when Regexp

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

       else

        raise ArgumentError

      end

     end

     list

    end


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

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


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

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

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

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

    Часто возникает необходимость хранить и извлекать данные более прозрачным способом. Модуль

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

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

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

    Marshal
    позволяет сериализовать и десериализовать объекты.

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

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

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

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

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

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

     Marshal.dump(works,file)

    end


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

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

     works = Marshal.load(file)

    end

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

    IO
    ,
    Proc
    и
    Binding
    . Нельзя также сериализовать синглетные объекты, анонимные классы и модули.

    Метод

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

    s = Marshal.dump(works)

    p s[0] # 4

    p s[1] # 8

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

    verbose
    или
    v
    ), то версии должны совпадать точно. Эти номера версий не связаны с номерами версий Ruby.

    Третий параметр

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

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

     arr = []

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

                              # (ArgumentError)

     Marshal.dump(arr,file,1)


     arr = [1, 2, 3]

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

                              # (ArgumentError)

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


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

                              # (ArgumentError)

     Marshal.dump(arr,file,3)

    end


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

     p Marshal.load(file) # [ ]

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

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

    end

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

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

    Иногда мы хотим настроить маршалинг под свои нужды. Такую возможность дают методы

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

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

    class Person


     attr_reader :name

     attr_reader :age

     attr_reader :balance


     def initialize(name,birthdate,beginning)

      @name = name

      @birthdate = birthdate

      @beginning = beginning

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

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

     end


     def marshal_dump

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

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

      str

     end


     def marshal_load(str)

      self.instance_eval do

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

      end

     end


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

    end


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

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


    str = Marshal.dump(p1)

    p2 = Marshal.load(str)


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

    При сохранении объекта этого типа атрибуты

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

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

    В Ruby нет операции «глубокого копирования». Методы

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

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

    Marshal
    со всеми присущими ему недостатками:

    def deep_copy(obj)

     Marshal.load(Marshal.dump(obj))

    end


    a = deep_copy(b)

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

    Библиотека

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

    require "pstore"


    # Сохранить.

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

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

                     "salary" => 48000 }

    end


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

    require "pstore"

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

    emp = nil

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

    Обычно внутри блока транзакции используется переданный ему объект

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

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

    Мы можем завершить транзакцию досрочно, вызвав метод

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

    require "pstore"


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

    store = PStore.new("objects")

    store.transaction do |s|


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


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

     # a, h и т. д.


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

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


     case condition

      when 1

       puts "Отмена."

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

      when 2

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

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

      when 3

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

     end

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

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


    end

    Внутри транзакции можно вызвать метод

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

    store.transaction do |s|

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

     if s.root?("my_tree")

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

     else

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

     end

     s.delete("my_hash")

     list2 = s.roots # ["my_array"]

    end

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

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

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

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

    Модуль CSV (

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

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

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

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

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

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

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

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

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

    require 'csv'


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

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

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

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

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

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

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

    end

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

    data.csv
    :

    "name","age","salary"

    "mark",29,34500

    "joe",42,32000

    "fred",22,22000

    "jake",25,24000

    "don",32,52000

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

    require 'csv'


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

     p row

    end


    # Выводится:

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

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

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

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

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

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

    Этот фрагмент можно было бы записать и без блока, тогда метод

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

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

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

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

    Затребовав директивой

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

    require 'yaml'


    str = "Hello, world"

    num = 237

    arr = %w[ Jan Feb Mar Apr ]

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


    puts str.to_yaml

    puts num.to_yaml

    puts arr.to_yaml

    puts hsh.to_yaml


    # Выводится:

    # --- "Hello, world"

    # --- 237

    # ---

    # - Jan

    # - Feb

    # - Mar

    # - Apr

    # ---

    # just a: hash.

    # This: is

    Обратным по отношению к

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

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

    data.yaml
    :

    ---

    - "Hello, world"

    - 237

    -

      - Jan

      - Feb

      - Mar

      - Apr

    -

     just a: hash.

     This: is

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

    require 'yaml'

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

    array = YAML.load(file)

    file.close

    p array

    # Выводится:

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

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

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

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

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

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

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

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

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

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

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

    Madeleine предлагает два основных метода доступа к системе объектов. Метод

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

    Оба метода принимают в качестве параметра объект

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

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

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

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

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

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

    require 'dbm'


    d = DBM.new("data")

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

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

    d.close


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


    e = DBM.open("data")

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

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

    e.close


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

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

    Интерфейс к DBM реализован в виде одного класса, к которому подмешан модуль

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

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

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

                           #  Try again - "data.dbm"

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

    dbm
    .

    Метод

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

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

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

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

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

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

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

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

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

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

    Чтобы создать таблицу, вызывается метод

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

    require 'kirbybase'


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


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

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

             :author, :String)

    В текущей версии KirbyBase распознает следующие типы полей:

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

    Для вставки записи в таблицу применяется метод

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

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

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

                 :author => "Alexis de Tocqueville")

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

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

    books.insert(book)

    В любом случае метод

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

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

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


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

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

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

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

    Symbol
    добавлен метод, соответствующий унарному минусу.)

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

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

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

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

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

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

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

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


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

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

    В KirbyBase есть ограниченные средства печати отчетов; достаточно вызвать метод

    to_report
    для любого результирующего набора. Пример:

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

    puts rpt

    # Выводится:

    # recno | title                | author

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

    #     2 | Democracy in America | Alexis de Tocqueville

    #     1 | The Case for Mars    | Robert Zubrin

    #     3 | The Ruby Way         | Hal Fulton

    Атрибут таблицы

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

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

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

    require 'kirbybase'

    require 'drb'

    host = 'localhost'

    port = 44444

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


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

    DRb.thread.join

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

    :client
    вместо обычного
    :local
    .

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

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

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

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

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

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

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

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

    Привязка Ruby к SQLite довольно прямолинейна. API, написанный на С, обернут в класс

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

    В большинстве ситуаций вам будет достаточно класса

    SQLite::Database
    . Вот пример кода:

    require 'sqlite'


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


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

     p row

    end


    db.close


    # Выводится:

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

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

    # ...

    Если блок не задан, то метод

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

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

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

    rs.close

    Если получен объект

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

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

    while ! rs.eof?

     rec = rs.next

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

    end

    rs.close

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

    SQLite::Exception
    , так что легко перехватываются поодиночке или целой группой.

    Отметим еще, что библиотека написана так, что может работать совместно с библиотекой

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

    Хотя библиотека

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

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

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

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

    require 'mysql'


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

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

    r.each_hash do |f|

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

    end


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


    # John Doe - 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. Интерфейс с Oracle

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

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

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

    Модуль OCI8 включает классы

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

    Чтобы установить соединение с сервером, вызывается метод

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

    require 'oci8'


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


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

    cursor = session.exec(query)

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

    cursor.close

    session.logoff

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

    fetch
    . Конечно, можно выбрать и несколько строк:

    query = 'select * from some_table'

    cursor = session.exec(query)

    while row = cursor.fetch

     puts row.join(",")

    end

    cursor.close

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


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

     puts row.join(",")

    end

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

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

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


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

    session.exec(query,'John Smith')


    # Второй...

    cursor = session.parse(query)

    cursor.exec('John Smith')


    # Третий...

    cursor = session.parse(query)

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

    cursor.exec


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

    cursor = session.parse(query)

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

    cursor.exec

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

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

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

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

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

    require "dbi"


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

    query = "select * from people"


    stmt = db.prepare(query)

    stmt.execute


    while row = stmt.fetch do

     puts row.join(",")

    end


    stmt.finish

    db.disconnect

    Здесь метод

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

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

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

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

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

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

    ActiveRecord
    и
    Og
    (последняя аббревиатура обозначает «object graph» — граф объектов).

    Библиотека

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

    Каждая таблица описывается классом, производным от

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

    require 'active_record'


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

     :username => "username",

     :password => "password",

     :database => "mydb",

     :host => "myhost")


    class SomeTable < ActiveRecord::Base

     set_table_name "test_table"

     set_primary_key "some_id"

    end


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

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

    end


    item = SomeTable.new

    item.id = 1001

    item.some_column = "test"

    item.save

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

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

    При работе с

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

    Определяя подлежащий хранению класс, мы пользуемся методом

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

    class SomeClass

     property :alpha, String

     property :beta, String

     property :gamma, String

    end

    Поддерживаются также типы данных

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

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

    ActiveRecord
    или
    DBI
    .

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

     :name => 'mydb',

     :store => :mysql,

     :user => 'hal9000',

     :password => 'chandra')

    У каждого объекта есть метод

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

    obj = SomeClass.new

    obj.alpha = "Poole"

    obj.beta = "Whitehead"

    obj.gamma = "Kaminski"

    obj.save

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

    class Dog

     has_one    :house

     belongs_to :owner

     has_many   :fleas

    end

    Эти, а также другие методы, например

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

    Библиотека

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

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

    В данной главе был представлен обзор ввода/вывода в Ruby. Мы рассмотрели сам класс

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

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

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









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх