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

17

有没有一种快速简单的方法来使用PHP检查文件是ASCII还是二进制?


这个问题以前已经被问过了,但我总是想知道,为什么你关心它是ASCII还是二进制? - Pyrolistical
类似但不是重复的问题。这个问题有一个简单的技术答案,而所谓的相同问题则更加困难。询问文件是否处于编码X中或者是否处于任何编码中之间有很大的区别。 - Devin Jeanpierre
不,再读一遍,那些类型只是举例。他正在寻找二进制与文本相同的东西。 - Pyrolistical
这不是重复问题,因为那是一个普遍性问题(更多的是理论问题),而这个问题是针对特定语言的(实际应用)。无论如何,我最终做的如下所示。 - davethegr8
@Pyrolistical:检查 uploaded.avi 是否是其他类型的文件,因为检查MIME类型似乎不够准确。 - Leo
@davethegr8 我知道这是一个非常老的问题,但您是否有可能愿意重新审查您选择的答案? - Brogan
5个回答

24

这仅适用于 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';

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


2
可能应该检查一下 if (!$finfo){ echo "Opening fileinfo database failed"; exit(); },并且不要忘记:finfo_close($finfo);... - user257319
这对于 application/javascript 不会失败吗? - Shivanand Sharma
这告诉您文件是否仅包含可打印的字符,但它不会告诉您文件是ASCII还是二进制的。 - Brogan

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

Devin的回答非常含糊,与我编写的第一段代码完成此任务有关。结果还可以。我发现在许多情况下,按字节搜索对于二进制文件更快。如果您找到一个大于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;

我的处理器并不是很先进,但我发现一个大小为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;

这个函数的速度差异相当显著,只需要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;

太棒了!只用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);

这是我测试两个文件的方法...
<?php
$filename = 'E:\ASCII';
$finfo = finfo_open(FILEINFO_MIME);
echo (substr(finfo_file($finfo, $filename), 0, 4) == 'text') ? 'ASCII' : 'Binary';

将输出:

二进制

以及...

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

输出为:

二进制

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

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

并再次进行了测试。

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

这将输出:

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;

哎呀!0.2秒告诉我我的600Kb文件是ASCII格式。我知道,我的大型ASCII文件只包含可见的ASCII字符。它似乎知道我的二进制文件是二进制的。而我的纯ASCII文件却是二进制的!

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

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

与davethegr8的答案一样,这个函数只告诉你是否包含可打印的ASCII字符,并不能告诉你文本是否实际上是ASCII格式。这并不一定意味着MacroA完全错误,他们只是不完全正确。与str_replace相比,str_ireplace很慢,而仅替换那三个控制字符以测试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
}

这句话的意思是:“告诉我,我的600Kb文件是ASCII格式,只用了0.04秒。”

我相信所有这些测试并不是完全无用的,因为它给了我一个新的想法。为什么不在我的原始函数中添加可打印文件模式呢!虽然在我的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;

我还测试了 ctype_print 和正则表达式,发现 ctype_print 稍微快一些。

$printable = preg_match('/[^\x20-\x7E]/', $buffer) === 0;

这是我的最终函数,其中查找可打印文本和缓冲区大小都是可选的。
<?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";

正在开发一个恶意软件扫描器,我不能冒任何误报的风险。现在的恶意软件甚至隐藏在jpg和ico扩展名中。我真的希望能够从这里使用一些代码来跳过那些肯定是二进制文件的文件。这可以使用file_get_contents吗? - Shivanand Sharma
“Bio-Bäckerei Onder de Linden”这个字符串被包含在一个纯文本文件中,但被标记为二进制文件。 - Shivanand Sharma
1
Shivanand Sharma,那是因为上面的字符串是二进制的,需要全部使用8个比特位,而不是纯文本。 - Brogan
谢谢。那么有没有办法区分包含这些字符的文件和包含二进制文件中的字符的文件?我认为 mb_check_encoding 可以完成这项工作,但是 PHP 默认情况下未安装 mbstring 扩展。 - Shivanand Sharma
Shivanand,我的整篇文章都致力于解决这个问题。我不确定你为什么要在评论中提问。 - Brogan
@ShivanandSharma 检查有效的UTF-8编码,您可以使用//u。Brogan,块大小为256比任何硬盘块大小都要小(即使是25年前的硬盘);40968192将是更合理的默认值。 - Fravadona

4

因为ASCII只是文本的编码方式,具有二进制表示,所以不完全准确。你可以检查所有字节是否小于128,但即使如此也不能保证它被解码为ASCII。你可能会发现它是一种疯狂的图像格式,或者是完全不使用所有八位的另一种文本编码方式。但是,如果你只想检查一个文件是否是有效的ASCII,即使它不是“文本文件”,这绝对足够了。


3

你可能需要检查文件的MIME类型,但如果你愿意将文件加载到内存中,也许你可以使用类似以下代码来检查缓冲区是否由所有可打印字符组成:

<?php
$probably_binary = (is_string($var) === true && ctype_print($var) === false);

虽不完美,但在某些情况下可能有所帮助。


5
不幸的是,Tabs和回车符会导致ctype_print()返回FALSE。 - dotancohen

3
这样在我的项目中看起来是可以的:
function probably_binary($stringa) {
    $is_binary=false;
    $stringa=str_ireplace("\t","",$stringa);
    $stringa=str_ireplace("\n","",$stringa);
    $stringa=str_ireplace("\r","",$stringa);
    if(is_string($stringa) && ctype_print($stringa) === false){
        $is_binary=true;
    }
    return $is_binary;
}

PS:抱歉,这是我的第一篇帖子,我想在之前的评论中添加一条评论 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接