PHP应使用fopen('rb') + fread(4) + bin2hex()读取文件前4字节并比对十六进制值判断BOM,而非mb_detect_encoding或file_get_contents;常见BOM为'efbbbf'(UTF-8)、'fffe'(UTF-16 LE)等。

PHP如何读取文件前3字节判断BOM
直接读取文件开头几个字节,比用 mb_detect_encoding 可靠得多——后者根本不识别BOM,且对UTF-8无BOM和GBK混杂的情况极易误判。
关键不是“检测编码”,而是“确认有没有BOM”。BOM是硬编码在文件头的字节序列,只需比对:
-
EF BB BF→ UTF-8 BOM -
FF FE→ UTF-16 LE BOM(小端) -
FE FF→ UTF-16 BE BOM(大端) -
FF FE 00 00→ UTF-32 LE -
00 00 FE FF→ UTF-32 BE
实操建议:用 fopen + fread 读前4字节(覆盖所有常见BOM长度),再用 bin2hex 转成小写十六进制字符串比对。避免用 file_get_contents 全量读取大文件,也别依赖 mb_check_encoding——它不看BOM。
为什么 file_get_contents($f, false, null, 0, 3) 不够用
这个写法看似省事,但有两处隐患:file_get_contents 默认按当前脚本编码解析内容,若文件含BOM而PHP内部处理时做了隐式转换(比如开启 default_charset 或用了输出缓冲),前三字节可能被截断或变形;更严重的是,它无法区分 \xEF\xBB\xBF 和普通中文字符的UTF-8三字节序列(如“一”是 \xE4\xB8\x80),纯靠长度不够。
立即学习“PHP免费学习笔记(深入)”;
必须绕过字符层,走二进制流:
- 用
fopen($f, 'rb')强制二进制模式 - 用
fread($fp, 4)读4字节(UTF-32 BOM需4字节) - 用
unpack('H*', $bytes)或bin2hex($bytes)获取原始十六进制 - 注意:
strlen($bytes)必须 ≥ 3 才能判断UTF-8 BOM,空文件或只读到1–2字节时不能贸然断言“无BOM”
实际处理时容易漏掉的边界情况
BOM不一定在文件最开头——如果文件被追加写入、或用某些编辑器“保存为带BOM”时中间插入了不可见字符,^\xEF\xBB\xBF 正则会失效。所以严格来说,BOM只应出现在文件起始位置,其他位置的相同字节不算。
还有几个易错点:
- Windows记事本另存为UTF-8时,一定带BOM;VS Code默认不带,但用户可手动开启——不能假设编辑器行为一致
- PHP CLI下读取文件,
$_SERVER['SCRIPT_FILENAME']指向的文件本身若有BOM,会导致“Cannot modify header information”错误,但错误提示里完全不提BOM - 用
curl或file_get_contents请求远程URL返回的内容,即使响应头声明Content-Type: text/html; charset=utf-8,也不能反推它是否含BOM——BOM是文件/响应体字节层面的事,和HTTP头无关
封装一个可靠的 is_file_has_bom 函数
不要每次重复写 fopen/fread/bin2hex,封装成函数更稳妥。注意它只负责判断,不负责去除:
function is_file_has_bom(string $path): bool
{
if (!is_readable($path)) {
return false;
}
$fp = fopen($path, 'rb');
if (!$fp) {
return false;
}
$bytes = fread($fp, 4);
fclose($fp);
$hex = bin2hex($bytes);
return in_array($hex, [
'efbbbf', // UTF-8
'fffe', // UTF-16 LE
'feff', // UTF-16 BE
'fffe0000', // UTF-32 LE
'0000feff', // UTF-32 BE
], true);
}
这个函数不依赖扩展,不触发任何编码转换,也不加载整个文件。真正麻烦的是后续动作:发现BOM后,你是跳过它解析、还是用 file_put_contents 去除、还是报错中断?这些决策得结合业务场景来定——比如配置文件含BOM可能导致 parse_ini_file 解析失败,但日志文件里的BOM通常可以忽略。











