Существует множество вариантов строк с префиксом длины, но ключевые биты сводятся к тому, как вы сохраняете длину.
Вы десериализуете длину как односимвольное число ASCII, что означает, что вы можете обрабатывать только длины от 0 до 9. (На самом деле вы не проверяете это на размере сериализации, поэтому вы можете генерировать мусор, но давайте забудь об этом.)
Итак, очевидный вариант - использовать 2 символа вместо 1. Давайте добавим немного обработки ошибок, пока мы на этом; код все еще довольно прост:
def _len(word):
s = format(len(word), '02')
if len(s) != 2:
raise ValueError(f"Can't serialize {s}; it's too long")
return s
def serialize(list_o_words):
return ''.join(_len(word) + word for word in list_o_words)
def deserialize(serialized_list_o_words):
index = 0
deserialized_list = []
while index+1 < len(serialized_list_o_words):
word_length = int(serialized_list_o_words[index:index+2])
next_index = index + word_length + 2
deserialized_list.append(serialized_list_o_words[index+2:next_index])
index = next_index
return deserialized_list
Но теперь вы не можете обрабатывать строки> 99 символов.
Конечно, вы можете продолжать добавлять больше цифр для более длинных строк, но если вы думаете, что «мне никогда не понадобится строка из 100 000 символов»… вам это понадобится, и тогда у вас будет миллион лет файлы в 5-значном формате, несовместимые с новым 6-значным форматом.
Кроме того, это тратит много байтов. Если вы используете 5-значную длину, s
кодируется как 00000s
, что в 6 раз больше исходного значения.
Вы можете растягивать вещи намного дальше, используя двоичные длины вместо ASCII. Теперь, используя два байта, мы можем обрабатывать длину до 65535 вместо всего 99. И если вы перейдете к четырем или восьми байтам, то может на самом деле будет достаточно большим для всех ваших строк. Конечно, это работает, только если вы храните bytes
вместо строк Unicode, но это нормально; вам, вероятно, все равно нужно было закодировать ваши строки для сохранения. Итак:
def _len(word):
# already raises an exception for lengths > 65535
s = struct.pack('>H', len(word))
def serialize(list_o_words):
utfs8 = (word.encode() for word in list_o_words)
return b''.join(_len(utf8) + utf8 for utf8 in utfs8)
Конечно, это не очень читабельно или редактируемо; в шестнадцатеричном редакторе вам должно быть удобно заменить строку в файле таким образом.
Другим вариантом является определение длины. Это может звучать как шаг назад, но все же дает нам все преимущества знания длины заранее. Конечно, вы должны «читать до запятой», но вам не нужно беспокоиться о запятых или кавычках, как при работе с CSV-файлами, и если вы беспокоитесь о производительности, чтение будет намного быстрее буфер 8K за раз и разделить его с помощью некоторого цикла C (будь то нарезка, или str.find
, для сравнения едва ли имеет значение), чем фактически читать либо до запятой, либо до двух байтов.
Это также имеет преимущество решения проблемы синхронизации. С разделенными значениями, если вы входите в середину потока или не синхронизированы из-за ошибки, это не имеет большого значения; просто читайте до следующего неэкранированного разделителя, и в худшем случае вы пропустили несколько значений. Со значениями с префиксом длины, если вы не синхронизированы, вы читаете произвольные символы и рассматриваете их как длину, что просто делает вас еще более не синхронизированным. Формат netstring представляет собой небольшую вариацию этой идеи, с чуть большей избыточностью для упрощения обнаружения и устранения проблем синхронизации.
Возвращаясь к двоичной длине, есть все виды хитрых трюков для кодирования чисел переменной длины. Вот одна идея в псевдокоде:
if the current byte is < hex 0x80 (128):
that's the length
else:
add the low 7 bits of the current byte
plus 128 times (recursively process the next byte)
Теперь вы можете обрабатывать короткие строки длиной всего 1 байт, но если появляется строка длиной 5 миллиардов символов, вы можете справиться и с этим.
Конечно, это даже менее понятно для человека, чем фиксированные двоичные длины.
И, наконец, если вы хотите иметь возможность хранить другие виды значений, а не только строки, вам, вероятно, нужен формат, использующий «код типа». Например, используйте I
для 32-разрядного типа int, f
для 64-разрядного числа с плавающей запятой, D
для datetime.datetime
и т. Д. Затем вы можете использовать s
для строк <256 символов длиной 1 байт , <code>S для строк <65536 символов с длиной 2 байта, <code>z для строки <4B символов с длиной 4 байта и <code>Z для строк без ограничений со сложной длиной переменной int (или, может быть, null- завершенные строки, или, может быть, длина в 8 байтов достаточно близка к неограниченной - в конце концов, никто никогда не захочет иметь больше 640 КБ на компьютере…).