Создание кодека строки

Создание кодека строки

  • 2020-09-30 / 20:36

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


В Python 2 была интересная возможность кодировать строку с помощью ZIP-архивации.
Ведь что такое кодирование? Просто преобразование данных из одного вида в другой. Компрессия это тоже просто другая форма данных. 

Представление данных в виде ZIP просто и легко сокращает размер этих данных. Это бывает полезно или даже критично при передаче данных по медленным каналам.

# Python2
>>> my_str = 'Hello ZIP'
>>> my_zip_str = my_str.encode('zip')
>>> print my_zip_str
xЬєH═╔╔WИЄ♀  ☼╨♥️

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

>>> my_zip_str.decode('zip')
'Hello ZIP'

Всё на месте!
Пытливые умы заметили, что мы нифига не сэкономили 😭

>>> print len(my_str), len(my_zip_str)
9, 17

Увеличили размер почти в 2 раза! Да, это так. Но вся сила ZIP раскрывается на больших данных

>>> my_str = 'Hello ZIP ' * 100
>>> my_zip_str = my_str.encode('zip')
>>> print len(my_str), len(my_zip_str)
1000 27

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

Да, круто, но к чемуто это я? Дело в том что в Python 3 этот код не сработает. Во-первых, кодек переименован в "zlib_codec", во вторых подобный код вызовет ошибку и отправит нас в модуль codecs.

my_str.encode('zlib_codec')
LookupError: 'zlib_codec' is not a text encoding; use codecs.encode() to handle arbitrary codecs

Данный функционал можно повторить и в Python 3 но кода получится куда больше.
И что теперь? Неужели я предлагаю заняться "некрокодингом" на Python 2?
Нет, не нужно тревожить пенсионера 🚑. 

Вы знаете, что строки в Python можно кодировать и декодировать в разные кодировки.

>>> s = 'Привет'
>>> s.encode()  # в байты
b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'

>>> s.encode('ascii') # в ASCII (если получится)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

>>> s.encode('euc_jp')  # японская кодировка
b'\xa7\xb1\xa7\xe2\xa7\xda\xa7\xd3\xa7\xd6\xa7\xe4'

>>> s.encode('hz')  # Simplified Chinese
b"~{'1'b'Z'S'V'd~}"

И другие...

А можно ли добавить свою кодировку в этот список?
Можно! И сейчас мы это сделаем. Давайте добавим ZIP-кодировку из Python 2.

Для начала нам нужен алгоритм. Я хочу сжимать строку через ZIP и после получения байтов преобразовывать их в строку через base64 (можно и без base64). Получится такой код:

import zlib, base64
>>> orig = 'hello python'
>>> compressed = zlib.compress(orig.encode(), 5)
>>> compresed_b = base64.encodebytes(compressed)
>>> print(compresed_b)
b'eF7LSM3JyVcoqCzJyM8DAB7wBNc='

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

>>> compressed = base64.decodebytes(compresed_b)
>>> restored = zlib.decompress(compressed).decode()
>>> orig == restored
True

Отлично! 😎
Теперь, чтобы создать кодек на базе этих алгоритмов, следует воспользоваться функций codecs.register().
В неё подаётся имя функции которая должна вернуть объект codecs.CodecInfo. 
Данный объект будет содержать две функции - кодирование и декодирование. Это и будет наш кодек.
Давайте назовём наш кодек просто "z". Тогда его создание будет выглядеть как-то так:

import zlib, base64, codecs
def z_encode(data):
    return base64.encodebytes(zlib.compress(data.encode(), 5)), 0

def z_decode(data):
    return zlib.decompress(base64.decodebytes(data)).decode(), 0def z_search(encoding_name):
    return codecs.CodecInfo(z_encode, z_decode, name='z')codecs.register(z_search)

Тестим!

>>> s = 'Hello New codec!'
>>> s_enc = s.encode('z')
>>> print(s_enc)
b'eF7zSM3JyVfwSy1XSM5PSU1WBAAvxwV+\n'

Теперь обратно

>>> s_enc.decode('z')
'Hello New codec!'

А теперь практика!. Сжимаем JSON со списком файлов 

>>> from pathlib import Path
>>> import json
>>> files = list(map(str, list(Path('~/Documents').expanduser().glob('**/*'))))
>>> print(len(files))
5390
>>> text = json.dumps(files)
>>> print(len(text))
513839
>>> enc = text.encode('z')
>>> print(len(enc))
53317

Профит почти в 10 раз!

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

PS. А вот стандартный и самый короткой способ для ZIP-сжатия байтов в Python3 

>>> import codecs
>>> codecs.encode(my_bytes, 'zip'))