Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Произвольный доступ к двоичным данным в файлах В предыдущих разделах рассматривалась методика, когда все данные программы целиком читаются в память, обрабатываются и затем цели ком записываются в файл. В современных компьютерах так много опе ративной памяти, что эта методика имеет полное право на существова ние даже в случае больших объемов данных. Однако в некоторых си туациях более предпочтительной может оказаться методика, когда данные полностью хранятся на диске, в память небольшими порциями читаются только необходимые данные, а на диск записываются только изменения. Подход, основанный на произвольном доступе к данным на Произвольный доступ к двоичным данным в файлах 377 диске, легко реализовать при использовании базы данных типа ключ значение («DBM») или полноценной базы данных SQL – оба варианта будут рассматриваться в главе 11, а в этом разделе будет показано, как вручную реализовать произвольный доступ к данным в файлах. Для начала будет представлен класс BinaryRecordFile.BinaryRecordFile. Экземпляры этого класса являются универсальным представлением двоичных файлов, доступных для чтения и записи, состоящих из по следовательности записей фиксированной длины. Затем, чтобы проде монстрировать, как использовать двоичные файлы с произвольным доступом, будет рассмотрен класс BikeStock.BikeStock, хранящий кол лекцию объектов BikeStock.Bike в виде записей в объекте BinaryRecord File.BinaryRecordFile Универсальный класс BinaryRecordFile Своим прикладным интерфейсом класс BinaryRecordFile.BinaryRecord File напоминает список, так как он обеспечивает возможность получе ния/добавления/удаления записи по заданному номеру позиции. Ко гда запись удаляется, она просто помечается как «удаленная», благо даря этому исчезает необходимость перемещать все последующие за писи, чтобы заполнить промежуток; что также означает, что после удаления все первоначальные индексы остаются допустимыми. Дру гое преимущество такого подхода состоит в том, что запись легко мо жет быть восстановлена, достаточно лишь убрать метку. Однако при таком подходе, удаляя записи, мы не можем экономить дисковое про странство. Эта проблема будет решаться за счет методов «уплотнения» файла, которые будут ликвидировать удаленные записи (и соответст венно будут изменяться номера позиций записей). Прежде чем приступить к рассмотрению реализации, взглянем на ти пичный пример использования: Contact = struct.Struct("<15si") contacts = BinaryRecordFile.BinaryRecordFile(filename, Contact.size) Здесь создается структура (с обратным порядком следования байтов, 15байтовая строка байтов и 4байтовое целое число со знаком), кото рая будет представлять записи. Затем создается экземпляр класса Bi naryRecordFile.BinaryRecordFile , которому передается имя файла и раз мер записи, соответствующий размеру используемой структуры. Если файл уже существует, его содержимое при открытии остается на мес те; в противном случае создается новый файл; и в любом случае файл открывается для чтения/записи в двоичном режиме. contacts[4] = Contact.pack("Abe Baker".encode("utf8"), 762) contacts[5] = Contact.pack("Cindy Dove".encode("utf8"), 987) Мы можем воспринимать файл как список, и использовать оператор доступа к элементам ([]). Здесь выполняется присваивание двух бай 378 Глава 7. Работа с файлами товых строк (объектов bytes, каждый из которых содержит строку и целое число) двум записям, с использованием номеров их позиций в файле. Эти операции присваивания перезапишут прежнее содержи мое, а если файл содержит менее шести записей, будут созданы новые записи, каждая из которых будет заполнена байтами 0x00. contact_data = Contact.unpack(contacts[5]) contact_data[0].decode("utf8").rstrip(chr(0)) # вернет: 'Cindy Dove' Поскольку строка «Cindy Dove» содержит менее 15 символов UTF8, при упаковывании в конец ее будут добавлены байты 0x00. Поэтому при извлечении записи contact_data будет содержать кортеж из двух элементов (b'Cindy Dove\x00\x00\x00\x00\x00', 987). Чтобы получить имя, необходимо декодировать последовательность байтов в кодиров ке UTF8 для получения строки Юникода и потом удалить завершаю щие байты 0x00. Теперь, когда мы мельком увидели класс в действии, можно присту пать к рассмотрению программного кода. Определение класса Binary RecordFile.BinaryRecordFile находится в файле BinaryRecordFile.py. Вслед за обычными предварительными сведениями следуют два част ных определения значений байтов: _DELETED = b"\x01" _OKAY = b"\x02" Каждая запись начинается с байта «состояния», который может иметь одно из двух значений: _DELETED или _OKAY (или b"\x00" в случае пустой записи). Ниже приводятся строка с инструкцией class и программный код ме тода инициализации: class BinaryRecordFile: def __init__(self, filename, record_size, auto_flush=True): self.__record_size = record_size + 1 mode = "w+b" if not os.path.exists(filename) else "r+b" self.__fh = open(filename, mode) self.auto_flush = auto_flush Существует два разных размера записи. Значение BinaryRecordFile.re cord_size определяется пользователем и является размером записи с точки зрения пользователя. Частное значение BinaryRecordFile.__re cord_size – это истинный размер записи, который включает байт со стояния. Мы предотвращаем усечение файла при открытии, если файл сущест вует (используя режим "r+b"), и создаем его, если файл отсутствует (ис пользуя режим "w+b"). Элемент «+» в строке режима указывает, что файл открывается на чтение и запись. Если атрибут BinaryRecordFi le.auto_flush имеет значение True, файл будет выталкиваться на диск перед каждой операцией чтения и после каждой операции записи. Произвольный доступ к двоичным данным в файлах 379 @property def record_size(self): return self.__record_size 1 @property def name(self): return self.__fh.name def flush(self): self.__fh.flush() def close(self): self.__fh.close() Мы оформили размер записи и имя файла как свойства, доступные только для чтения. Размер записи, который сообщается пользовате лю, является тем размером, который был установлен пользователем при создании объекта и соответствует размеру записи пользователя. Методы flush() и close() просто вызывают соответствующие методы объекта файла. def __setitem__(self, index, record): assert isinstance(record, (bytes, bytearray)), \ "binary data required" assert len(record) == self.record_size, ( "record must be exactly {0} bytes".format( self.record_size)) self.__fh.seek(index * self.__record_size) self.__fh.write(_OKAY) self.__fh.write(record) if self.auto_flush: self.__fh.flush() Этот метод обеспечивает поддержку синтаксиса brf[i] = data, brf – это объект класса BinaryRecordFile, i – номер позиции записи и data – стро ка байтов. Обратите внимание, что запись должна иметь тот же раз мер, что был указан при создании объекта BinaryRecordFile. Если аргу менты содержат корректные значения, выполняется перемещение указателя в файле в позицию первого байта записи – обратите внима ние на то, что здесь используется истинный размер записи, то есть раз мер с учетом байта состояния. По умолчанию метод seek() перемещает указатель в файле в абсолютную позицию. С помощью второго аргу мента можно выполнять перемещение относительно текущей позиции или относительно конца файла. (Атрибуты и методы объектов файлов перечислены в табл. 7.4.) Так как производится изменение элемента, вполне очевидно, что он не был удален, поэтому в байт состояния записывается значение _OKAY, а затем записываются двоичные данные пользователя. Объект Binary RecordFile ничего не знает о структуре используемой записи, он беспо коится лишь о том, чтобы записи имели корректный размер. 380 Глава 7. Работа с файлами Метод не проверяет выход индекса за допустимые границы. Если ин декс находится за пределами файла, запись будет записана в коррект ное местоположение, а каждый байт между прежним концом файла и началом новой записи автоматически будет установлен в значение b"\x00" . Такие пустые записи не имеют значения _OKAY или _DELETED в байте состояния, благодаря этому мы сможем их отличать, когда в этом появится необходимость. def __getitem__(self, index): self.__seek_to_index(index) state = self.__fh.read(1) if state != _OKAY: return None return self.__fh.read(self.record_size) Таблица 7.4. Методы и атрибуты объекта файла Синтаксис Описание f.close() Закрывает объект файла f и записывает в атрибут f.closed значение True f.closed Возвращает True, если файл закрыт f.encoding Кодировка, используемая при преобразованиях bytes ↔ str f.fileno() Возвращает дескриптор файла. (Доступно только для объектов файлов, имеющих дескрипторы.) f.flush() Выталкивает выходные буферы объекта f на диск f.isatty() Возвращает True, если объект файла ассоциирован с консолью. (Доступно только для объектов файлов, ссылающихся на фактические файлы.) f.mode Режим, в котором был открыт объект файла f f.name Имя файла (если таковое имеется) f.newlines Виды последовательностей перевода строки, встречаю щиеся в текстовом файле f f.__next__() Возвращает следующую строку из объекта файла f. В большинстве случаев этот метод вызывается неявно, например, for line in f f.peek(n) Возвращает n байтов без перемещения позиции указа теля в файле f.read(count) Читает до count байтов из объекта файла f. Если значе ние count не определено, то читаются все байты, начи ная от текущей позиции и до конца. При чтении в дво ичном режиме возвращает объект bytes, при чтении в текстовом режиме – объект str. Если из ничего не бы ло прочитано (конец файла), возвращается пустой объ ект bytes или str Произвольный доступ к двоичным данным в файлах 381 f.readable() Возвращает True, если f был открыт для чтения f.readinto(ba) Читает до len(ba) байтов в объект ba типа bytearray и возвращает число прочитанных байтов (0, если был достигнут конец файла). (Доступен только в двоичном режиме.) f.readline(count) Читает следующую строку (до count байтов, если значе ние count было определено и число прочитанных байтов было достигнуто раньше, чем встретился символ пере вода строки \n), включая символ перевода строки \n f.readlines(sizehint) Читает все строки до конца файла и возвращает их в ви де списка. Если значение аргумента sizehint определе но, то будет прочитано примерно sizehint байтов, если внутренние механизмы, на которые опирается объект файла, поддерживают такую возможность f.seek(offset, whence) Перемещает позицию указателя в файле (откуда будет начато выполнение следующей операции чтения или за писи) в заданное смещение, если аргумент whence не оп ределен или имеет значение os.SEEK_SET. Перемещает по зицию указателя в файле в заданное смещение (которое может быть отрицательным) относительно текущей по зиции, если аргумент whence имеет значение os.SEEK_CUR, или относительно конца файла, если аргумент whence имеет значение os.SEEK_END. Запись всегда выполняется в конец файла, если был определен режим добавления в конец "a", независимо от местоположения указателя в файле. В текстовом режиме в качестве смещений должны использоваться только значения, возвращае мые методом tell() f.seekable() Возвращает True, если f поддерживает возможность произвольного доступа f.tell() Возвращает текущую позицию указателя в файле отно сительно его начала f.truncate(size) Усекает файл до текущей позиции указателя в файле или до размера size, если аргумент size задан f.writable() Возвращает True, если f был открыт для записи f.write(s) Записывает в файл объект s типа bytes/bytearray, если он был открыт в двоичном режиме, и объект s типа str, если он был открыт в текстовом режиме f.writelines(seq) Записывает в файл последовательность объектов (стро ки – для текстовых файлов, строки байтов – для двоич ных файлов) Синтаксис Описание 382 Глава 7. Работа с файлами При чтении записи могут иметь место четыре ситуации, которые сле дует учитывать: запись не существует, то есть указанный индекс нахо дится за пределами файла; запись пустая; запись была удалена и нор мальная запись. Если запись не существует, частный метод __seek_to_ index() возбудит исключение IndexError. В противном случае он пере местит указатель в файле в позицию первого байта требуемой записи, и мы можем прочитать байт состояния. Если состояние не равно значе нию _OKAY, то запись должна быть либо пустой, либо удаленной, и в этом случае вызывающей программе возвращается значение None, в противном случае возвращается запись. (При попытке чтения пус той или удаленной записи, вместо того чтобы возвращать None, можно было бы возбуждать наше собственное исключение, например, BlankRecordError или DeletedRecordError.) def __seek_to_index(self, index): if self.auto_flush: self.__fh.flush() self.__fh.seek(0, os.SEEK_END) end = self.__fh.tell() offset = index * self.__record_size if offset >= end: raise IndexError("no record at index position {0}".format( index)) self.__fh.seek(offset) Этот частный вспомогательный метод используется некоторыми дру гими методами для перемещения указателя в файле в позицию перво го байта записи с заданным индексом. Сначала метод проверяет, нахо дится ли заданный индекс в пределах файла. Для этого выполняется перемещение указателя в конец файла (смещение 0 относительно кон ца файла), и с помощью метода tell() определяется абсолютная пози ция указателя. Если смещение записи (индекс ×истинный размер за писи) оказывается в конце файла или за его пределами, возбуждается соответствующее исключение. В противном случае выполняется пере мещение указателя в заданную позицию, откуда будет выполняться следующая операция чтения или записи. def __delitem__(self, index): self.__seek_to_index(index) state = self.__fh.read(1) if state != _OKAY: return self.__fh.seek(index * self.__record_size) self.__fh.write(_DELETED) if self.auto_flush: self.__fh.flush() Сначала метод выполняет перемещение в нужную позицию в файле. Если указанный индекс находится в пределах файла (то есть если не было возбуждено исключение IndexError) и запись не пустая и не была Произвольный доступ к двоичным данным в файлах 383 удалена ранее, то выполняется запись значения _DELETED в байт состоя ния записи. def undelete(self, index): self.__seek_to_index(index) state = self.__fh.read(1) if state == _DELETED: self.__fh.seek(index * self.__record_size) self.__fh.write(_OKAY) if self.auto_flush: self.__fh.flush() return True return False Сначала метод отыскивает требуемую запись и читает байт состояния. Если запись была удалена, в байт состояния записывается значение _OKAY и вызывающей программе возвращается значение True как свиде тельство успешного выполнения операции; в противном случае (для пустой или не удалявшейся ранее записи) возвращается значение False def __len__(self): if self.auto_flush: self.__fh.flush() self.__fh.seek(0, os.SEEK_END) end = self.__fh.tell() return end // self.__record_size Этот метод возвращает количество записей в двоичном файле. Это чис ло определяется путем деления позиции последнего байта в файле (то есть количества байтов в файле) на истинный размер записи. На этом мы закончили рассмотрение основных функциональных возможностей класса BinaryRecordFile.BinaryRecordFile. Остался по следний вопрос, который необходимо рассмотреть: уплотнение фай ла с целью убрать пустые и удаленные записи. Фактически имеется два способа решения этой задачи. Первый способ состоит в том, чтобы перезаписать пустые или удаленные записи записями с большими зна чениями индексов и усечь файл с конца, если в нем имелись пустые или удаленные записи. Этот способ реализован в методе inplace_com pact() . Другой способ состоит в том, чтобы скопировать непустые и не удаленные записи во временный файл и затем переименовать его, дав имя оригинального файла. Использование временного файла удобно, в частности для создания резервных копий. Этот способ реализован в методе compact(). Начнем рассмотрение с метода inplace_compact(), разделив его на две части: def inplace_compact(self): index = 0 length = len(self) 384 Глава 7. Работа с файлами while index < length: self.__seek_to_index(index) state = self.__fh.read(1) if state != _OKAY: for next in range(index + 1, length): self.__seek_to_index(next) state = self.__fh.read(1) if state == _OKAY: self[index] = self[next] del self[next] break else: break index += 1 В методе выполняются итерации по всем записям и для каждой опре деляется ее состояние. Если обнаруживается пустая или удаленная за пись, выполняется поиск следующей непустой и неудаленной записи. Если такая запись обнаруживается, производится замещение пустой или удаленной записи непустой и неудаленной записью и выполняет ся удаление оригинальной записи; в противном случае цикл while пре рывается, так как были просмотрены все непустые и неудаленные записи. self.__seek_to_index(0) state = self.__fh.read(1) if state != _OKAY: self.__fh.truncate(0) else: limit = None for index in range(len(self) 1, 0, 1): self.__seek_to_index(index) state = self.__fh.read(1) if state != _OKAY: limit = index else: break if limit is not None: self.__fh.truncate(limit * self.__record_size) self.__fh.flush() Если первая запись пустая или удаленная, то все они должны быть пустыми или удаленными, так как предыдущий фрагмент кода пере местил все непустые и неудаленные записи в начало файла, оставив пустые и удаленные записи в конце. В этом случае можно просто усечь размер файла до нуля. Если имеется хотя бы одна непустая и неудаленная запись, метод вы полняет итерации в обратном порядке, от конца файла, поскольку из вестно, что все пустые и удаленные записи были перемещены в конец. Переменная limit получает в качестве значения индекс самой первой |