php побайтовое чтение файла
Как прочитать большой файл средствами PHP (не грохнув при этом сервак)
PHP разработчикам не так уж часто приходится следить за расходом памяти в своих приложениях. Сам движок PHP неплохо подчищает мусор за нами, да и модель веб-сервера с контекстом исполнения, «умирающим» после выполнения каждого запроса, позволяет даже самому плохому коду не создавать больших долгих проблем.
Однако, в некоторых ситуациях, мы можем столкнуться с проблемами нехватки оперативной памяти — например, пытаясь запустить композер на маленьком VPS, или при открытии большого файла на сервере не богатом ресурсами.
Последняя проблема и будет рассмотрена в этом уроке.
Мерила Успеха
При проведении любых оптимизаций кода, мы всегда должны замерять результаты его выполнения до и после, для того чтобы оценивать эффективность(или пагубность) наших оптимизаций.
Обычно измеряют загрузку CPU и использование оперативной памяти. Часто бывает, что экономия одного, ведёт к увеличенным затратам другого и наоборот.
В асинхронной модели приложения(мультипроцессорные и многопоточные) всегда очень важно следить как за процессором, так и за памятью. В классических приложениях контроль ресурсов становится проблемой лишь при приближении к лимитам сервера.
Измерять использование CPU внутри PHP плохая идея. Лучше использовать какую-либо утилиту, как top из Ubuntu или macOS. Если вы у вас Windows, то можно использовать Linux Subsystem, чтобы иметь доступ к top.
В этом уроке мы будем измерять использование памяти. Мы посмотрим, как память расходуется в традиционных скриптах, а затем применим парочку фишек для оптимизации и сравним результаты. Надеюсь, к концу статьи, читатель получит базовое понимание основных принципов оптимизации расхода памяти при чтении больших объемов данных.
Будем замерять память так:
Эту функцию мы будем использовать в конце каждого скрипта, и сравнивать полученные значения.
Какие есть варианты?
Существует много разных подходов для эффективного чтения данных, но всех их условно можно разделить на две группы: мы либо считываем и сразу же обрабатываем считанную порцию данных(без предварительной загрузки всех данных в память), либо вовсе преобразуем данные в поток, не заморачиваясь над его содержимым.
Давайте представим, что для первого варианта мы хотим читать файл и отдельно обрабатывать каждые 10000 строк. Нужно будет держать по крайней мере 10000 строк в памяти и передавать их в очередь(в какой бы форме она не была реализована).
Для второго сценария, предположим, мы хотим сжать содержимое очень большого ответа API. Нам не важно, что за данные там содержатся, важно вернуть их в сжатой форме.
В обоих случаях нужно считать большие объемы информации. В первом, нам известен формат данных, во втором, формат значения не имеет. Рассмотрим оба варианта.
Чтение Файла Строка За Строкой
Есть много функций для работы с файлами. Давайте напишем с их помощью свой ридер:
Тут мы считываем файл с работами Шекспира. Размер файла около 5.5MB и пиковое использование памяти 12.8MB.
А теперь, давайте воспользуемся генератором:
Файл тот же, а пиковое использование памяти упало до 393KB! Но пока мы не выполняем со считываемыми данными никаких операций, это не имеет практической пользы. Для примера, мы можем разбивать документ на части, если встретим две пустые строки:
Хотя мы разбили документ на 1,216 кусков, мы использовали лишь 459KB памяти. Всё это, благодаря особенности генераторов — объем памяти для их работы равен размеру самой большой итерируемой части. В данном случае, самая большая часть состоит из 101,985 символов.
Генераторы могут применяться и в других ситуациях, но данный пример хорошо демонстрирует производительность при чтении больших файлов. Возможно, генераторы один из лучших вариантов для обработки данных.
Пайпинг между файлами
В ситуациях, когда обработка данных не требуется, мы можем пробрасывать данные из одного файла в другой. Это называется пайпингом( pipe — труба, возможно потому что мы не видим что происходит внутри трубы, но видим что входит и выходит и неё). Это можно сделать с помощью потоковых методов. Но сперва, давайте напишем классический скрипт, который тупо передает данные из одного файла в другой:
Неудивительно, что этот скрипт использует намного больше памяти, чем занимает копируемый файл. Это связано с тем, что он должен читать и хранить содержимое файла в памяти до тех пор пока файл не будет скопирован полностью. Для маленьких файлов в этом нет ничего страшного, но не для больших.
Давайте попробуем стримить(или пайпить) файлы, один в другой:
Код довольно странный. Мы открываем оба файла, первый на чтение, второй на запись. Затем мы копируем первый во второй, после чего закрываем оба файла. Возможно будет сюрпризом, но мы потратили всего 393KB.
Для того чтобы осуществить задуманное этим способом потребовалось 581KB. Теперь попробуем сделать то же самое с помощью потоков.
Потратили немного меньше памяти(400KB) при одинаковом результате. А если б нам не нужно было сохранять картинку в памяти, мы могли бы сразу застримить её в stdout :
Другие потоки
Существуют и другие потоки, в/из которых можно стримить:
Фильтры
Есть еще одна фишка, которую мы можем использовать — это фильтры. Промежуточный вариант, который дает нам немного контроля над потоком, без необходимости детально погружаться в его содержимое. Допустим, мы хотим сжать файл. Можно применить zip extension:
Хороший код, но он потребляет почти 11MB. С фильтрами, получится лучше:
Здесь мы используем php://filter/zlib.deflate который считывает и сжимает входящие данные. Мы можем пайпить сжатые данные в файл, или куда-нибудь еще. Этот код использовал лишь 896KB.
Я знаю что это не совсем тот же формат, что и zip архив. Но задумайтесь, если у нас есть возможность выбрать иной формат сжатия, затратив в 12 раз меньше памяти, стоит ли это делать?
Чтобы распаковать данные, применим другой zip фильтр.
Вот парочка статей, для тех кому хотелось бы поглубже погрузиться в тему потоков: “Understanding Streams in PHP” и“Using PHP Streams Effectively”.
Кастомизация потоков
fopen и file_get_contents имеют ряд предустановленных опций, но мы можем менять их как душе угодно. Чтобы сделать это, нужно создать новый контекст потока:
В этом примере мы пытаемся сделать POST запрос к API. Прописываем несколько заголовков, и обращаемся к API по файловому дескриптору. Существует много других опций для кастомизации, так что не будет лишним ознакомиться с документацией по этому вопросу.
Создание своих протоколов и фильтров
Перед тем как закончить, давайте поговорим о создании кастомных протоколов. Если посмотреть в документацию, то можно увидеть пример:
Написание своей реализации такого тянет на отдельную статью. Но если все же озадачиться и сделать это, то можно будет легко зарегистрировать свою обертку для стримов:
Аналогичным образом, можно создать и кастомные фильтры потока. Пример класса фильтра из доков:
И его также легко зарегистрировать:
Хотя это не самая частая проблема, с которой мы мучаемся, очень легко накосячить при работе с большими файлами. В асинхронных приложениях, вообще очень просто положить весь сервер, если не контролировать использование памяти в своих скриптах
Надеюсь, что этот урок подарил вам несколько новых идей(или освежил их в памяти) и теперь вы сможете работать с большими файлами гораздо эффективнее. Познакомившись с генераторами и потоками( и перестав использовать функции по типу file_get_contents ) можно избавить наши приложения от целого класса ошибок. That seems like a good thing to aim for!
Чтение и запись данных в файле
При изучении данной темы нужно учитывать, что для обращения к файлу одни PHP функции используют дескриптор файла, а другие путь к файлу. Для получения дескриптора нужно открыть файл. Это рассматривалось в предыдущей теме.
Чтение из файла
Функцию fread() можно использовать для получения определённого количества данных из файла.
Если конец файла будет достигнут раньше, чем функция прочитает указанный размер данных, то она вернёт строку меньшего размера. Данную функцию удобно применять для чтения бинарных файлов.
При указании длины строки и при перемещении указателя нужно учитывать, что русские буквы имеют размер не один байт, а больше. Подробнее об этом сказано в теме «Работа со строками». Также не забудте, что в начале файла в кодировке UTF-8 есть несколько байт. В кодировке UTF-8 без BOM эти байты отсутствуют.
Прочитаем перыве 10 байт из файла myfile.txt. Откроем его в режиме ‘r+’. Указатель будет находится в начале.
Если в файле присутствуют русские буквы, то последняя буква может отображаться неправильно. Это происходит потому, что символ имеет размер больше одного байта и одна часть прочитана, а вторая нет.
Функция fgets() возвращает одну строку, начиная от указателя до конца строки.
Если длина не указана, то в старых версиях PHP максимальная длина возвращаемой строки была ограничена. В более новых версиях этого ограничения нет.
Запуск функции в цикле позволяет произвести чтение файла построчно.
Прочитаем одну строку строку из файла myfile.txt. Так как указатель уже перемещён, то будет прочитана не вся строка, а от указателя.
Функция fgetss() также возвращает одну строку, но удаляет из неё HTML теги. Если в коде присутствует PHP скрипт, то он тоже будет удалён.
fgetss (файл, длина, нужные тэги)
Функция file() читает весь файл и помещает данные в массив. Каждая строка помещается в элемент массива. Для работы этой функции не требуется открывать файл. Расположение указателя не учитывается.
file (путь к файлу, флаги, контекст)
Можно указать такие флаги:
Если указывается несколько флагов, то они разделяются оператором «|».
Выведем файл myfile.txt на страницу.
Запись в файл
Функция fwrite() записывает данные в файл.
fwrite (файл, данные, длина)
Если указатель находится в начале или середине файла, функция заменяет символы, находящиеся в файле, на новые. То есть, если записывается 5 символов, то из файла удаляются 5 символов и на их место добавляются новые.
Когда вы записываете русские буквы, то кодировка скрипта должна соответствовать кодировке файла.
Запишем в файл строку «текст из скрипта». В нашем примере дескрептор находится в середине файла и строка запишется туда. Когда нужно добавить данные в конец файла, то его можно открыть в режиме ‘a’. А когда нужно перезаписать файл, его открывают в режиме ‘w’. Мы не будем передвигать указатель, запишем данные туда, где он находится.
Коприрование материалов сайта возможно только с согласия администрации
Использование fread () в PHP
Он может принимать два аргумента и возвращает конкретное содержимое файла в виде строки. Первый аргумент принимает обработчик любого открытого файла, а второй аргумент принимает длину байтов в качестве числа, которое будет читать. Значение аргумента может быть больше исходного размера файла.
Создать текстовый файл
Создайте текстовый файл с разделителями-запятыми с именем employee.txt со следующим содержимым. Затем функция fread() используется для чтения этого текстового файла различными способами в следующей части этого руководства.
Пример 1. Прочтите конкретное содержимое текстового файла.
В следующем примере показано, как конкретное содержимое файла можно прочитать с помощью функции fread(). Функция fopen() используется в скрипте, чтобы открыть файл employee.txt для чтения. Затем функция fread() используется для чтения первых 30 байтов текстового файла, который будет напечатан позже.
Следующий вывод появится после запуска сценария с сервера. В выходных данных показаны первые 30 байтов файла employee.txt.
Пример 2: прочитать полное содержание текстового файла
В следующем примере показано, как полное содержимое текстового файла можно прочитать с помощью функции fread(). Как и в предыдущем примере, функция fopen() используется для открытия файла employee.txt для чтения. Файл() используется в скрипте для определения общего размера открываемого текстового файла. Функция fread() считывает полное содержимое файла, когда значение общего размера файла передается в качестве второго аргумента этой функции.
Следующий вывод появится после запуска сценария с сервера. В выходных данных отображается полное содержимое файла employee.txt.
Пример 3: прочитать содержимое двоичного файла
В следующем примере показано, как можно прочитать файл изображения с помощью функции fread(). Здесь функция fopen() используется для открытия двоичного файла с именем flower.jpeg для чтения. Функция fread() используется с функцией Size() для чтения полного содержимого двоичного файла. Функция base64_encode() используется в скрипте для преобразования содержимого двоичного файла в удобочитаемый формат. Затем тег используется для печати изображения.
Если файл flower.jpeg существует в текущем месте, изображение будет отображаться как вывод.
Пример 4: прочитать файл по внешней ссылке
Существующие локальные текстовые и двоичные файлы используются в трех предыдущих примерах. В следующем примере показано, как можно прочитать файл по внешней ссылке с помощью функции fread(). Внешний текстовый файл открывается для чтения с помощью функции fopen(). Первая функция fread() используется для чтения 1024 байтов из внешнего текстового файла. Затем с помощью функции fopen() открывается внешняя ссылка на двоичный файл для чтения. Вторая функция fread() используется для чтения 10000 байтов из внешнего двоичного файла. Двоичный контент преобразуется в удобочитаемый формат, как в предыдущем примере, и распечатывает изображение.
Работа с файлами в PHP
Прочитав эту статью, вы узнаете, как создавать, получать доступ и управлять файлами на веб-сервере с помощью функции PHP fread и другими.
Открытие файла с помощью функции PHP fopen()
Файл может быть открыт в одном из следующих режимов:
Режим | Что он делает |
R | Открывает файл только для чтения. |
r+ | Открывает файл для чтения и записи. |
W | Открывает файл только для записи и очищает содержимое файла. Если файл не существует, PHP попытается его создать. |
w+ | Открывает файл для чтения и записи и очищает содержимое файла. Если файл не существует, PHP попытается его создать. |
A | Открывает файл только для записи. Сохраняет содержимое файла, записывая его в конец файла. Если файл не существует, PHP попытается его создать. |
a+ | Открывает файл для чтения и записи. Сохраняет содержимое файла, записывая его в конец файла. Если файл не существует, он будет создан. Можно использовать вместо PHP fread () |
X | Открывает файл только для записи. Возвращает значение FALSE и генерирует ошибку, если файл уже существует. Если файл не существует, PHP попытается его создать. |
x+ | Открывает файл для чтения и записи; в противном случае имеет такое же поведение, как «x». |
Если вы попытаетесь открыть файл, который не существует, PHP сгенерирует сообщение с предупреждением. Чтобы избежать этого, необходимо выполнять простую проверку, существует ли файл или нет, прежде чем пытаться получить к нему доступ. Это делается с помощью функции PHP file_exists() :
Закрытие файла
Чтение из файлов с помощью функции PHP fread()
Теперь вы знаете, как открывать и закрывать файлы. В следующем разделе вы узнаете, как читать данные из файла. PHP включает в себя сразу несколько функций для чтения данных из файла. За одну операцию можно считать, как один символ, так и весь файл.
Чтение фиксированного количества символов
Функция PHP fread() может использоваться для чтения заданного количества символов из файла. Базовый синтаксис функции:
Приведенный выше пример даст следующий результат:
Чтение всего содержимого файла
Функция fread() может использоваться в связке с функцией filesize() для полного считывания всего файла. filesize() возвращает размер файла в байтах. PHP fread пример:
Приведенный выше пример даст следующий результат:
Приведенный выше пример даст следующий результат:
Запись файлов с использованием функции PHP fwrite()
Функция fwrite() принимает два параметра: дескриптор файла и строку данных, которые должны быть записаны:
Переименование файлов с помощью функции PHP rename()
Удаление файлов с помощью функции PHP unlink()
Функции файловой системы PHP
В приведенной ниже таблице представлен обзор других полезных функций, которые можно использовать для динамического чтения и записи файлов ( альтернатива PHP fread() и другим функциям ):
Двоичные и побитовые операции в PHP
Недавно я обратил внимание, что в разных проектах мне приходится активно писать побитовые операции на PHP. Это очень интересное и полезное умение, которое пригодится начиная с чтения двоичных файлов до эмуляции процессоров.
В PHP есть много инструментов, помогающих манипулировать двоичными данными, но хочу сразу предупредить: если вам нужно супернизкоуровневая эффективность, то этот язык не для вас.
Почему PHP может оказаться не лучшим кандидатом
Я люблю PHP, не поймите меня неправильно. И я уверен, что этот язык будет прекрасно работать в большинстве случаев. Но если вам нужна максимальная эффективность обработки двоичных данных, то PHP не потянет.
Поясню: я не говорю о том, что приложение может потреблять на пять или десять мегабайт больше, а о выделении конкретного количества памяти для хранения данных определённого типа.
Согласно официальной документации о целых числах, PHP представляет десятичные, шестнадцатеричные, восьмеричные и двоичные значения с помощью целочисленного типа (integer). Так что не имеет значения, какие данные вы туда положите, они всегда будут целочисленными.
Суть вот в чём: не имеет значения, нужно ли вам хранить 0xff, 0xffff, 0xffffff или что-то другое. В PHP все эти значения будут храниться как long (lval) с длиной 32 или 64 бита.
К примеру, недавно я экспериментировал с эмуляцией микроконтроллеров. И хотя необходимо корректно обрабатывать содержимое памяти и операции, мне не требовалось слишком большой эффективности использования памяти, потому что моя хостинговая машина компенсировала расходы на порядки.
Конечно, всё меняется, если мы говорим о С-расширениях или FFI, но это и не входит в мои цели. Я рассказываю о чистом PHP.
Поэтому помните: он работает и может вести себя так, как вам нужно, но в большинстве случаев типы будут расходовать память неэффективно.
Быстрое введение в двоичное и шестнадцатеричное представление данных
Прежде чем разговаривать о том, как PHP обрабатывает двоичные данные, нужно сначала поговорить о том, что такое двоичность. Если вы думаете, что уже всё знаете об этом, то переходите к главе Двоичные числа и строки в PHP.
В математике есть понятие «основание». Оно определяет, как мы можем представлять количества в разных форматах. Люди обычно используют десятичное основание (основание 10), что позволяет нам представлять любое число с помощью цифр 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9.
Чтобы пояснить следующий пример, я буду называть число 20 как «десятичное 20».
Двоичные числа (основание 2) могут представлять любое число, но только с помощью двух цифр: 0 и 1.
Десятичное 20 в двоичной форме выглядит так: 0b00010100. Вам не нужно преобразовывать его в привычный вид самостоятельно, пусть это делают компьютеры. 😉
Шестнадцатеричные числа (основание 16) могут представлять любые числа с помощью десяти цифр 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9, а также дополнительных шести символов из латинского алфавита: a, b, c, d, e и f.
Десятичное 20 в шестнадцатеричной форме выглядит так: 0x14. Его преобразование тоже возложите на компьютеры, они в этом эксперты!
Важно понимать, что числа можно представлять по разным основаниям: двоичному (основание 2), восьмеричному (основание 8), десятичному (основание 10, наше обычное) и шестнадцатеричному (основание 16).
В PHP и многих других языках двоичные числа пишутся как и любые другие, но с префиксом 0b: десятичное 20 выглядит как 0b00010100. Шестнадцатеричные числа получают префикс 0x: десятичное 20 выглядит как 0x14.
Как вы уже можете знать, компьютеры не хранят литеральные данные. Они всё представляют в виде двоичных чисел, нулей и единиц. Символы, цифры, буквы, инструкции — всё представлено по основанию 2. Буквы являются лишь условностью числовых последовательностей. Например, буква «a» имеет номер 97 в ASCII-таблице.
Но хотя всё хранится в двоичном виде, программистам удобнее всего читать данные в шестнадцатеричном формате. Они так лучше выглядят. Вы только посмотрите:
Хотя двоичный формат визуально занимает много места, шестнадцатеричные данные очень похожи на двоичное представление. Поэтому обычно мы используем их в низкоуровневом программировании.
Операции переноса
Вы уже знакомы с концепцией переноса (carry), но я должен уделить ей внимание, чтобы мы могли использовать её с разными основаниями.
В десятичном наборе у нас есть десять отдельных цифр для представления чисел, от 0 до 9. Но когда мы пытаемся представить числе больше девяти, нам не хватает цифр! И тут применяется операция переноса: мы делаем для числа префикс из цифры 1, а правую цифру сбрасываем в 0.
Двоичное основание ведёт себя так же, только оно ограничено цифрами 0 и 1.
То же самое и с шестнадцатеричным основанием, только у него диапазон гораздо шире.
Как вы поняли, для операции переноса нужно больше цифр для представления определённых чисел. Это позволяет нам понять, как ограничены определённые типы данных и, поскольку они хранятся в компьютерах, как ограничено их представление в двоичной форме.
Представление данных в памяти компьютера
Как я упоминал выше, компьютеры всё хранят в двоичном формате. То есть они содержат в памяти только нули и единицы.
Проще всего визуализировать эту концепцию в виде большой таблицы из одной строки и множества колонок (столько, сколько позволяет ёмкость памяти. Каждая колонка представляет собой двоичное число (бит).
Представление нашего десятичного 20 в такой таблице с помощью 8 бит выглядит так:
Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Бит | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Беззнаковое 8-битное целое — это число, которое можно представить максимум с помощью 8 двоичных чисел. То есть 0b11111111 (десятичное 255) будет самым большим среди беззнаковых 8-битных чисел. Добавление к нему 1 потребует применения операции переноса, что уже нельзя представить с помощью того же количества цифр.
Зная это, мы можем легко разобраться, почему для чисел существует так много представлений в памяти и что они собой представляют: uint8 — это беззнаковые 8-битные целочисленные (десятичные 0—255), uint16 — беззнаковые 16-битные целочисленные (десятичные 0—65535). Есть также uint32, uint64 и, теоретически, более высокие.
Знаковые целые числа, которые могут представлять отрицательные значения, обычно используют последний бит для определения положительности (последний бит = 0) или отрицательности (последний бит = 1). Как вы понимаете, они позволяют хранить в том же объёме памяти более маленькие значения. Знаковое 8-битное целочисленное варьируется от —128 до десятичного 127.
Вот десятичное —20, представленное в виде знакового 8-битного целочисленного. Обратите внимание, что задан первый бит (адрес 0, значение 1), это означает отрицательное число.
Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Бит | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Надеюсь, пока всё понятно. Это введение очень важно для понимания внутренней работы компьютеров. Помните об этом, и тогда всегда будете понимать, как PHP работает под капотом.
Арифметические переполнения
Выбранное представление числа (8-битное, 16-битное) определяет минимальное и максимальное значение диапазона. Всё дело в том, как числа хранятся в памяти: добавление 1 к двоичной цифре 1 приводит к операции переноса, то есть нужен другой бит в качестве префикса для текущего числа. Поскольку целочисленный формат очень тщательно определён, мы не можем полагаться на операции переноса, выходящие за заданные пределы (на самом деле это возможно, но довольно безумно).
Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Бит | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
Здесь мы очень близки к 8-битному пределу (десятичному 255). Если мы добавим единицу, то получим десятичное 255 в двоичном представлении:
Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Бит | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Все биты назначены! Добавление 1 потребует операции переноса, которая будет невозможна, потому что у нас не хватает битов, все 8 уже назначены! Эта ситуация называется переполнением, мы выходим за какой-то предел. Двоичная операция 255 + 2 должна дать 8-битный результат 1.
Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Бит | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Такое поведение не случайно, новое значение вычисляется с помощью определённых правил, которые мы не будем здесь рассматривать.
Двоичные числа и строки в PHP
Вернёмся к PHP! Извините за этот большой экскурс, но я считаю его важным.
Надеюсь, у вас в голове уже начали собираться кусочки мозаики: двоичные числа, в каком виде они хранятся, что такое переполнение, как PHP представляет числа…
Десятичное 20, представленное в PHP в виде целочисленного значения, в зависимости от платформы может иметь два разных представления. На х86-платформе это будет 32-битное представление, на х64 — 64-битное, но в обоих случаях будет стоять знак (то есть значение может быть отрицательным). Мы знаем, что десятичное 20 может поместиться в 8-битное пространство, но PHP обращается с любым десятичным числом как с 32- или 64-битным.
Также в PHP есть двоичные строки, которые можно преобразовывать туда-обратно с помощью функций pack() и unpack().
В PHP главное отличие между двоичными строками и числами в том, что строки просто содержат данные, как буфер. Целочисленные значения (двоичные и не только) позволяют выполнять с собой арифметические операции, но и двоичные (побитовые), такие как AND, OR, XOR и NOT.
Двоичность: что использовать в PHP, числа или строки?
Для транспортировки данных мы обычно используем двоичные строки. Поэтому чтение двоичного файла или сетевое взаимодействие требует упаковки и распаковки двоичных строк.
Однако фактические операции, такие как OR и XOR, со строковыми не получится выполнять надёжно, поэтому нужно использовать числа.
Отладка двоичных значений в PHP
Теперь давайте развлечёмся и немного поиграем с PHP-кодом!
Сначала я покажу, как визуализировать данные. Надо ведь понять, с чем мы имеем дело.
Отлаживать целые числа очень-очень просто, мы можем использовать функцию sprintf(). У неё очень мощное форматирование, и она поможет нам быстро понять, с какими значениями мы работаем.
Давайте представим десятичное 20 в 8-битном двоичном формате и в 1-байтном шестнадцатеричном:
Визуализация двоичных строк
Хотя в PHP целые числа всегда длиной 32 или 64 бита, длина строк равна длине их содержимого. Чтобы декодировать их двоичные значения и визуализировать их, нам нужно исследовать и преобразовать каждый байт.
К счастью, в PHP строки не являются именоваными, как массивы, и каждая позиция указывает на символ размером в 1 байт. Вот пример обращения к символам:
Если считать, что один символ занимает 1 байт, мы можем вызвать функцию ord() для приведения к 1-байтному целому числу:
Теперь можно выполнить двойную проверку с помощью приложения для командной строки hexdump:
Также при обработке двоичных строк мы можем использовать функции pack() и unpack(), и у меня есть для вас отличный пример! Допустим, вам нужно прочитать JPEG-файл, чтобы извлечь какие-нибудь данные (например, EXIF). С помощью режима чтения двоичных данных можно открыть обработчик файла и сразу же прочитать первые два байта:
Чтобы извлечь значения в целочисленный массив, можно просто распаковать их:
Побитовые операции
PHP реализует все побитовые операции, какие вам могут понадобиться. Они встроены в качестве выражений, а результат их работы описан ниже:
Я объясню работу каждого из них!
Как работает NOT (
Операции NOT требуется один параметр, она просто меняет значения всех переданных битов. Все 0 она превращает в 1, а все 1 — в 0.:
То же самое со смещением вправо:
Что такое битовая маска
С этими операциями и прочими методиками можно сделать много интересного. Например, применить битовую маску. Так называется произвольное двоичное число на ваш выбор, созданные для извлечения очень специфической информации.
Такое часто бывает нужно при работе с флагами. Можно даже найти примеры использования в самом PHP, например, флаги сообщения об ошибках.
Можно выбрать, какого рода ошибки будут выдаваться:
Что здесь происходит? Просто посмотрите на своё значение:
Когда PHP видит уведомление, которое можно передать, он проверяет нечто подобное:
И вы увидите это везде! Двоичные файлы, процессоры, всякие низкоуровневые вещи!
Нормализация чисел
В PHP есть одна особенность, связанная с обработкой двоичных чисел: целые числа имеют размер 32 или 64 бита. Это означает, что зачастую нам нужно нормализовать их, чтобы доверять своим вычислениям.
Например, исполнение этой операции на 64-битной машине даст странный (но ожидаемый) результат:
Что тут произошло? Операция NOT в 8-битном целом числе ( 0x20 ) превратила все нулевые биты в единицы. Угадайте, что у нас было нулями? Правильно, все остальные 56 битов слева, которые до этого игнорировались!
Повторюсь, причина в том, что в PHP длина целых чисел составляет 32 или 64 бита, вне зависимости от их значений!
Однако код работает ожидаемо. Например, результатом операции
0x20 & 0b11011111 === 0b11011111 будет булево значение (true). Но не забывайте, что эти биты слева никуда не деваются, иначе вы получите странное поведение кода.
Для решения этой проблемы можно нормализовать числа, применив битовую маску, которая очищает все нули. Например, для нормализации
0x20 в 8-битное целое число нужно применить AND с 0xFF ( 0b11111111 ), чтобы все предыдущие 56 битов превратились в нули.
Внимание! Не забывайте о том, что содержится в ваших переменных, иначе получите неожиданное поведение. Например, давайте взглянем, что произойдёт, когда мы смещаем вышеописанное значение вправо без 8-битной маски:
Поясню: с точки зрения PHP это является ожидаемым, потому что вы явно обрабатываете 64-битное число. Вы должны понимать, что ожидает ВАША программа.
Совет: избегайте подобных глупых ошибок, программируя в парадигме TDD.
Заключение: двоичность и PHP классные
Когда вооружишься такими инструментами, всё остальное превращается лишь в поиск правильной документации по поведению двоичных файлов или протоколов. Ведь всё является двоичными последовательностями.
Очень рекомендую почитать спецификации PDF или EXIF. Возможно, вы даже захотите поэкспериментировать с собственной реализацией формата сериализации MessagePack, или Avro, Protobuf… Возможности безграничны!