Текстовые форматы файлов

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

Как посмотреть содержимое файла?

Вы знакомы как минимум с командами cat и less, которые предназначены для просмотра содержимого файла. Но проблема в том, что терминал умеет отображать содержимое далеко не всякого файла. Наиболее удобным средством просмотра содержимого файла произвольной природы является шестнадцатиричный дамп. Его можно получить, например, командами hexdump, hd и xxd (наиболее красивый дамп получается командой hd, которая является синонимом hexdump с правильными опциями).

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

00000000  61 62 63 64 0a                                    |abcd.|
00000005

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

Стандарт ASCII

Стандарт ASCII устанавливает соответствие между числами от 0 до 127 и сигналами, которые мог подавать/воспринимать типичный телетайп 60-х годов. Большая часть таблицы ASCII соответствует цифрам, буквам и знакам препинания, которые можно набрать с клавиатуры. Первые 32 значения соответствуют управляющим командам (например, по команде номер 10, поданной на телетайп нажатием кнопки или же сигналом с компьютера, он отматывал рулон бумаги на одну строчку; а по команде номер 7 звенел колокольчиком).

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

  • клавиша Shift уменьшает значение кода нажатой кнопки на 32
  • клавиша Ctrl обнуляет 6-й и 7-й разряды кода нажатой кнопки (то есть убирает из двоичного представления слагаемые 64 и 32)

Конечно, современные ОС всего лишь пытаются эмулировать такое поведение: клавиатура передаёт компьютеру информацию о всех нажатых клавишах, а уже ОС решает, что с этим делать.

Поскольку стандарт ASCII позволяет кодировать те символы, из которых состоит то, что люди называют словом «текст», файлы, содержимое которых предназначено для интерпретации в соответствии со стандартом ASCII, называются текстовыми файлами (в кодировке ASCII).

Кодировка русского языка

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

  • кодировка CP866 (также встречается под названием DOS)
  • кодировка CP1251 (также встречается под названием WINDOWS)
  • кодировка KOI8-R
  • кодировка UTF-8

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

Кодировки CP866 и CP1251 весьма похожи друг на друга: как и в ASCII, в этих кодировках буквы русского алфавита идут подряд друг за другом в алфавитном порядке, а заглавные отличаются от строчных на 32. Единственная проблема: в русском языке 33 буквы, что несовместимо со свойствами, описанными в предыдущем предложении. Козлом отпущения была выбрана буква «ё». Что в CP866, что в CP1251 эта буква, во-первых, находится вне алфавита, а во-вторых — заглавная отличается от строчной не на 32.

Кодировка KOI8-R гораздо интереснее: она была придумана в те времена, когда часть сетевого оборудования и программного обеспечения, предназначенных для передачи текста, использовала старший бит каждого байта в служебных целях. Основное отличительное свойство KOI8-R — текст сохранял читаемость при внесении произвольных изменений в старший бит его байт. Например, если обнулить каждый старший бит в KOI8-R-представлении текста

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

получится

s_E[X VE E]# \TIH MQGKIH FRANCUZSKIH BULOK, DA WYPEJ ^A@!

А вот кодировка UTF-8 уже не является однобайтовой: в ней каждый символ может кодироваться последовательностью байт длины от 1 до 6, причём само понятие «символа» понимается существенно более широко, чем в любой из вышеупомянутых кодировок.

Юникод

Юникод — современный стандарт, определяющий понятия «символ» и «текст», а также — способы их представления.

С этим стандартом связано некоторое количество аббревиатур, которые полезно уметь различать:

  • UCS — «алфавит» юникода; таблица номеров всевозможных символов
  • UCS-2 — кодировка постоянной ширины (каждый символ кодируется 2 байтами), предшественник UTF-16
  • UCS-4 — то же, что и UTF-32
  • UTF-8 — кодировка переменной ширины (каждый символ кодируется количеством байт от 1 до 6), расширение ASCII
  • UTF-16 — кодировка переменной ширины (каждый символ кодируется двумя или четырьмя байтами)
  • UTF-32 — кодировка постоянной ширины (каждый символ кодируется четырьмя байтами)

Наиболее просто устроена кодировка UTF-32. Каждый символ в ней кодируется просто его номером в таблице UCS. Есть единственная проблема: на самом деле, кодировок UTF-32 не одна, а две. Различаются они тем, в каком порядке перечисляются байты каждого из номеров: от младшего к старшему, или наоборот.

В качестве подсказки файл, закодированный UTF-32, может содержать в качестве первого символа т.н. Byte-Order Mark (BOM) с номером 65279 (FEFF в шестнадцатеричной записи).

С UTF-16 всё несколько хитрее: как и с UTF-16, есть два вида этой кодировки, различающиеся порядком байт. При этом символы с номерами, не превышающими 65535, кодируются этими самыми номерами (как и в UTF-32). А вот для символов с большими номерами используется хитрая схема кодирования (подробности см. например в википедии).

Кодировка UTF-8 на настоящий момент является самой распространённой из всех кодировок юникода. Она устроена следующим образом: номер символа записывается в двоичной системе счисления, далее достраивается ведущими нулями до одной из точек остановки (7 бит, 11 бит, 16 бит, 21 бит, 26 бит, 31 бит), после чего эти биты распределяются по байтам в соответствии с таблицей:

 7 бит  0xxxxxxx
11 бит  110xxxxx 10xxxxxx
16 бит  1110xxxx 10xxxxxx 10xxxxxx
21 бит  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
26 бит  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
31 бит  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

Порядок бит в этой таблице традиционный: они идут от самых старших к самым младшим. Например, символ «скрипичный ключ» (𝄞), имеющий номер 119070 (1d11e в шестнадцатеричном виде), представляется в UTF-8 так: число 1d11e записывается 17ю битами:

11101000100011110
1   d   1   1   e

Ближайшая точка остановки: 21 бит. Дополняем нулями, получая последовательность

000011101000100011110

Далее разбиваем её на шестёрки, начиная с конца:

000 011101 000100 011110

Наконец, вписываем служебные последовательности:

двоичное    11110000 10011101 10000100 10011110
десятичное       240      157      132      158
16-ричное         f0       9d       84       9e

Как перекодировать текстовый файл?

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

Для таких целей проще всего использовать утилиту iconv со следующим синтаксисом:

iconv -f из_какой_кодировки -t в_какую_кодировку имя_файла

Результат перекодировки выдаётся на стандартный выход. Обычно его перенаправляют в какой-нибудь файл.

Как это сделать в питоне?

В Python есть свои встроенные средства перекодировки. Кроме типа str для текстов есть тип bytes для последовательностей байт. Эти два типа можно преобразовывать друг в друга следующим образом:

str(последовательность_байт, название_кодировки)
bytes(текст, название_кодировки)

Первая конструкция интерпретирует последовательность байт как текст в указанной кодировке. Вторая преобразует текст в последовательность байт согласно указанной кодировке.

Например, очень ущербный аналог iconv может быть реализован так (конечно, опции командной строки в реальности нужно распознавать при помощи модуля argparse, а не вручную):

#!/usr/bin/env python3
import sys
import os

def get_opt(opt):
    for i,arg in enumerate(sys.argv):
        if i == 0: continue

        if arg == "-"+opt: return sys.argv[i+1]

    return None

def get_from_enc(): return get_opt("f")
def get_to_enc():   return get_opt("t")

def get_filename():
    for i,arg in enumerate(sys.argv):
        if i == 0: continue
        
        if (sys.argv[i-1][0] != "-" and 
            sys.argv[i][0]   != "-"): return arg

    return None

def main():
    from_enc = get_from_enc()
    to_enc   = get_to_enc()
    filename = get_filename()

    if from_enc == None:
        print("Входная кодировка не указана")
        return
    
    if to_enc == None:
        print("Выходная кодировка не указана")
        return

    contents = None
    # --------------V  стандартный поток входа
    with (os.fdopen(0,"rb") if filename == None else 
          open(filename,"rb")) as file:
        contents = file.read()

    # -------V  это номер стандартного выхода
    os.write(1,bytes(str(contents,from_enc),to_enc))

main()

Из этой программы можно уяснить следующее:

  • файлы можно открывать в режиме "rb" (а также "wb" и "ab"); в этом случае методы read и write работают с типом bytes, а не str
  • with можно использовать вместе с open
  • sys.stdout открыт в режиме "w", а не "wb"; именно поэтому печать результата производится при помощи os.write
  • с sys.stdin ровно та же ситуация, осложнённая тем, что os.read не умеет читать сразу всё содержимое файла

Как вручную отредактировать содержимое нетекстового файла?

Проще всего воспользоваться командой xxd. У неё есть обратный режим xxd -r, который получает на вход шестнадцатеричный дамп и выдаёт соответствующее этому дампу содержимое. То есть процесс работы выглядит так:

xxd название_файла > название_дампа

vim/nano/emacs/любой_другой_редактор  название_дампа

xxd -r название_дампа > название_файла