Можно ли в Python создавать бинарные файлы? Конечно можно.
Для этого в Python есть следующие инструменты:
▫️ тип данных bytes и bytearray
▫️ открытие файла в режиме wb (write binary) или rb (read binary)
▫️ модуль struct
Про модуль struct поговорим в первую очередь.
Файл в формате JSON или Yaml внутри себя содержит разметку данных. Всегда можно определить где список начался а где закончился. Где записана строка а где словарь. То есть формат записи данных содержит в себе элементы разметки данных.
В binary-файле данные не имеют визуальной разметки. Это просто байты, записанные один за другим. Правила записи и чтения находятся вне файла.
Модуль struct как раз и помогает с организацией данных в таком файле с помощью определения форматов записи для разных частей файла.
Модуль struct преобразует Python-объекты в массив байт, готовый к записи в файл и имеющий определённый вид.
Для этого всегда следует указывать формат преобразования (или, как оно здесь называется - запаковки).
Формат нужен для того, чтобы выделить достаточное количество байт для записи конкретного типа объекта. В последствии с помощью того же формата будет производиться чтение.
При этом следует помнить что мы говорим о типах языка С а не Python.
Именно формат определяет, что записано в конкретном месте файла, число, строка или что-то еще.
Вот какие токены формата у нас есть.
Помимо этого, первым символом можно указать порядок байтов. На разных системах одни и те же типы данных могут записываться по-разному, поэтому желательно указать конкретный способ из доступных. Если этого не сделать, то используется символ '@', то есть нативный для текущей системы.
В строке формата мы пишем в каком порядке и какие типы собираемся преобразовать в байты.
Запакуем в байты простое число, токен "i".
#lib #basic
Для этого в Python есть следующие инструменты:
▫️ тип данных bytes и bytearray
▫️ открытие файла в режиме wb (write binary) или rb (read binary)
▫️ модуль struct
Про модуль struct поговорим в первую очередь.
Файл в формате JSON или Yaml внутри себя содержит разметку данных. Всегда можно определить где список начался а где закончился. Где записана строка а где словарь. То есть формат записи данных содержит в себе элементы разметки данных.
В binary-файле данные не имеют визуальной разметки. Это просто байты, записанные один за другим. Правила записи и чтения находятся вне файла.
Модуль struct как раз и помогает с организацией данных в таком файле с помощью определения форматов записи для разных частей файла.
Модуль struct преобразует Python-объекты в массив байт, готовый к записи в файл и имеющий определённый вид.
Для этого всегда следует указывать формат преобразования (или, как оно здесь называется - запаковки).
Формат нужен для того, чтобы выделить достаточное количество байт для записи конкретного типа объекта. В последствии с помощью того же формата будет производиться чтение.
При этом следует помнить что мы говорим о типах языка С а не Python.
Именно формат определяет, что записано в конкретном месте файла, число, строка или что-то еще.
Вот какие токены формата у нас есть.
Помимо этого, первым символом можно указать порядок байтов. На разных системах одни и те же типы данных могут записываться по-разному, поэтому желательно указать конкретный способ из доступных. Если этого не сделать, то используется символ '@', то есть нативный для текущей системы.
В строке формата мы пишем в каком порядке и какие типы собираемся преобразовать в байты.
Запакуем в байты простое число, токен "i".
>>> import structТеперь несколько
>>> struct.pack('=i', 10)
b'\n\x00\x00\x00'
float, при этом нужно передавать элементы не массивом а последовательностью аргументов.>>> struct.pack('=fff', 1.0, 2.5, 4.1)
b'\x00\x00\x80?\x00\x00 @33\x83@'
Вместо нескольких токенов можно просто указать нужное количество элементов перед одним токеном, результат будет тот же.>>> struct.pack('=3f', 1.0, 2.5, 4.1)
b'\x00\x00\x80?\x00\x00 @33\x83@'
Теперь запакуем разные типы>>> data = struct.pack('=fiQ', 1.0, 4, 100500)
я запаковал типы float, int и unsigned long long (очень большой int, на 8 байт)b'\x00\x00\x80?\x04\x00\x00...'Распаковка происходит аналогично, но нужно указать тот же формат, который использовался при запаковке. Результат возвращается всегда в виде кортежа.
>>> struct.unpack('=fiQ', data)
(1.0, 4, 100500)
Как видите, ничего страшного!#lib #basic
Теперь запакуем строку.
В этом случае следует передавать тип данных bytes.
#libs #basic
В этом случае следует передавать тип данных bytes.
>>> struct.pack('=s', b'a')
b'a'
Для записи слова следует указывать количество символов.>>> struct.pack('=5s', b'hello')
b'hello'
Кстати, запакованный вид соответствует исходному тексту. Всё верно, символ есть в таблице ASCII, то есть его код попадает в диапазон 0-127, он может быть записан одним байтом и имеет визуальное представление. А вот что будет если добавить символ вне ASCII>>> struct.pack(f'=s', b'ё')
SyntaxError: bytes can only contain ASCII literal characters.
Ошибка возникла еще на этапе создания объекта bytes, который не может содержать такой символ. Поэтому надо кодировать эти байты из строки.>>> enc = 'ёжик'.encode('utf-8')
>>> struct.pack(f'={len(enc)}s', enc)
b'\xd1\x91\xd0\xb6\xd0\xb8\xd0\xba'
Заметьте, длина такой строки в байтах отличается от исходной длины, так как символы вне ASCII записываются двумя байтами и более. Поэтому здесь формат создаём на лету, используя получившуюся длину как каунтер токена.#libs #basic
Есть один интересный момент с запаковкой строк.
Если вам требуется записать просто одну строку какой-то рандомной длины, то паковка тут не нужна. Можете писать любые байты в файл как угодно без запаковки.
А смысл паковки в том, что с помощью формата мы гарантируем правильную длину всех частей записываемых данных.
Если данных слишком много, они обрезаются, если мало, то лишнее заполнится нулевым байтом. Ведь мы читаем данные, ориентируясь на заведомо установленные и предсказуемые позиции байтов в файле.
Вот абстрактный пример спецификации файла:
◽️
Если мы хоть на один байт сместимся, то данные распакуются некорректно.
Когда размер данных заранее неизвестен, то, обычно, перед непосредственно данными пишут сколько они занимают места до следующего блока данных, как в моём примере.
В более простых случаях это необязательно.
❗️Еще раз, помимо преобразования разных типов в байты, модуль struct занимается форматированием или разметкой данных в файле. Именно это и означает что разметка находится вне файла с данными. И это критически важный момент при создании бинарных файлов!
#libs
>>> struct.pack(f'=6s', b'python')
b'python'
>>> 'python'.encode()
b'python'
Хммм..., struct создает тип bytest но при этом для строки просит тоже bytes. На выходе получаем опять bytes без изменений. В чем логика? Ведь ничего же не поменялось! Зачем тогда нам вообще нужен struct если encode делает тоже самое?Если вам требуется записать просто одну строку какой-то рандомной длины, то паковка тут не нужна. Можете писать любые байты в файл как угодно без запаковки.
А смысл паковки в том, что с помощью формата мы гарантируем правильную длину всех частей записываемых данных.
Если данных слишком много, они обрезаются, если мало, то лишнее заполнится нулевым байтом. Ведь мы читаем данные, ориентируясь на заведомо установленные и предсказуемые позиции байтов в файле.
>>> struct.pack(f'=3s', b'python')
b'pyt'
>>> struct.pack(f'=10s', b'python')
b'python\x00\x00\x00\x00'
Формат запаковки нужен как раз для того, чтобы фиксировать разметку файла на основе размера используемых типов данных.Вот абстрактный пример спецификации файла:
◽️
128 байт : какой-то заголовок (str)
◽️8 байт : количество элементов (int)
◽️[4 байта] : массив данных (тип float до конца файла по 4 байта)
Теперь мы знаем как записывать и считывать этот файл, у нас есть его спецификация. Просто берëм нужный диапазон байт и распаковываем в нужный тип данных.Если мы хоть на один байт сместимся, то данные распакуются некорректно.
Когда размер данных заранее неизвестен, то, обычно, перед непосредственно данными пишут сколько они занимают места до следующего блока данных, как в моём примере.
В более простых случаях это необязательно.
data1 = [...]
data2 = [...]
struct.pack(f'=Q{len(data1)}i', len(data1), data1)
struct.pack(f'=Q{len(data2)}i', len(data2), data2)
Начиная с начала файлы мы точно знаем что следующие 8 байт (Q = unsigned long long) это число с количеством элементов, записанных сразу после него. И точно знаем где находится следующий блок данных.❗️Еще раз, помимо преобразования разных типов в байты, модуль struct занимается форматированием или разметкой данных в файле. Именно это и означает что разметка находится вне файла с данными. И это критически важный момент при создании бинарных файлов!
#libs
Давайте посмотрим что со скоростью записи в байты.
Написал тестовый скрипт который пишет 10к значений в 3к файлов с помощью JSON и через struct.
Код берём здесь↗️
Вот результаты на моём железе.
Теперь в функцию json.dump() добавим аргумент indent=4, разница станет еще больше.
Запись х35, чтение х3.1, размер файла х3.2.
И чем больше данных, тем больше разница.
4к файлов по 15к значений, indent=4:
Запись х40.4, чтение х3.3, размер файла х3.3
Очевидно, что при записи в JSON много времени уходит на преобразование данных в строку в нужном формате. И обратная операции во время чтения. Удобство имеет свою цену)
В свою очередь байты пишутся как есть без изменений и лишних знаков форматирования. Нужно лишь преобразовать каждый тип данных в массив байт.
Формат находится вне файла, то есть никакой разметки, в отличие от JSON файла. Поэтому файл на много меньше по размеру.
#libs
Написал тестовый скрипт который пишет 10к значений в 3к файлов с помощью JSON и через struct.
Код берём здесь↗️
Вот результаты на моём железе.
JSON:Через байты скорость записи х25.8 быстрей, чтение х1.7. Размер файла в 2 раза меньше.
Array Size: 85176
File Size: 80560
Time: W:41.9381s R:24.909s
BYTES:
Array Size: 40033
File Size: 40000
Time: W:1.6251s R:14.5471s
Теперь в функцию json.dump() добавим аргумент indent=4, разница станет еще больше.
Запись х35, чтение х3.1, размер файла х3.2.
И чем больше данных, тем больше разница.
4к файлов по 15к значений, indent=4:
Запись х40.4, чтение х3.3, размер файла х3.3
Очевидно, что при записи в JSON много времени уходит на преобразование данных в строку в нужном формате. И обратная операции во время чтения. Удобство имеет свою цену)
В свою очередь байты пишутся как есть без изменений и лишних знаков форматирования. Нужно лишь преобразовать каждый тип данных в массив байт.
Формат находится вне файла, то есть никакой разметки, в отличие от JSON файла. Поэтому файл на много меньше по размеру.
#libs
Gist
struct_vs_json.py
GitHub Gist: instantly share code, notes, and snippets.
Исследуем бинарный файл с изображением. Файл примера можно забрать сразу после этого поста.
Для простоты эксперимента, я сделал BMP файл размером 1х1 пиксель, сохранённый без сжатия.
Наша задача — достать RGB информацию этого единственного пикселя.
Файл я сделал в Photoshop и закрасил пиксель цветом [255, 128, 50]. Сохранил с глубиной цвета 24 бит (по 8 бит на канал, то есть 1 байт).
Вооружившись спецификацией формата BMP мы можем рассчитать где что записано в таком файле.
В начале файла записаны заголовки с различной информацией. Для её отображения можете использовать этот код.
Эти данные активно используют программы-просмотрщики. Например, первые два байта это сигнатура файла (вспоминаем посты по этой тему).
Полезное для нас поле - DataOffset, которое говорит где начинаются данные относительно начала файла.
Открываем файл
Проверяем
PS:
Кто в теме, может покопаться в скрипте для парсинга бинарника mb-файла (файл сцены Autodesk Maya)
https://github.com/westernx/mayatools/blob/master/mayatools/binary.py
#libs
Для простоты эксперимента, я сделал BMP файл размером 1х1 пиксель, сохранённый без сжатия.
Наша задача — достать RGB информацию этого единственного пикселя.
Файл я сделал в Photoshop и закрасил пиксель цветом [255, 128, 50]. Сохранил с глубиной цвета 24 бит (по 8 бит на канал, то есть 1 байт).
Вооружившись спецификацией формата BMP мы можем рассчитать где что записано в таком файле.
В начале файла записаны заголовки с различной информацией. Для её отображения можете использовать этот код.
Эти данные активно используют программы-просмотрщики. Например, первые два байта это сигнатура файла (вспоминаем посты по этой тему).
Полезное для нас поле - DataOffset, которое говорит где начинаются данные относительно начала файла.
Offset: 54
То есть, с начала файла надо пропустить 54 байта, после чего пойдут пиксели! Так и сделаем.Открываем файл
file = open('one_pixel.bmp', 'rb')
Пропускаем 54 байтаfile.seek(54)
В разделе Additional Info в спецификации написано, что порядок записи каналов такой: BGR. Поэтому забираем наши данные о каналах в таком же порядкеb, g, r = struct.unpack('BBB', file.read(3))
file.close()
Почему "B"? Потому что в спецификации указано, что на канал использовано по 1 байту.Проверяем
print(f'R:{r} G:{g} B:{b}')
R:255 G:128 B:50
Отлично, мы добыли то что нам требовалось 😊PS:
Кто в теме, может покопаться в скрипте для парсинга бинарника mb-файла (файл сцены Autodesk Maya)
https://github.com/westernx/mayatools/blob/master/mayatools/binary.py
#libs
Пора нам придумать свой бинарный формат😉
В качестве примера я запишу в файл анимационный канал из объекта Autodesk Maya.
Можете открыть мой код↗️ и следить по тексту.
Если у вас есть Maya, то можно даже запустить код и посмотреть на результат.
🔸Начнём запись!
Сначала запишем имя канала, это будет имя атрибута, с которого пишется анимация. Сделаем предел в 64 байта.
Итого у нас получился такой формат
🔸Теперь чтение.
Считываем первые 64 байта, это имя канала.
Первое с чем столкнёмся, это нулевые байты в имени канала, которые заполняют свободное пространство в выделенных 64 байтах. Просто удаляем их.
Остальной код примера связан с манипуляцией объектами в Maya, чтобы визуально можно было увидеть, что анимация корректно восстановилась из файла.
Вот таким образом мы придумали свой бинарный формат данных.
Возможно, такие сложности вам покажутся излишними, но представьте когда данных действительно много, и один кадр содержит миллионы позиций 3D точек, и записать требуется 50000 кадров!
Всё это в оперативку явно не поместится, придётся для каждого кадра делать отдельный файл. Если же мы можем писать данные постепенно, то это не проблема. Можно постепенно заполнять файл или писать несколько файлов паралельно.
#libs #tricks
В качестве примера я запишу в файл анимационный канал из объекта Autodesk Maya.
Можете открыть мой код↗️ и следить по тексту.
Если у вас есть Maya, то можно даже запустить код и посмотреть на результат.
🔸Начнём запись!
Сначала запишем имя канала, это будет имя атрибута, с которого пишется анимация. Сделаем предел в 64 байта.
struct.pack('=64s', channel_name.encode())
Далее диапазон кадров, это два числа типа longstruct.pack('=2L', start_frame, end_frame)
Потом пишем анимацию в виде массива float значений. В примере запись идёт покадрово, то есть мы не загружаем весь массив ключей в память.for i in range(start_frame, end_frame + 1):
val = obj.attr(channel_name).get(t=i)
f.write(struct.pack('=f', val))
Всё, файл готов! 😉Итого у нас получился такой формат
"=64s2L{N}f", где {N} это количество записанных значений.🔸Теперь чтение.
Считываем первые 64 байта, это имя канала.
Первое с чем столкнёмся, это нулевые байты в имени канала, которые заполняют свободное пространство в выделенных 64 байтах. Просто удаляем их.
struct.unpack('=64s', f.read(64))[0].rstrip(b'\x00')
Читаем диапазон кадров, записанный в этот файл.frange_len = struct.calcsize('=2L')
Функция struct.calcsize() возвращает размер данных в зависимости от указанного формата. Используем это чтобы прочитать нужное количество байт из файла.start_frame, end_frame = struct.unpack('=2L', f.read(frange_len))
Из диапазона рассчитаем длину анимации и забираем массив float значений. В коде есть вариант чтения по одному значению и полностью весь массив.key_count = end_frame - start_frame + 1
frmt = f'={key_count}f'
keys = struct.unpack(frmt, f.read(struct.calcsize(frmt)))
Всё, данные прочитаны!😎Остальной код примера связан с манипуляцией объектами в Maya, чтобы визуально можно было увидеть, что анимация корректно восстановилась из файла.
Вот таким образом мы придумали свой бинарный формат данных.
Возможно, такие сложности вам покажутся излишними, но представьте когда данных действительно много, и один кадр содержит миллионы позиций 3D точек, и записать требуется 50000 кадров!
Всё это в оперативку явно не поместится, придётся для каждого кадра делать отдельный файл. Если же мы можем писать данные постепенно, то это не проблема. Можно постепенно заполнять файл или писать несколько файлов паралельно.
#libs #tricks
Gist
save_load_anim_channel_test.py
GitHub Gist: instantly share code, notes, and snippets.
В модуле struct есть класс Struct, специально для тех то любит в ООП.
Возможно, кому-то будет удобней работать с классом вместо функций.
Один раз указываем формат в конструкторе класса и получаем удобные свойства и методы.
Возможно, кому-то будет удобней работать с классом вместо функций.
Один раз указываем формат в конструкторе класса и получаем удобные свойства и методы.
>>> st_head = struct.Struct('<20s')
>>> st_head.format
'<20s'
>>> st_values = struct.Struct('=100i')
>>> st_values.size
400
Для запаковки или распаковки просто передаём данные в соответствующие методы.>>> st_head.pack(b'some_name')#libs #tricks
b'some_name\x00\x00...'
>>> st_values.pack(*range(100))
b'\x00\x00\x00\x00\x01\x00\x00...'
Формат структуры поддерживает две удобные фишки
▫️ Вместо дублирования токена можно указать цифру и сразу после неё нужный токен (это вы уже знаете по прошлым постам).
▫️ Вместо дублирования токена можно указать цифру и сразу после неё нужный токен (это вы уже знаете по прошлым постам).
struct.pack('=10s', data)
▫️ Для визуального удобства токены можно разделять пробелами, но не каунтеры (цифры перед токеном)struct.pack('= 10s I I 100Q', *items)
#libs #tricksЧем отличается тип bytes от bytearray? Всё просто, bytes неизменяемый тип, а bytearray изменяемый.
Что это нам даёт? Как известно, строка это неизменяемый тип. Всякий раз когда вы делаете любые манипуляции со строкой вы создаёте новую строку.
Если же её преобразовать в bytearray то все изменения будут происходить с оригинальным объектом без копирования.
Создаём массив
#tricks #libs
Что это нам даёт? Как известно, строка это неизменяемый тип. Всякий раз когда вы делаете любые манипуляции со строкой вы создаёте новую строку.
Если же её преобразовать в bytearray то все изменения будут происходить с оригинальным объектом без копирования.
Создаём массив
>>> arr = bytearray(struct.pack('=11s', b'Hello World'))
bytearray(b'Hello World')
Можем добавить элемент в массив>>> arr.append(0)Или удалить лишний элемент по индексу
bytearray(b'Hello World\x00')
>>> del arr[-1]Для добавления в строку используем extend
bytearray(b'Hello World')
>>> arr.extend(b'!')С помощью pack_into() вставляем данные в имеющийся массив заменяя данные
bytearray(b'Hello World!')
>> struct.pack_into("=6s", arr, 6, b'Python')
bytearray(b'Hello Python')
Достаём результат>>> struct.unpack("=12s", arr)[0]
b'Hello Python'
И всё это мы сделали не создавая новых объектов! Это и экономит память, и выполняется быстрей, так как мы работаем с одним и тем же объектом.#tricks #libs
Уже полтора года как Python2 отправился на пенсию.
Как идёт процесс перехода на 3ю ветку?
В вебе всё более менее нормально. Django начиная с 2.0 и недавно вышедший Flask 2.0 официально больше не поддерживают Python 2.
На странице Qt for Python вторая ветка пропала из таблицы поддерживаемых версий. Теперь минимальная версия 3.8.
Но меня больше интересует готовность CG-софта. Я предполагал, что период перехода займёт от 3 до 5 лет. При том что резко, как с web, перескочить не получится и какое-то время придётся поддерживать обе ветки (как это сделали с Houdini и Maya). А ведь переделывать там ой как много.
Но, к счастью, процесс идёт достаточно бодро! Судя по этой статистике три четверти приложений уже на Ру3!😊 Остальные догоняют.
Надеюсь план по переходу на 3й Python будет завершён к концу 2021 года.
#2to3
Как идёт процесс перехода на 3ю ветку?
В вебе всё более менее нормально. Django начиная с 2.0 и недавно вышедший Flask 2.0 официально больше не поддерживают Python 2.
На странице Qt for Python вторая ветка пропала из таблицы поддерживаемых версий. Теперь минимальная версия 3.8.
Но меня больше интересует готовность CG-софта. Я предполагал, что период перехода займёт от 3 до 5 лет. При том что резко, как с web, перескочить не получится и какое-то время придётся поддерживать обе ветки (как это сделали с Houdini и Maya). А ведь переделывать там ой как много.
Но, к счастью, процесс идёт достаточно бодро! Судя по этой статистике три четверти приложений уже на Ру3!😊 Остальные догоняют.
Надеюсь план по переходу на 3й Python будет завершён к концу 2021 года.
#2to3
Vfxpy
VFX Python 3 Readiness
Python 3 support graph for most popular Python libraries and DCC applications used in VFX production
Библиотека pstray поможет легко создать иконку в системном трее максимально нативными средствами системы без тяжеловесного Qt и ему подобных. Здесь же есть средства создать меню, нотификации и даже radio button.
#libs
#libs
GitHub
GitHub - moses-palmer/pystray
Contribute to moses-palmer/pystray development by creating an account on GitHub.
Что-нибудь слышали про Лабораторию динамики флуоресценции?
Если не интересуетесь конкретно этой наукой то здесь вам ловить нечего. Кроме одного момента! Сотрудник лаборатории Christoph Gohlke поддерживает неофициальную библиотеку бинарников для Python под Windows. Большая коллекция скомпиленных библиотек под разные версии Python.
Именно здесь я долгое время качал старую версию PySide под Python2 и OpenImageIO, пока не потребовалось собрать её иначе.
В общем, всем тем кто на Windows, советую страничку в закладки. Также будет полезно тем кто еще на Python2.
Кстати, эта коллекция всё еще обновляется.
#libs #2to3
Если не интересуетесь конкретно этой наукой то здесь вам ловить нечего. Кроме одного момента! Сотрудник лаборатории Christoph Gohlke поддерживает неофициальную библиотеку бинарников для Python под Windows. Большая коллекция скомпиленных библиотек под разные версии Python.
Именно здесь я долгое время качал старую версию PySide под Python2 и OpenImageIO, пока не потребовалось собрать её иначе.
В общем, всем тем кто на Windows, советую страничку в закладки. Также будет полезно тем кто еще на Python2.
Кстати, эта коллекция всё еще обновляется.
#libs #2to3
www.lfd.uci.edu
LFD - Laboratory for Fluorescence Dynamics
A NIH research resource center for biomedical fluorescence spectroscopy at the University of California, Irvine.
Тип строки в Python имеет очень много удобных методов. Сегодня пост про два таких метода которые чаще всего используются "однобоко". Это методы startswith() и endswith()
Самый обычный сценарий использования — проверка, начинается ли строка с указанной подстроки?
🔸 Сравнение нескольких подстрок
Для проверки нескольких подстрок в одной строке обычно вызывают эти функции несколько раз. Но на самом деле достаточно передать кортеж со всеми строками один раз. Если будет хоть одно совпадение то функция вернёт
Вторым аргументом можно передать индекс символа с которого следует начать сравнение, а третий аргумент это индекс последнего символа.
#trics #basic
Самый обычный сценарий использования — проверка, начинается ли строка с указанной подстроки?
>>> "some_string".startswith("some")
True
И аналогичная ситуация с зеркальным вариантом этой функции, проверка совпадения с конца>>> "some_string".endswith("some")
False
Так они используются в большинстве случаев что я видел. Но у этих функций есть еще два варианта использования.🔸 Сравнение нескольких подстрок
Для проверки нескольких подстрок в одной строке обычно вызывают эти функции несколько раз. Но на самом деле достаточно передать кортеж со всеми строками один раз. Если будет хоть одно совпадение то функция вернёт
True.>>>"my_image.png".endswith(("jpg", "png", "exr"))
True
🔸 Диапазон поискаВторым аргументом можно передать индекс символа с которого следует начать сравнение, а третий аргумент это индекс последнего символа.
>>> ".filename.ext".startswith("file", 1)
True
>>> "file_###.ext".endswith('#', 0, -4)
True
Индексы можно указать отрицательными, что означает отсчёт с конца.#trics #basic
Модуль ensurepip, стал стандартным начиная с версии 3.4 и портирован в 2.7
Это встроенная альтернатива файлу get-pip.py. Модуль позволяет установить или обновить pip.
🔸Установка pip:
Это встроенная альтернатива файлу get-pip.py. Модуль позволяет установить или обновить pip.
🔸Установка pip:
python -m ensurepip🔸Обновление до актуальной версии
python -m ensurepip --upgrade🔸Установка в директорию юзера, если вас не устраивает системный или просто нет доступа для обновления (когда не используем venv, то есть ставим глобально)
python -m ensurepip --user#libs #basic
Недавно писал тесты для модуля, который рисует на картинках текст и разные фигуры.
Обычные ошибки в коде можно поймать простым исключением. Но как убедиться что нарисовано именно то что надо? Например цвет правильный или шрифт выбран верно. Для этого нужно визуально сравнивать правильный рендер и тест.
Чтобы авто тесты оставались "авто", я использовал библиотеку imgcompare
С помощью неё достаточно просто сравнить два изображения и получить процентное соотношение различий между картинками.
Очень удобно проверять расхождения даже в мелочах. Например если что-то пошло не так и использовался шрифт по умолчанию. К тому же мелкие различия глазами не так уж просто заметить. Видите разницу в 1 процент на картинке к посту? Нет? А она есть🐹!
➡️ https://github.com/datenhahn/imgcompare
#libs #tricks
Обычные ошибки в коде можно поймать простым исключением. Но как убедиться что нарисовано именно то что надо? Например цвет правильный или шрифт выбран верно. Для этого нужно визуально сравнивать правильный рендер и тест.
Чтобы авто тесты оставались "авто", я использовал библиотеку imgcompare
С помощью неё достаточно просто сравнить два изображения и получить процентное соотношение различий между картинками.
Очень удобно проверять расхождения даже в мелочах. Например если что-то пошло не так и использовался шрифт по умолчанию. К тому же мелкие различия глазами не так уж просто заметить. Видите разницу в 1 процент на картинке к посту? Нет? А она есть🐹!
➡️ https://github.com/datenhahn/imgcompare
#libs #tricks
👍1
Для проверки целостности или идентичности файлов всегда используется проверка контрольной суммы.
Это работает в большинстве случаев, но не всегда. Давайте сделаем простой тест.
Создадим несколько рандомных файлов
Добавим их в архив два раза
Чтобы решить проблему следует проверять хеш сумму самих файлов внутри архива. То есть разархивировать данные без сохранения на диск и посчитать хеш для них.
#libs #tricks
Это работает в большинстве случаев, но не всегда. Давайте сделаем простой тест.
Создадим несколько рандомных файлов
import osЯ создал 5 файлов с рандомными бинарными данными. Нам сейчас неважно что там находится, главное что это некоторые файлы по 10мб.
# create random test files
files_to_archive = []
for i in range(5):
name = f'example_file{i}.txt'
open(name, 'wb').write(os.urandom(10**7))
files_to_archive.append(name)
Добавим их в архив два раза
import tarfileИ проверим хеш сумму
def create_tar(archive_path, files):
with tarfile.open(archive_path, 'w:gz') as tar:
for file in files:
tar.add(file)
create_tar('archive1.tar.gz', files_to_archive)
create_tar('archive2.tar.gz', files_to_archive)
>>> hashlib.md5(open("archive1.tar.gz", "rb").read()).hexdigest()
'ded8771a6ba57281f52a0e0ec38c29b8'
>>> hashlib.md5(open("archive2.tar.gz", "rb").read()).hexdigest()
'2a70bd3137a174393197cf67cbe91a8d'
Несмотря на то, что мы сделали два одинаковых архива, внутри он не очень-то и одинаковы! Причина тут в алгоритме сжатия, который может зависеть от некоего рандома, и в записываемых мета-данных, например время создания файла архива. Даже отличие в один байт делает хеш сумму совершенно другой, несмотря на то, что файлы внутри полностью идентичны.Чтобы решить проблему следует проверять хеш сумму самих файлов внутри архива. То есть разархивировать данные без сохранения на диск и посчитать хеш для них.
def get_hash_tar(path):Таким образом мы обошли те байты архива которые отличаются и посчитали только фактические данные файлов.
hsum = hashlib.md5()
with tarfile.open(path) as tar:
for file in tar.getmembers():
hsum.update(tar.extractfile(file).read())
return hsum.hexdigest()
>>> get_hash_tar('archive1.tar.gz')
'0b27c443737b0a84381b827e1d9a913b'
>>> get_hash_tar('archive2.tar.gz')
'0b27c443737b0a84381b827e1d9a913b'
#libs #tricks
В прошлом примере мы добились совпадения хеш-суммы двух архивов. Но не даёт покоя тот факт, что делается это слишком долго. Давайте сравним скорость.
Кажется что для 100 итераций это время нормальное, но представьте что архив будет размером не 50 Мб а 10Гб. Время возрастёт серьезно!
Попробуем сократить разрыв. Давайте считать не все данные а только хеш файлов, который посчитает сам модуль tarfile.
Почему так, можно почитать в комментарии. Если коротко, мы считываем только заголовки элементов архива. Но на сколько я понял, это не отменяет чтение всего буфера из архива.
А можно быстрей? Можно...
#libs #tricks
>>> from timeit import timeit
>>> timeit("hashlib.md5(
open('archive1.tar.gz', 'rb').read()
).hexdigest()", number=100, globals=globals())
# 8.6
>>> timeit("get_hash_tar1('archive1.tar.gz')",
number=100, globals=globals())
# 29.8
Разница больше чем в 3 раза! Видимо потому, что кроме простого чтения байтиков мы еще применяем алгоритм разжатия данных?Кажется что для 100 итераций это время нормальное, но представьте что архив будет размером не 50 Мб а 10Гб. Время возрастёт серьезно!
Попробуем сократить разрыв. Давайте считать не все данные а только хеш файлов, который посчитает сам модуль tarfile.
def get_hash_tar2(path):
hsum = hashlib.md5()
with tarfile.open(path) as tar:
for file in tar.getmembers():
hsum.update(file.chksum.to_bytes(8, byteorder='big'))
return hsum.hexdigest()
>>> timeit.timeit("get_hash_tar2('archive1.tar.gz')",
number=100, globals=globals())
11.5
Прирост скорости x3! Уже неплохо, почти как просчет хеша для архива без разжатия. Почему так, можно почитать в комментарии. Если коротко, мы считываем только заголовки элементов архива. Но на сколько я понял, это не отменяет чтение всего буфера из архива.
А можно быстрей? Можно...
#libs #tricks
GitHub
cpython/Lib/tarfile.py at main · python/cpython
The Python programming language. Contribute to python/cpython development by creating an account on GitHub.
На самом деле архивы TAR оказались менее удобными в нашей теме проверки идентичности. Давайте сделаем всё тоже самое для ZIP.
Допустим, тестовые файлы мы уже создали используя код из прошлого поста.
Теперь создадим архивы.
Ну всё, на этом расходимся...
Хотя подождите ка, часто ли вы проверяете один и тот же файл на идентичность?
Давайте имитируем ситуацию когда файл был перезаписан или "модифицирован" но при этом фактически не изменился. То есть изменились только его атрибуты. Для этого можно использовать Linux-команду touch, которая обновляет время последнего доступа к файлу.
Давайте теперь сделаем функцию для проверки файлов внутри архивов, которая считывает непосредственно данные файлов в разжатом виде.
А что по времени?
⭐️ А теперь самая главная фишка ZIP - при создании архива он СРАЗУ записывает контрольную сумму файла в заголовки!
А это значит что мы можем просто считать готовые хеш-суммы и сравнить их! Это называется CRC (cyclic redundancy check)
🌐 Полный листинг тестов в Jupyter (для экспериментов жмём Open in Colab)
📌 И просто в Gists
#libs #tricks
Допустим, тестовые файлы мы уже создали используя код из прошлого поста.
Теперь создадим архивы.
import zipfile
def create_zip(archive_path, files):
with zipfile.ZipFile(archive_path, "w") as zf:
for file in files:
zf.write(file)
create_zip('archive1.zip', files_to_archive)
create_zip('archive2.zip', files_to_archive)
Проверим хеш>>> hashlib.md5(open("archive1.zip", "rb").read()).hexdigest()
'd54670be5e01e483797ee4ae30089423'
>>> hashlib.md5(open("archive2.zip", "rb").read()).hexdigest()
'd54670be5e01e483797ee4ae30089423'
Отлично! ZIP создаёт одинаковые архивы и сразу выдаёт одинаковую хеш-сумму! Ну всё, на этом расходимся...
Хотя подождите ка, часто ли вы проверяете один и тот же файл на идентичность?
Давайте имитируем ситуацию когда файл был перезаписан или "модифицирован" но при этом фактически не изменился. То есть изменились только его атрибуты. Для этого можно использовать Linux-команду touch, которая обновляет время последнего доступа к файлу.
touch example_file0.txt
touch example_file1.txt
...
Либо альтернативу на Pythonfrom pathlib import Path
for f in files_to_archive:
Path(f).touch()
Содержимое файлов не изменилось! Но изменились атрибуты. Пересоздаём второй архив.create_zip('archive2.zip', files_to_archive)
Проверяем>>> hashlib.md5(open("archive1.zip", "rb").read()).hexdigest()
'd54670be5e01e483797ee4ae30089423'
>>> hashlib.md5(open("archive2.zip", "rb").read()).hexdigest()
'aa508dbba4e223abe45e16dba4ad6e1f'
Вот это более правдивая ситуация.Давайте теперь сделаем функцию для проверки файлов внутри архивов, которая считывает непосредственно данные файлов в разжатом виде.
def get_hash_zip(path):
hash_md5 = hashlib.md5()
with zipfile.ZipFile(path, "r") as z:
for f_name in z.namelist():
with z.open(f_name) as f:
hash_md5.update(f.read())
return hash_md5.hexdigest()
Сравним теперь хеш-суммы архивов>>> get_hash_zip('archive1.zip')
'0b27c443737b0a84381b827e1d9a913b'
>>> get_hash_zip('archive2.zip')
'0b27c443737b0a84381b827e1d9a913b'
Всё чётко сработало! А что по времени?
>>> timeit.timeit("get_hash_zip('archive1.zip')", number=100, globals=globals())
10.8
Ну тоже неплохо.⭐️ А теперь самая главная фишка ZIP - при создании архива он СРАЗУ записывает контрольную сумму файла в заголовки!
А это значит что мы можем просто считать готовые хеш-суммы и сравнить их! Это называется CRC (cyclic redundancy check)
def get_hash_zip2(path):
h = hashlib.md5()
for info in zipfile.ZipFile(path).infolist():
h.update(info.CRC.to_bytes(8, byteorder='big'))
return h.hexdigest()
>>> timeit.timeit("get_hash_zip2('archive1.zip')", number=100, globals=globals())
0.008
То есть даже буфер никакой не считывается, только несколько байт из заголовков каждого файла. Выполняется моментально. В моем случае 100 итераций за 8 мс! 🌐 Полный листинг тестов в Jupyter (для экспериментов жмём Open in Colab)
📌 И просто в Gists
#libs #tricks
Telegram
Python Заметки
Для проверки целостности или идентичности файлов всегда используется проверка контрольной суммы.
Это работает в большинстве случаев, но не всегда. Давайте сделаем простой тест.
Создадим несколько рандомных файлов
import os
# create random test files
files_to_archive…
Это работает в большинстве случаев, но не всегда. Давайте сделаем простой тест.
Создадим несколько рандомных файлов
import os
# create random test files
files_to_archive…
🔖 Подводя итоги по прошлым постам плюс пара заметок:
🔸 Если требуется проверять идентичность содержимого архивов, лучше использовать ZIP и проверять только CRC.
🔸 Указанный способ может помочь проверить отдельные файлы в архиве с возможностью перекачать только новые а не весь архив
🔸 Для проверки хеш-суммы файла можно использовать утилиту md5sum (она не умеет проверять хеш внутри ахрхивов)
#libs #tricks
🔸 Если требуется проверять идентичность содержимого архивов, лучше использовать ZIP и проверять только CRC.
🔸 Указанный способ может помочь проверить отдельные файлы в архиве с возможностью перекачать только новые а не весь архив
🔸 Для проверки хеш-суммы файла можно использовать утилиту md5sum (она не умеет проверять хеш внутри ахрхивов)
# Linux:(находится в директории скриптов /Tools/scripts/md5sum.py)
md5sum filename
# Windows:
python md5sum.py filename
#libs #tricks