По историческим причинам конец строки текста в файле представляется на разных платформах различными символами. В Unix и Linux — это одиночный символ \n, а в Windows — это последовательность из двух символов \r\n. В результате файлы, перемещаемые между Linux и Windows, могут после передачи странно выглядеть в текстовом редакторе — они могут сохранить окончание строки, принятое на исходной платформе.
Например, большинство текстовых редакторов для Windows обрабатывает текст в формате Unix, но Блокнот (Notepad) составляет заметное исключение — текстовые файлы, скопированные из Unix или Linux, обычно выглядят в Блокноте, как одна большая строка со странными символами внутри (\n). Точно так же при копировании файлов из Windows в Unix в двоичном режиме в них сохраняется символ \r (который в текстовых редакторах часто отображается как ~M).
Сценариям на языке Python это обычно безразлично, потому что объекты файлов автоматически отображают последовательность DOS \r\n в одиночный символ \n. При выполнении сценариев в Windows это действует так:
• Для файлов, открытых в текстовом режиме, при чтении \r\n преобразуется в \n.
• Для файлов, открытых в текстовом режиме, при записи \n преобразуется в \r\n.
• Для файлов, открытых в двоичном режиме, никакие преобразования не производятся.
В Unix-подобных системах преобразование не производится в любом режиме, потому что в файлах используется символ \n. Следует запомнить два важных следствия из этих правил. Во-первых, почти всегда во всех сценариях на языке Python символ конца строки представляется одиночным \n, независимо от способа его сохранения во внешних файлах на соответствующей платформе. Путем соответствующего преобразования \n при чтении и записи Python скрывает присущие платформам различия.
Второе следствие из этого преобразования более тонкое: при обработке двоичных файлов использование двоичного режима (например, rb, wb) отключает механизм преобразования символов конца строки. Если выбрать неправильный режим, указанные преобразования вполне могут повредить данные, как при чтении, так и при записи, — случайно оказавшиеся среди двоичных данных байты \r могут быть ошибочно отброшены при чтении или ошибочно добавлены к байтам \n при записи. В итоге двоичные данные окажутся искаженными, что, вероятно, совсем не то, что вам хотелось бы получить при работе с изображениями или аудиоклипами!
В Python 3.X эта проблема ушла на задний план, потому что мы в принципе не можем использовать двоичные данные с файлами, открытыми в текстовом режиме, из-за того, что текстовый режим предполагает автоматическое применение кодировок Юникода к содержимому файлов. Операции чтения и записи просто будут терпеть неудачу, если данные не смогут быть декодированы при чтении или закодированы при записи. Использование двоичного режима позволяет избежать ошибок, связанных с преобразованием Юникода, и автоматически запрещает преобразование символов конца строки как таковое (ошибки, связанные с преобразованием Юникода, можно было бы перехватывать в инструкции try). Итак, стоит запомнить как отдельный факт, что двоичный режим предохраняет двоичные данные от искажения в результате преобразования символов конца строки, особенно если вы работаете только с текстовыми данными ASCII, когда можно смело забыть обо всех проблемах, связанных с Юникодом.
Ниже демонстрируется действие механизма преобразования символов конца строки в Python 3.1 в Windows — объект файла, открытого в текстовом режиме, выполняет преобразование символов конца строки и обеспечивает переносимость наших сценариев:
>>> open(‘temp.txt’, ‘w’).write(‘shrubbery\n’) # запись в текстовом режиме:
10 # \n -> \r\n
>>> open(‘temp.txt’, ‘rb’).read() # чтение двоичных данных:
b’shrubbery\r\n’ # фактические байты из файла
> >> open(‘temp.txt’, ‘r’).read() # проверка чтением: \r\n -> \n
‘shrubbery\n’
При записи в двоичном режиме, напротив, предотвращаются любые преобразования, как и предполагалось, даже когда данные содержат байты, которые в текстовом режиме интерпретировались бы как часть символов конца строки (при выводе строк байтов отдельные байты выводятся как символы ASCII, если они соответствуют печатаемым символам, и как экранированные шестнадцатеричные последовательности в противном случае):
> >> data = b’a\0b\rc\r\nd’ # 4 байта, 4 обычных символа
> >> len(data)
8
> >> open(‘temp.bin’, ‘wb’).write(data) # запись двоичных данных как есть
8
> >> open(‘temp.bin’, ‘rb’).read() # чтение двоичных данных: b’a\x00b\rc\r\nd’ # без преобразования
Но при чтении двоичных данных в текстовом режиме, неважно, случайно или нет, механизм преобразования символов конца строки может повредить данные (предполагая, что вообще при декодировании не возникло ошибок, как с нашими ASCII-данными на платформе Windows):
> >> open(‘temp.bin’, ‘r’).read() # чтение в текстовом режиме: искажены \r! ‘a\x00b\nc\nd’
Тот же эффект может возникнуть при записи двоичных данных в текстовом режиме — могут изменяться или вставляться байты, значения которых совпадают с символами конца строки (и снова имеются в виду данные, успешно прошедшие этап кодирования в кодировку по умолчанию для текущей платформы):
> >> open(‘temp.bin’, ‘w’).write(data) # в текстовом режиме должна
TypeError: must be str, not bytes # передаваться строка типа str
# используйте bytes.decode()
# для преобразования типа
>>> data.decode()
‘a\x00b\rc\r\nd’
>>> open(‘temp.bin’, ‘w’).write(data.decode())
8
>>> open(‘temp.bin’, ‘rb’).read() # запись в текстовом режиме: добавит \r b’a\x00b\rc\r\r\nd’
>>> open(‘temp.bin’, ‘r’).read() # опять искажение, изменит \r
‘a\x00b\nc\n\nd’
Проще говоря, запомните, что во всех текстовых файлах при определении конца строки следует ориентироваться на символ \n, а двоичные файлы всегда должны открываться в двоичном режиме, чтобы предотвратить преобразование символов конца строки и кодирование/ декодирование символов Юникода. Вообще тип содержимого файла определяется режимом его открытия, а режимы открытия определяют способы обработки содержимого, что в точности соответствует нашим желаниям.
Однако следует понимать, что в особых случаях может потребоваться открывать текстовые файлы в двоичном режиме. Так, в примерах из главы 6 мы иногда будем использовать двоичный режим для текстовых файлов, чтобы избежать возможных ошибок декодирования в Юникод при работе с файлами, созданными на разных платформах, где могут использоваться различные кодировки. Данный прием позволяет избежать ошибок кодирования, но при этом некоторые операции с текстом могут выполняться не так, как предполагается, — поиск в таком двоичном тексте не всегда может давать точные результаты, потому что искомый ключ также придется преобразовать в строку байтов в соответствии с некоторой кодировкой, возможно, несовместимой с кодировкой текста в файле.
В примере текстового редактора PyEdit, в главе 11, нам также потребуется перехватывать исключения, вызванные ошибками преобразования Юникода в утилите поиска «grep» по файлам в каталоге, и мы пойдем еще дальше, позволив пользователю определять кодировку символов содержимого файлов для целого дерева каталогов. Кроме того, когда необходимо явно выполнить преобразование символов конца строки в соответствии с соглашениями для двух разных платформ, может потребоваться прочитать текст в двоичном режиме, чтобы сохранить оригинальное представление концов строк, — при открытии в текстовом режиме они могут оказаться преобразованными в \n к моменту, когда данные попадут в сценарий.
Имеется также возможность запретить преобразование символов конца строки в текстовом режиме с помощью дополнительных аргументов функции open, которые мы не будем рассматривать здесь. Подробности ищите в описании аргумента newline в справочной документации по функции open, но, в двух словах: если в этом аргументе передать пустую строку, это предотвратит преобразование символов конца строки и сохранит остальные особенности поведения текстового режима. Теперь обратимся к следующим двум типичным случаям использования двоичных файлов: работа с упакованными двоичными данными и произвольный доступ.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011