在PHP文件中的UTF-8 BOM签名

29

我在编写一些有注释的 PHP 类时遇到了一个问题。我的名字(用于 @author 标签)以 ș 结尾 (这是一个 UTF-8 字符,...很奇怪的名字,我知道)。

即使我将文件保存为 UTF-8,一些朋友报告说他们看到那个字符被完全搞砸了(È™)。通过添加 BOM 签名,这个问题就会消失。但这让我有点困扰,因为我对它并不是那么了解,除了我在维基百科和一些其他类似的SO问题上看到的东西。

我知道它在文件开头添加了一些内容,从我理解的来看,这并不是什么大问题,但我担心的是,我所读到的唯一有问题的场景都涉及 PHP 文件。由于我正在编写 PHP 类来共享它们,所以要想保持 100% 的兼容性比在注释中使用我的名字更重要。

但我试图理解这种情况的影响,我应该毫不担忧地使用它吗?还是有可能会造成损害的情况?什么时候会出现这种情况呢?


注意,今天我遇到了一个问题,即在包含的PHP文件顶部使用<?php die('test')时,文件顶部显示UTF-8 BOM(这些字符:)。很难找出原因。后来我发现其中一位开发人员将文件保存为Unicode而不是ASCII,因此PHP无法读取Unicode PHP文件。我开始了一个新的副本,复制数据并保存为ASCII,问题得到解决。请注意,如果您遇到此类问题,则某些FTP程序可能会对您造成影响。 - Volomike
请注意,当页面存在UTF-8 BOM问题时,会话变量似乎无法在页面之间正常工作。我不得不使用像Ubuntu上的ghex这样的十六进制编辑器,以及重复使用iconv -f utf8 -t ascii old.php > new.php命令来检测所有Unicode问题,将其删除,并最终以ASCII格式保存页面,以避免iconv命令中的错误。完成后,我注意到会话变量在页面之间保持状态。 - Volomike
当在文件中检测到UTF-8 BOM时,似乎不会发送包含会话的标头,因此页面之间的会话变量将获得全新的会话,而不是保持相同的会话。 - Volomike
并不是PHP“检测”到BOM,然后“决定”丢弃会话变量——问题在于PHP(至少我见过的某些版本如此)读取文件,读取一个ï,打印它,读取一个»,打印它,读取一个¿,打印它……现在的问题是session_start()会导致一些头部通信,而这只能在我们仍处于头部通信阶段时发生——而打印某些内容会结束这个阶段。如果你将“display_errors”ini变量设置为“On”,你会收到一条消息,告诉你由于这个原因session_start失败了。 - Algoman
8个回答

26

事实上,BOM是发送给浏览器的实际数据。浏览器可以忽略它,但你仍然不能发送头部。

我认为问题实际上在于你和你朋友的编辑器设置。如果没有BOM,你朋友的编辑器可能无法自动识别该文件为UTF-8。他可以尝试设置编辑器以期望文件为UTF-8(如果您使用像NetBeans这样的真正IDE,则甚至可以将其作为项目设置与代码一起传输)。

另一个选择是尝试一些技巧:一些编辑器会使用基于输入文本的一些启发式算法来确定编码方式。你可以尝试以以下方式开始每个文件:

<?php //Úτƒ-8 encoded

也许启发式算法会解决这个问题。可能有更好的方法来解决这个问题,你可以通过谷歌搜索常见的编码检测启发式算法,或者尝试一些自己的方法:-)

总而言之,我建议您只需修复编辑器设置。

哦,等等,我误读了最后一部分:为了将代码传播到任何地方,我想您最安全的方式就是使所有文件仅包含低7位字符,即纯ASCII字符,或者接受一些古老编辑器看到您的名字写错的情况。没有绝对可靠的方法。BOM肯定是不好的,因为已经发送了头部信息。另一方面,只要您在注释中只放置UTF-8字符,某些编辑器误解编码的唯一影响是出现奇怪的字符。我会正确拼写您的名称并添加一个针对启发式算法的注释,以便大多数编辑器都能够理解它,但总有一些人会看到无效的字符。


谢谢您的建议。我明白了自己的立场,我认为与其使用编码检测启发式算法这种奇怪的妥协,我会做出正确的选择,只是用“s”而不是“ș”来拼写我的名字,因为大多数可能的程序员甚至没有那个字符在他们的语言中。对吧? :) - treznik
4
浏览器不会忽略BOM。这些错误很难追踪。永远不要保存带有BOM的PHP文件。 - hakre
不行,因为这不是一个错误。BOM 是一种可憎的东西,不要使用它。 - skrebbel
1
这绝对是一个 bug。PHP 可以在头部阶段结束时轻松地“重新排版”它。存在许多使用 BOM 的好理由,包括尽管具有在文件旁边存储内容编码的技术手段(包括 xattr/windows ADS),但实际上没有什么东西这样做,因此...我们需要像 BOM 和 <meta charset> 这样的内部方法。此外,它只是一个魔数,就像之前许多编码/文件格式一样。 - DimeCadmium

17

BOM会导致Headers already sent错误,因此在PHP文件中不能使用BOM。


11
这是一篇旧帖子,已经得到了答案,但我可以给你留下一些其他的资源,当我面对BOM问题时,我发现这些资源很有用。
使用http://people.w3.org/rishida/utils/bomtester/index.php这个页面,你可以检查特定文件是否包含BOM。
此外,还有一个方便的脚本,可以输出当前目录中所有具有BOM的文件。
<?php 
function fopen_utf8 ($filename) { 
    $file = @fopen($filename, "r"); 
    $bom = fread($file, 3); 
    if ($bom != b"\xEF\xBB\xBF") 
    { 
        return false; 
    } 
    else 
    { 
        return true; 
    } 
} 

function file_array($path, $exclude = ".|..|design", $recursive = true) { 
    $path = rtrim($path, "/") . "/"; 
    $folder_handle = opendir($path); 
    $exclude_array = explode("|", $exclude); 
    $result = array(); 
    while(false !== ($filename = readdir($folder_handle))) { 
        if(!in_array(strtolower($filename), $exclude_array)) { 
            if(is_dir($path . $filename . "/")) { 
                                // Need to include full "path" or it's an infinite loop 
                if($recursive) $result[] = file_array($path . $filename . "/", $exclude, true); 
            } else { 
                if ( fopen_utf8($path . $filename) ) 
                { 
                    //$result[] = $filename; 
                    echo ($path . $filename . "<br>"); 
                } 
            } 
        } 
    } 
    return $result; 
} 

$files = file_array("."); 
?>

我在php.net网站上找到了这段代码。

Dreamweaver也可以帮助处理这个问题,它提供了一个选项让你保存文件时不包括BOM内容。

虽然回答有点晚,但我仍然希望能对你有所帮助。再见。


1
fopen_utf8()脚本帮助我从供应商提供的SDK中隔离出BOM文件。非常有帮助! - Chris Rasco
我可以在哪里运行这个脚本?是shell吗? - Melly

9

提醒一下,PHP中有一个选项zend.multibyte,可以使PHP在读取带BOM的文件时不会出现Headers already sent错误。

从php.ini文件中可以看到:

; If enabled, scripts may be written in encodings that are incompatible with
; the scanner.  CP936, Big5, CP949 and Shift_JIS are the examples of such
; encodings.  To use this feature, mbstring extension must be enabled.
; Default: Off
;zend.multibyte = Off

4
在PHP中,除了“标头已发送”错误之外,BOM的存在也可能以更微妙的方式破坏浏览器中的HTML。参见W3C国际化的UTF-8 BOM引起的显示问题,其中概述了这个问题并重点关注了PHP。
当发生这种情况时,渲染页面顶部通常会有一个明显的空格,而且如果在Firefox或Chrome中检查HTML,则可能会注意到head部分为空,其元素似乎在body中。
当然,查看源代码将在插入的位置显示所有内容,但是浏览器正在将其解释为body内容(文本)并将其插入到文档对象模型(DOM)中。

2

或者您可以在php.ini中激活输出缓冲,这将解决“头部已发送”问题。如果您的网站有重要的负载,使用输出缓冲对性能也非常重要。


2
BOM实际上是识别UTF-8文件最有效的方式,现代浏览器和标准都支持和鼓励在HTTP响应正文中使用它。对于PHP文件来说,发送的是生成的输出而不是文件本身,因此将所有PHP文件保存为带有BOM的文件开头显然不是一个好主意,但这并不意味着您不应该在响应中使用BOM。实际上,您可以在doctype声明之前(如果您正在生成HTML作为响应)安全地注入以下代码:<?="\u{FEFF}"?>(或PHP 7.0.0之前:<?="\xEF\xBB\xBF"?>)。更多阅读:https://www.w3.org/International/questions/qa-byte-order-mark#transcoding

0

除了 @omabena 的答案,您可以使用以下代码来定位和删除文件中的BOM。务必先备份您的文件以防万一。

function fopen_utf8 ($filename) { 
    $file = @fopen($filename, "r"); 
    $bom = fread($file, 3); 
    if ($bom != b"\xEF\xBB\xBF") 
    { 
        return false; 
    } 
    else 
    { 
        return true; 
    } 
} 

function file_array($path, $exclude = ".|..|design", $recursive = true) { 
    $path = rtrim($path, "/") . "/"; 
    $folder_handle = opendir($path); 
    $exclude_array = explode("|", $exclude); 
    $result = array(); 
    while(false !== ($filename = readdir($folder_handle))) { 
        if(!in_array(strtolower($filename), $exclude_array)) { 
            if(is_dir($path . $filename . "/")) { 
                                // Need to include full "path" or it's an infinite loop 
                if($recursive) $result[] = file_array($path . $filename . "/", $exclude, true); 
            } else { 
                if ( fopen_utf8($path . $filename) ) 
                { 
                    //$result[] = $filename; 
                    echo ($path . $filename . "<br>"); 
                    $pathname = $path . $filename; // change the pathname to your target file(s) which you want to remove the BOM.
                    $file_handler = fopen($pathname, "r");
                    $contents = fread($file_handler, filesize($pathname));
                    fclose($file_handler);
                    for ($i = 0; $i < 3; $i++){
                        $bytes[$i] = ord(substr($contents, $i, 1));
                    }
                    if ($bytes[0] == 0xef && $bytes[1] == 0xbb && $bytes[2] == 0xbf){
                        $file_handler = fopen($pathname, "w");
                        fwrite($file_handler, substr($contents, 3));
                        fclose($file_handler);
                        printf("%s BOM removed.<br/>n", $pathname);
                    }
                } 
            } 
        } 
    } 
    return $result; 
} 

$files = file_array("."); 

我可以在哪里运行这段代码?我有一个 Laravel 项目。 - Melly

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