Разобрать файл по битам

Обновлено: 04.07.2024

Файл, содержащий бинарные данные, называется двоичным (бинарным) файлом. Любые форматированные и неформатированные бинарные данные хранятся в бинарных файлах, нечитабельных для человека и использующихся компьютером напрямую.

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

Подготовка

Перед тем, как начать урок, желательно создать один или несколько бинарных файлов, чтобы воспользоваться скриптом из примера. Ниже представлены два скрипта на Python, которые создадут два бинарника. Файл binary1.py создаёт string.bin, содержащий строковые данные, а binary2.py – number_list.bin со списком из числовых данных.

Binary1.py

Binary2.py

Считываем бинарный файл со строковыми данными в массив байтов

В Python существует множество способов прочитать бинарный файл. Можно прочитать определённое количество байтов или весь файл сразу.

Результат

После выполнения скрипта мы получим следующий результат.


Считываем бинарный файл со строковыми данными в массив

Следующий скрипт поможет нам прочитать бинарник number_list.bin, созданный нами ранее.

Бинарный файл содержит список с числовыми данными. Как и в предыдущем примере, функция open() открывает файл и читает из него данные. Затем из бинарника читаются первые 5 чисел и перед выводом объединяются в список.

Результат

После выполнения скрипта мы получим следующий результат. Бинарный файл содержит 7 чисел, первые 5 вывелись на консоль.


Читаем бинарный файл с помощью NumPy

В этой части мы поговорим о том, как создать бинарный файл и прочитать его с помощью массивов NumPy. Перед началом работы необходимо установить модуль NumPy командой в терминале или через ваш редактор Python, в котором вы будете писать программу.

Функция tofile() создаёт текстовый или бинарный файл, а fromfile() считывает данные из файла и создаёт массив.

Синтаксис tofile()

Первый аргумент обязательный – он принимает имя файла, путь или строку. Файл создастся, только если будет указан первый аргумент. Второй аргумент – необязательный, он используется для разделения элементов массива. Третий аргумент также необязателен, он отвечает за форматированный вывод содержимого файла.

Синтаксис fromfile()

Первый аргумент обязательный – он принимает имя файла, путь или строку. Содержимое файла будет прочитано, только если вы укажете имя файла. dtype определяет тип данных в возвращаемом массиве. Count задаёт число элементов массива. Sep – для разделения элементов текста или массива. Offset определяет позицию в файле, с которой начинается считывание. Последний аргумент нужен, чтобы создать массив, не являющийся массивом NumPy.

Напишем следующий код, чтобы создать бинарный файл с помощью массива NumPy, прочитать его и вывести содержимое.

Результат

После выполнения скрипта мы увидим следующий результат.


Заключение

Мы рассмотрели 3 разных способа чтения бинарных файлов. В первом примере мы получили содержимое файла в виде массива байтов, во втором и третьем – в виде списка.

  • Гибкая и быстрая работа напрямую с регистрами микроконтроллера (в том числе для написания библиотек)
  • Более эффективное хранение данных (упаковка нескольких значений в одну переменную и распаковка обратно)
  • Хранение символов и другой информации для матричных дисплеев (упаковка в один байт)
  • Максимально быстрые вычисления
  • Работа со сдвиговыми регистрами и другими подобными железками
  • Разбор чужого кода

Двоичная система и хранение данных

2 в степени DEC BIN
0 1 0b00000001
1 2 0b00000010
2 4 0b00000100
3 8 0b00001000
4 16 0b00010000
5 32 0b00100000
6 64 0b01000000
7 128 0b10000000

Макросы для манипуляций с битами

Макросы Arduino.h Действие
bitRead(value, bit) Читает бит под номером bit в числе value
bitSet(value, bit) Включает (ставит 1) бит под номером bit в числе value
bitClear(value, bit) Выключает (ставит 0) бит под номером bit в числе value
bitWrite(value, bit, bitvalue) Ставит бит под номером bit в состояние bitvalue (0 или 1) в числе value
bit(bit) Возвращает 2 в степени bit
Другие встроенные макросы
_BV(bit) Возвращает 2 в степени bit
bit_is_set(value, bit) Проверка на включенность (1) бита bit в числе value
bit_is_clear(value, bit) Проверка на выключенность (0) бита bit в числе value

Битовые операции

Переходим к более сложным вещам. На самом деле они максимально просты для микроконтроллера, настолько просты, что выполняются за один такт. При частоте 16 МГц (большинство плат Arduino) одна операция занимает 0.0625 микросекунды.

Битовое И

Битовое ИЛИ

Также можно использовать составной оператор |=

Битовое НЕ

Битовая операция НЕ (NOT) выполняется оператором

и просто инвертирует бит:

Также она может инвертировать байт:

Битовое исключающее ИЛИ

Битовая операция исключающее ИЛИ (XOR) выполняется оператором ^ или xor и делает следующее:

Данная операция обычно используется для инвертирования состояния отдельного бита:

То есть мы взяли бит №7 в байте 0b11001100 и перевернули его в 0, получилось 0b01001100, остальные биты не трогали.

Битовый сдвиг

Битовый сдвиг делает не что иное, как умножает или делит байт на 2 в степени. Да, это операция деления, выполняющаяся за один такт процессора! К этому мы ещё вернёмся ниже. Посмотрите на работу оператора сдвига и сравните её с макросами bit() и _BV() :

Включаем-выключаем

Вспомним пример из пункта про битовое ИЛИ, про установку нужного бита. Вот эти варианты кода делают одно и то же:

Как насчёт установки нескольких бит сразу?

Или прицельного выключения бит? Тут чуть по-другому, используя &= и

Выключить несколько бит сразу? Пожалуйста!

Именно такие конструкции встречаются в коде высокого уровня и библиотеках, именно так производится работа с регистрами микроконтроллера. Вернёмся к устройству Ардуиновских макросов:

Я думаю, комментарии излишни: макросы состоят из тех же элементарных битовых операций и сдвигов!

Быстрые вычисления

Примечание: рассмотренные выше операции работают только с целочисленными типами данных!

Экономия памяти

При помощи битовых операций можно экономить немного памяти, пакуя данные в блоки. Например, переменная типа boolean занимает в памяти 8 бит, хотя принимает только 0 и 1. В один байт можно запаковать 8 логических переменных, например вот так:

Хороший трюк, может пригодиться! Я сделал удобную библиотеку для хранения битовых флагов, документация и примеры есть здесь.

Ещё интересный пример сжатия

Таким образом можно сжимать, разжимать и просто хранить маленькие данные в стандартных типах данных. Давайте ещё пример: нужно максимально компактно хранить несколько чисел в диапазоне от 0 до 3, то есть в бинарном представлении это 0b00 , 0b01 , 0b10 и 0b11 . Видим, что в один байт можно запихнуть 4 таких числа (максимальное занимает два бита). Запихиваем:

Как и в примере со светодиодами, мы просто брали нужные биты ( в этом случае младшие два, 0b11 ) и сдвигали их на нужное расстояние. Для распаковки делаем в обратном порядке:

И получим обратно наши байты. Также маску можно заменить на более удобную для работы запись, задвинув 0b11 на нужное расстояние:

Ну и теперь, проследив закономерность, можно сделать для себя функцию или макрос чтения пакета:

На битовых операциях можно сделать очень много всего интересного, и работать оно будет очень быстро и занимать мало места. Огромный список битовых трюков и хаков можно посмотреть в этой статье, их там очень много и все с примерами. Есть ещё один небольшой сборник самых простых и полезных хаков вот здесь (английский). Его я перевёл, смотрите ниже под спойлером. Другой вариант перевода (могут быть не все трюки) можно посмотреть здесь.

Перемотка бита

Забавный алгоритм, может пригодиться: перематывает один бит слева направо, то есть формирует последовательность 0b10000000, 0b01000000, 0b00100000, 0b00010000, 0b00001000, 0b00000100, 0b00000010, 0b00000001, 0b10000000 , или 128, 64, 32, 16, 8, 4, 2, 1, 128

Целые

Установка n го бита

Выключение n го бита

Инверсия n го бита

Округление до ближайшей степени двойки

Округление вниз

Получение максимального целого

Получение минимального целого

Получение максимального long

Умножение на 2

Деление на 2

Умножение на m ую степень двойки

Деление на m ую степень двойки

Остаток от деления

Проверка равенства

Проверка на чётность (кратность 2)

Обмен значениями

Получение абсолютного значения

Максимум из двух

Минимум из двух

Проверка на одинаковый знак

Смена знака

Вернёт 2 n

Является ли число степенью 2

Остаток от деления на 2 n на m

Среднее арифметическое

Получить m ый бит из n (от младшего к старшему)

Получить m ый бит из n (от старшего к младшему)

Проверить включен ли n ый бит

Выделение самого правого включенного бита

Выделение самого правого выключенного бита

Выделение правого включенного бита

Выделение правого выключенного бита

n + 1

Получение отрицательного значения

if (x == a) x = b; if (x == b) x = a;

Поменять смежные биты

Different rightmost bit of numbers m & n

Common rightmost bit of numbers m & n

Десятичные дроби

Примечание: хаки с float могут не работать на Ардуино! Разбить float в массив бит (unsigned uint32_t)

Он читает только байт за байтом, моя цель - иметь возможность читать 12 бит за раз, а затем переносить их в массив. Любая помощь или указатели были бы очень признательны!

Добавляя к первому комментарию, вы можете попробовать читать по одному байту (объявить переменную типа char и записать туда), а затем использовать побитовые операторы >> и 7

Прочитать 2 байта и выполнить побитовые операции, чтобы в следующий раз прочитать 2 байта и далее применить побитовые операции, которые вернутся, как вы ожидали. . . .

Прочтите первые два байта из указателя файла a_file и проверьте биты в наименьшем или наибольшем байте - в зависимости от endianness вашей платформы (x86 - little-endian) - с использованием операторов битового сдвига.

Вы не можете поместить биты в массив, так как для битов нет типа данных. Вместо того, чтобы хранить единицы и нули в массиве, что неэффективно, кажется дешевле просто сохранить два байта в двухэлементном массиве (скажем, типа unsigned char * ) и написать функции для сопоставления этих двух байтов с одним из 4096 (2 ^ 12) интересующих значений.

В качестве осложнения при последующих чтениях, если вы хотите fread проходить через указатель каждые 12 бит, вы должны читать только один байт, используя оставшиеся биты от предыдущего чтения для построения новое 12-битное значение. Если остатков нет, вам нужно будет прочитать два байта.

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

Много лет назад я написал несколько процедур ввода-вывода на языке C для кодировщика Хаффмана. Это должно иметь возможность читать и писать с детализацией битов, а не байтов. Я создал функции, аналогичные read (2) и write (2), которые можно попросить (скажем) прочитать 13 бит из потока. Для кодирования, например, байты будут подаваться в кодер, а переменное количество битов будет выходить на другой стороне. У меня была простая структура с битовым указателем на текущий читаемый или записываемый байт. Каждый раз, когда он выходил за пределы, он сбрасывал завершенный байт и сбрасывал указатель на ноль. К сожалению, этого кода давно нет, но, возможно, стоит разобрать кодер Хаффмана с открытым исходным кодом и посмотреть, как там решается проблема. Точно так же кодирование base64 берет 3 байта данных и превращает их в 4 (или наоборот).

Я реализовал несколько методов для побитового чтения / записи файлов. Вот они. Независимо от того, жизнеспособно это или нет для вашего варианта использования, вы должны решить это сами. Я пытался сделать максимально читаемый, оптимизированный код, который мог, не будучи опытным разработчиком C (на данный момент).

Внутри он использует «bitCursor» для хранения информации о предыдущих битах, которые еще не соответствуют полному байту. В нем есть поля данных who: d хранит данные, а s хранит размер или количество битов, хранящихся в курсоре.

У вас есть четыре функции:

  • newBitCursor (), который возвращает объект bitCursor со значениями по умолчанию
    . Такой курсор нужен в начале последовательности операции чтения / записи в файл или из файла.
  • fwriteb (void * ptr, size_t sizeb, size_t rSizeb, FILE * fp, bitCursor * c), который записывает крайние правые биты sizeb значения, хранящегося в ptr, в fp.
  • fcloseb (FILE * fp, bitCursor * c), который записывает оставшийся байт, если предыдущие записи точно не инкапсулировали все данные, необходимые для быть написано, что это, вероятно, почти всегда так .
  • freadb (void * ptr, size_t sizeb, size_t rSizeb, FILE * fp, bitCursor * c), который считывает биты sizeb и побитовое ИЛИ преобразует их в * ptr. (поэтому вы обязаны инициализировать * ptr как 0 )

Больше информации в комментариях. Веселиться!

Изменить: Мне стало известно сегодня, когда я сделал это, я принял Little Endian! : P Ой! Всегда приятно осознавать, насколько я новичок; D.

Для вашей проблемы вы можете увидеть эту демонстрационную программу, которая читает 2 байта, но фактическая информация составляет только 12 бит. Также как этот тип вещей используется, это побитовый доступ.

Fwrite () - это стандартная библиотечная функция , которая принимает аргумент размера в виде байта и типа int. Таким образом, чтение с точностью до 12 бит невозможно. Если файл, который вы создаете, создайте, как показано ниже, а также прочитайте как ниже решите вашу проблему.

Если этот файл является специальным файлом, который не был написан вами, то следуйте стандарту, предусмотренному для этого файла для чтения, я думаю, они также пишут только так. Или вы можете предоставить axact, где я могу вам помочь.

В 18 уровне начались первые задачи побайтного чтения файлов: прочитать файл, далее найти минимальные/максимальные байты или вывести в упорядоченном виде и т.п.

Побайтовая работа с файлами - 1

  • Ввести с консоли имя файла
  • Считать все байты из файла.
  • Не учитывая повторений - отсортировать их по байт-коду в убывающем порядке.
  • Вывести на экран
  • Закрыть поток ввода-вывода

Решаем в лоб:

Решает все замечательно! Тест (если бы был — прошелся бы на ура). Но в жизни мало файлов содержащих только строчку "Мама мыла раму". Давайте скормим нашей программе файл в 46Мб (по нынешним меркам вроде и не особо много). Что такое, программа выполняется 220 секунд. Попытка скормить с вечера 1Gb файл (размер MPEG4 фильма не в самом лучшем качестве) не увенчалась успехом. Программа утром все еще читала - а мне идти на работу уже. В чем проблема? Наверное в использовании ArrayList<Integer> у которого внутри 1 миллиард элементов. Каждый элемент его занимает 16 байт минимум (Заголовок: 8 байт + Поле int: 4 байта + Выравнивание для кратности 8: 4 байта). Итого мы добровольно загоняем в память 16 Gb данных при размере оперативы в 8. Будем делать лучше. Нырнем в коллекции глубже. И ура, нашлось то, что нам нужно.

Встречаем TreeSet

  • не допускает хранение двух одинаковых элементов (а значит мы будем хранить в памяти все 255 элементов, вместо миллиарда!)
  • при манипуляциях со своими элементами автоматом упорядочивает (само сортирует - вот он, верх совершенства!)

Массив — побайтно

Имеем на выходе: 46Мб файл 158 секунд. 1Gb файл - 2 часа 55 минут. Опять улучшение, но небольшое. И мы сделали все простыми инструментами. Не использовали микроскоп для забивания гвоздей. Теперь лирическое отступление. Вспомним устройство компьютера. Память ОЗУ (DRAM) где обычно выполняется программа и хранятся переменные имеет высокую скорость доступа, но небольшой размер. Память на жестком/flash диске (HDD или Flash-накопители) где обычно хранятся файлы, наоборот имеет низкую скорость доступа, но большой размер. Так что когда мы побайтно читаем 1Gb файл (то есть миллиард раз обращаемся к HDD) - мы тратим много времени на работу с низкоскоростным устройством (по песчинке перекладываем песок с кузова КамАЗа в песочницу). Попробуем еще улучшить.

Читайте также: