如何在PHP中检查文件是ASCII还是二进制

dav*_*gr8 14 php

是否有一种快速,简单的方法来检查文件是ASCII还是二进制文件?

dav*_*gr8 21

这仅适用于PHP> = 5.3.0,并且不是100%可靠,但是嘿,它非常接近.

// return mime type ala mimetype extension
$finfo = finfo_open(FILEINFO_MIME);

//check to see if the mime-type starts with 'text'
return substr(finfo_file($finfo, $filename), 0, 4) == 'text';
Run Code Online (Sandbox Code Playgroud)

http://us.php.net/manual/en/ref.fileinfo.php

  • 可能应该检查 `if (!$finfo){ echo "Opening fileinfo database failed"; 出口(); }` 并且不要忘记:`finfo_close($finfo);`... (2认同)

Bro*_*gan 5

在我的一个较旧的 PHP 项目中,我使用 ASCII/二进制压缩。当用户上传文件时,他们需要指定该文件是 ASCII 还是二进制。我决定修改我的代码,让服务器自动决定文件模式是什么,因为依赖用户的决定可能会导致压缩失败。我决定我的代码必须是绝对的,并且不使用可能导致我的程序失败的技巧。我快速编写了一些代码,运行了一些速度测试,然后决定在互联网上搜索,看看是否有更快的代码示例来完成此任务。


德文非常模糊的答案与我为完成此任务而编写的第一个代码有关。结果马马虎虎。我发现在许多情况下,对于二进制文件来说,逐字节搜索速度更快。如果发现大于 127 的字节,则可以忽略文件的其余部分,整个文件被视为二进制文件。话虽这么说,您必须读取文件的每个最后一个字节才能确定该文件是否为 ASCII。对于许多二进制文件来说,它看起来更快,因为二进制字节可能会早于文件的最后一个字节,有时甚至第一个字节也是二进制的。

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'ASCII',
    2 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $handle = fopen($filename, 'rb');
            for($i = 0; $i < $size; ++$i) {
                $byte = fread($handle, 1);
                if(ord($byte) > 127) {
                    fclose($handle);
                    return 2; // Binary
                }
            }
            fclose($handle);
            return 1; // ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';

$loops = 1;
$x = 0;
$i = 0;
$start = microtime(true);

for($i = 0; $i < $loops; ++$i)
    $x = filemode($filename);

$stop = microtime(true);
$duration = $stop - $start;

echo
    'Filename: ', $filename, "\n",
    'Filemode: ', $filemodes[filemode($filename)], "\n",
    'Duration: ', $duration;
Run Code Online (Sandbox Code Playgroud)

我的处理器不太现代,但我发现 600Kb ASCII 文件大约需要 0.25 秒才能完成。如果我要在数百或数千个大文件上使用它,可能需要很长时间。我决定尝试通过使缓冲区大于单个字节来尝试加快速度,以便将文件作为块读取,而不是一次读取一个字节。使用块可以让我一次处理更多文件,但不会将太多内容加载到内存中。如果我们正在测试的文件很大并且我们要将整个文件加载到内存中,则它可能会占用太多内存并导致程序失败。

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'ASCII',
    2 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $buffer_size = 256;
            $chunks = ceil($size / $buffer_size);
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                $buffer_length = strlen($buffer);
                for($byte = 0; $byte < $buffer_length; ++$byte) {
                    if(ord($buffer[$byte]) > 127) {
                        fclose($handle);
                        return 2; // Binary
                    }
                }
            }
            fclose($handle);
            return 1; // ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';

$loops = 1;
$x = 0;
$i = 0;
$start = microtime(true);

for($i = 0; $i < $loops; ++$i)
    $x = filemode($filename);

$stop = microtime(true);
$duration = $stop - $start;

echo
    'Filename: ', $filename, "\n",
    'Filemode: ', $filemodes[filemode($filename)], "\n",
    'Duration: ', $duration;
Run Code Online (Sandbox Code Playgroud)

速度差异相当显着,只需要 0.15 秒,而不是前一个函数的 0.25 秒,读取 600Kb ASCII 文件的速度几乎快了十分之一秒。


现在我已经将文件分成了块,我认为找到替代方法来测试我的块中的二进制字符是个好主意。我的第一个想法是使用正则表达式来查找非 ASCII 字符。

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'ASCII',
    2 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $buffer_size = 256;
            $chunks = ceil($size / $buffer_size);
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                if(preg_match('/[\x80-\xFF]/', $buffer) === 1) {
                    fclose($handle);
                    return 2; // Binary
                }
            }
            fclose($handle);
            return 1; // ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';

$loops = 1;
$x = 0;
$i = 0;
$start = microtime(true);

for($i = 0; $i < $loops; ++$i)
    $x = filemode($filename);

$stop = microtime(true);
$duration = $stop - $start;

echo
    'Filename: ', $filename, "\n",
    'Filemode: ', $filemodes[filemode($filename)], "\n",
    'Duration: ', $duration;
Run Code Online (Sandbox Code Playgroud)

惊人的!0.02 秒即可将我的 600Kb 文件视为 ASCII 文件,并且此代码似乎 100% 可靠。


现在我已经到达这里,我有机会检查其他用户部署的其他几种方法。

今天最受接受的答案由 davethegr8 编写,使用 mimetype 扩展。首先,我需要在 php.ini 文件中启用此扩展。接下来,我针对没有文件扩展名的实际 ASCII 文件和没有文件扩展名的二进制文件测试了此代码。

这是我创建两个测试文件的方法。

<?php
$handle = fopen('E:\ASCII', 'wb');
for($i = 0; $i < 128; ++$i) {
    fwrite($handle, chr($i));
}
fclose($handle);

$handle = fopen('E:\Binary', 'wb');
for($i = 0; $i < 256; ++$i) {
    fwrite($handle, chr($i));
}
fclose($handle);
Run Code Online (Sandbox Code Playgroud)

这是我测试这两个文件的方法......

<?php
$filename = 'E:\ASCII';
$finfo = finfo_open(FILEINFO_MIME);
echo (substr(finfo_file($finfo, $filename), 0, 4) == 'text') ? 'ASCII' : 'Binary';
Run Code Online (Sandbox Code Playgroud)

哪个输出:

二进制

和...

<?php
$filename = 'E:\Binary';
$finfo = finfo_open(FILEINFO_MIME);
echo (substr(finfo_file($finfo, $filename), 0, 4) == 'text') ? 'ASCII' : 'Binary';
Run Code Online (Sandbox Code Playgroud)

哪个输出:

二进制

这段代码显示我的 ASCII 和二进制文件都是二进制的,这显然是不正确的,所以我必须找到导致 mimetype 成为“文本”的原因。对我来说,很明显,也许文本只是可打印的 ASCII 字符。所以我限制了 ASCII 文件的范围。

<?php
$handle = fopen('E:\ASCII', 'wb');
for($i = 32; $i < 127; ++$i) {
    fwrite($handle, chr($i));
}
fclose($handle);
Run Code Online (Sandbox Code Playgroud)

并再次测试了它。

<?php
$filename = 'E:\ASCII';
$finfo = finfo_open(FILEINFO_MIME);
echo (substr(finfo_file($finfo, $filename), 0, 4) == 'text') ? 'ASCII' : 'Binary';
Run Code Online (Sandbox Code Playgroud)

哪个输出:

ASCII码

如果我降低范围,它会将其视为二进制。如果我再次增加范围,它会将其视为二进制。

因此,最受接受的答案不会告诉您文件是否是 ASCII,而是告诉您它是否只包含可读文本。


最后,我需要测试另一个针对我的文件使用 ctype_print 的答案。我决定最简单的方法是使用我编写的代码并在 MarcoA 的代码中进行补充。

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'ASCII',
    2 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $buffer_size = 256;
            $chunks = ceil($size / $buffer_size);
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                $buffer = str_ireplace("\t", '', $buffer);
                $buffer = str_ireplace("\n", '', $buffer);
                $buffer = str_ireplace("\r", '', $buffer);
                if(ctype_print($buffer) === false) {
                    fclose($handle);
                    return 2; // Binary
                }
            }
            fclose($handle);
            return 1; // ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';

$loops = 1;
$x = 0;
$i = 0;
$start = microtime(true);

for($i = 0; $i < $loops; ++$i)
    $x = filemode($filename);

$stop = microtime(true);
$duration = $stop - $start;

echo
    'Filename: ', $filename, "\n",
    'Filemode: ', $filemodes[filemode($filename)], "\n",
    'Duration: ', $duration;
Run Code Online (Sandbox Code Playgroud)

哎哟! 0.2 秒告诉我我的 600Kb 文件是 ASCII。我知道,我的大型 ASCII 文件仅包含可见的 ASCII 字符。它似乎确实知道我的二进制文件是二进制的。而我的纯 ASCII 文件...二进制!

我决定阅读ctype_print的文档,其返回值定义为:

如果文本中的每个字符实际上都会创建输出(包括空格),则返回 TRUE。如果文本包含控制字符或根本没有任何输出或控制功能的字符,则返回 FALSE。

这个函数,就像 davethegr8 的答案一样,只告诉您文本是否包含可打印的 ASCII 字符,而不会告诉您文本是否实际上是 ASCII。这并不一定意味着 MacroA 完全错误,他们只是不完全正确。str_ireplace 比 str_replace 慢,并且仅替换这三个控制字符来测试 ctype_print 不足以知道字符串是否是 ASCII。为了使这个示例适用于 ASCII,我们必须替换每个控制字符!

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'ASCII',
    2 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $buffer_size = 256;
            $chunks = ceil($size / $buffer_size);
            $replace = array(
                "\x00", "\x01", "\x02", "\x03",
                "\x04", "\x05", "\x06", "\x07",
                "\x08", "\x09", "\x0A", "\x0B",
                "\x0C", "\x0D", "\x0E", "\x0F",
                "\x10", "\x11", "\x12", "\x13",
                "\x14", "\x15", "\x16", "\x17",
                "\x18", "\x19", "\x1A", "\x1B",
                "\x1C", "\x1D", "\x1E", "\x1F",
                "\x7F"
            );
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                $buffer = str_replace($replace, '', $buffer);
                if(ctype_print($buffer) === false) {
                    fclose($handle);
                    return 2; // Binary
                }
            }
            fclose($handle);
            return 1; // ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}
Run Code Online (Sandbox Code Playgroud)

这花了 0.04 秒才告诉我我的 600Kb 文件是 ASCII。


我相信所有这些测试并不是完全没有用,因为它确实给了我更多的想法。为什么不在我原来的函数中添加一个可打印的文件模式!虽然在我的 600Kb 可打印 ASCII 文件上看起来确实慢了 0.018 秒,但就是这样。

<?php
$filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'Printable',
    2 => 'ASCII',
    3 => 'Binary'
);

function filemode($filename) {
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            $printable = true;
            $buffer_size = 256;
            $chunks = ceil($size / $buffer_size);
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                if(preg_match('/[\x80-\xFF]/', $buffer) === 1) {
                    fclose($handle);
                    return 3; // Binary
                }
                else
                    if($printable === true)
                        $printable = ctype_print($buffer);
            }
            fclose($handle);
            return $printable === true ? 1 : 2; // Printable or ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';

$loops = 1;
$x = 0;
$i = 0;
$start = microtime(true);

for($i = 0; $i < $loops; ++$i)
    $x = filemode($filename);

$stop = microtime(true);
$duration = $stop - $start;

echo
    'Filename: ', $filename, "\n",
    'Filemode: ', $filemodes[filemode($filename)], "\n",
    'Duration: ', $duration;
Run Code Online (Sandbox Code Playgroud)

我还针对正则表达式测试了 ctype_print,发现 ctype_print 更快一些。

$printable = preg_match('/[^\x20-\x7E]/', $buffer) === 0;
Run Code Online (Sandbox Code Playgroud)

这是我的最终函数,其中查找可打印文本是可选的,缓冲区大小也是可选的。

<?php
const filemodes = array(
    -2 => 'Unreadable',
    -1 => 'Missing',
    0 => 'Empty',
    1 => 'Printable',
    2 => 'ASCII',
    3 => 'Binary'
);

function filemode($filename, $printable = false, $buffer_size = 256) {
    if(is_bool($printable) === false || is_int($buffer_size) === false)
        return false;
    $buffer_size = floor($buffer_size);
    if($buffer_size <= 0)
        return false;
    if(is_file($filename)) {
        if(is_readable($filename)) {
            $size = filesize($filename);
            if($size === 0)
                return 0; // Empty
            if($buffer_size > $size)
                $buffer_size = $size;
            $chunks = ceil($size / $buffer_size);
            $handle = fopen($filename, 'rb');
            for($chunk = 0; $chunk < $chunks; ++$chunk) {
                $buffer = fread($handle, $buffer_size);
                if(preg_match('/[\x80-\xFF]/', $buffer) === 1) {
                    fclose($handle);
                    return 3; // Binary
                }
                else
                    if($printable === true)
                        $printable = ctype_print($buffer);
            }
            fclose($handle);
            return $printable === true ? 1 : 2; // Printable or ASCII
        }
        else
            return -2; // Unreadable
    }
    else
        return -1; // Missing
}

// ==========

$filename = 'e:\test.txt';
echo
    'Filename: ', $filename, "\n",
    'Filemode: ', filemodes[filemode($filename, true)], "\n";
Run Code Online (Sandbox Code Playgroud)