【PHP】ファイルをバイナリとして読み出す

  • 2022年6月1日
  • PHP

 ファイルの中身をバイト単位(PHPにおける1byteは8bit)の16進数表記の文字列で読みたい時があります。PHPでこれをいい感じに扱うコードの例を紹介します。

 まずはファイルの中身をバイト単位で読み取って一次元配列にするコードです。

/**
 * ファイルをバイト単位で16進数表記にして読み込む。
 * PHP における1byteは8bit
 * @param  string   $filePath ファイルフルパス
 * @return string[] 16進数表記の文字列を詰め込んだ一次元配列
 */
function fread_as_bin(string $filePath): array
{
    /** @var resource $f 読み取るファイルのリソース */
    $f   = fopen($filePath, 'rb');
    $ret = [];
    // 1バイトずつ読み込み、読み込んだデータを16進表記文字列に変換する
    // @see https://www.php.net/manual/ja/function.bin2hex.php
    // bin2hex を while の内側に移動すると 0 をファイルから読んだ時に終了するので注意
    while ($byte = bin2hex(fread($f, 1))) {
        $ret[] = $byte;
    }

    return $ret;
}
$bin = fread_as_bin('./public/assets/img/logo.webp');
// バイトを16進数表示
foreach ($bin as $i => $v) {
    echo $v.' ';
    if ($i % 16 === 15) {
        echo "\n";
    }
}

/* 実行例
> php tmp.php
52 49 46 46 cc 58 00 00 57 45 42 50 56 50 38 4c
bf 58 00 00 2f b1 42 1e 10 0d 75 21 a2 ff c1 9a
80 88 08 1f 1f f6 7f 8a d4 d8 fa 56 f5 08 c3 e0
31 5c 36 6e 7b 16 b2 1a 5f 77 77 0f 90 3d 76 dd
fd 5e ce 75 5b f7 24 24 c4 73 e2 4e dc dd 05 77
# 以下省略
*/

 PHP にはバイナリデータを16進表現にするbin2hexという関数があり、これを用いることで簡単に人間が視認できる形でバイナリデータを表現できます。

PHP: bin2hex – Manual
[PHP: bin2hex – Manual](https://www.php.net/manual/ja/function.bin2hex.php)

 上記のシンプルな実装の注意点として、メモリ容量と実行速度があります。ファイルの中身を全て配列に入れるため巨大なファイルを読み込む際にメモリが足りなくなりやすいです。また1バイトずつ処理を実行するため、実行速度が比較的遅いです。これらを改善すると次の様になります。
 

<?php

/**
 * ファイルをバイト単位で16進数表記にして読み込む。
 * PHP における1byteは8bit
 * @param  string        $filePath   ファイルフルパス
 * @param  int|null      $limitByte  読み取るバイト数の制限
 * @param  callable|null $callback   バイトを読み取るたびに実行する関数。 ($byteList: string[]) => void。これを渡した場合、内部では変数上にファイルの中身を展開しない
 * @param  int           $readLength 一度に読み取るバイト数
 * @return string[]      16進数表記の文字列を詰め込んだ一次元配列
 */
function fread_as_bin(string $filePath, int|null $limitByte = null, callable|null $callback = null, int $readLength = 1024): array
{
    /** @var resource $f 読み取るファイルのリソース */
    $f   = fopen($filePath, 'rb');
    $ret = [];
    $i   = 0;
    // 引数で渡されたバイトずつ読み込むことでループ数を少なくする
    while ($char = fread($f, $readLength)) {
        if (isset($callback)) {
            // コールバックを引数で渡された場合、変数にファイルの中身を格納する代わりにコールバックに渡す
            $callback(str_split(bin2hex($char), 2));
        } else {
            // 複数バイトを一気に読み取るため複数の16進表記バイトが連なった文字列を2文字ずつ分解する
            // @see https://php.net/manual/ja/function.str-split.php
            array_push($ret, ...str_split(bin2hex($char), 2));
        }
        $i += $readLength;
        // 引数 $limitByte によってある程度のバイト数を読み取ったらそこで処理を打ち切る処理を追加
        if (isset($limitByte) && $i >= $limitByte) {
            break;
        }
    }

    return $ret;
}

 いささか複雑になりましたがおおよその場面に対応可能になります。これを使ってファイルヘッダからファイルの種別を見る、バイナリファイルの整合性の検証を行うなど、様々なことができます。
 実際に使うならば、例えば次の様になります。

<?php

// PHPの組み込み関数でエラーが起きた時、自動で呼ばれる関数を登録
// Laravel 等のフレームワークが自動でこのエラー登録を行ってくれている場合は不要
set_error_handler(static function ($errno, $errstr, $errfile, $errline) {
    // エラー原因を出力
    error_log("STRICT: {$errno} {$errstr} {$errfile} {$errline} ");
    // バックトレースを出力
    foreach (debug_backtrace() as $i => $trace) {
        $json = json_encode($trace);
        error_log("#$i {$json}");
    }
    exit('error');
}, E_ALL | E_STRICT);

/**
 * ファイルをバイト単位で16進数表記にして読み込む。
 * PHP における1byteは8bit
 * @param  string        $filePath   ファイルフルパス
 * @param  int|null      $limitByte  読み取るバイト数の制限
 * @param  callable|null $callback   バイトを読み取るたびに実行する関数。 ($byteList: string[]) => void。これを渡した場合、内部では変数上にファイルの中身を展開しない
 * @param  int           $readLength 一度に読み取るバイト数
 * @return string[]      16進数表記の文字列を詰め込んだ一次元配列
 */
function fread_as_bin(string $filePath, int|null $limitByte = null, callable|null $callback = null, int $readLength = 1024): array
{
    /** @var resource $f 読み取るファイルのリソース */
    $f   = fopen($filePath, 'rb');
    $ret = [];
    // 一度に読む量が渡された読み取り制限よりも大きいなら読み取り制限まで縮小
    if (isset($limitByte) && $readLength > $limitByte) {
        $readLength = $limitByte;
    }
    $alreadyReadByteTotalSize   = 0;
    // 引数で渡されたバイトずつ読み込むことでループ数を少なくする
    while ($char = fread($f, $readLength)) {
        if (isset($callback)) {
            // コールバックを引数で渡された場合、変数にファイルの中身を格納する代わりにコールバックに渡す
            $callback(str_split(bin2hex($char), 2));
        } else {
            // 複数バイトを一気に読み取るため複数の16進表記バイトが連なった文字列を2文字ずつ分解する
            // @see https://php.net/manual/ja/function.str-split.php
            array_push($ret, ...str_split(bin2hex($char), 2));
        }
        $alreadyReadByteTotalSize += $readLength;
        // 引数 $limitByte によってある程度のバイト数を読み取ったらそこで処理を打ち切る処理を追加
        if (isset($limitByte) && $alreadyReadByteTotalSize >= $limitByte) {
            break;
        }
    }

    return $ret;
}

// PHP8.0以降では引数の順番を無視した名前付き引数が使えます
// @see https://www.php.net/manual/ja/functions.arguments.php#functions.named-arguments
$bin = fread_as_bin('./public/assets/img/logo.webp', limitByte: 16);
// バイトを16進数表示
foreach ($bin as $i => $v) {
    echo $v.' ';
    if ($i % 16 === 15) {
        echo "\n";
    }
}
// # 出力結果
// 52 49 46 46 cc 58 00 00 57 45 42 50 56 50 38 4c

echo "\n";
// バイトを文字列表示。 bin2hex の逆変換である hex2bin を使うだけ
foreach ($bin as $i => $v) {
    echo hex2bin($v).' ';

    if ($i % 16 === 15) {
        echo "\n";
    }
}
// # 出力結果
// R I F F � X   W E B P V P 8 L
// この WEBP の様にファイルのヘッダー付近でファイルの中身を識別する方法が使えます
// @see https://filesignatures.net/
// @see https://en.wikipedia.org/wiki/List_of_file_signatures
// @see https://cpoint-lab.co.jp/article/201908/11105/

/** webp ファイルならばtrue */
function isWebp(string $filePath): bool{
    $bin = fread_as_bin($filePath, limitByte: 12);
    $binStr = hex2bin(implode("", $bin));
    return str_starts_with($binStr, 'RIFF') && strpos($binStr, 'WEBP') === 8;
}
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG