PHP无法找到分割UTF-8字符串的方法。

4

我刚开始涉猎php,但是我需要一些帮助来搞清楚如何处理UTF-8编码的字符串。

我正在使用Ubuntu 11.10 x86,PHP版本为5.3.6-13ubuntu3.2。我有一个UTF-8编码的文件(vim :set encoding 确认了这一点),然后我使用以下方式读取它:

$file = fopen("file.txt", "r");
while(!feof($file)){
    $line = fgets($file);
    //...
}
fclose($file);
  • 使用mb_detect_encoding($line)报告UTF-8
  • 如果我执行echo $line,则可以在浏览器中正确查看该行(无损坏字符)。
    • 所以我猜测浏览器和Apache都没有问题。尽管我搜索了我的Apache配置AddDefaultCharset,并尝试添加字符编码的http元标签(以防万一)

当我尝试使用$arr = mb_split(';',$line)拆分字符串时,生成的数组的字段包含乱码的utf-8字符(mb_detect_encoding($arr[0])也报告utf-8)。

因此,echo $arr[0]将导致出现以下内容:ΑΘΗÎÎ

我尝试设置mb_detect_order('utf-8')mb_internal_encoding('utf-8'),但是没有任何变化。我还尝试手动检测utf-8,使用这个w3 perl正则表达式,因为我在某个地方读到mb_detect_encoding有时可能会失败(误传?),但结果也是一样的。

所以我的问题是如何正确地拆分字符串?走mb_的路线是错误的吗?我漏掉了什么?

谢谢你的帮助!

更新:我正在添加示例字符串和base64等效项(感谢@chris'的建议)

1. original string: "ΑΘΗΝΑ;ΑΙΓΑΛΕΩ;12242;37.99452;23.6889"
2. base64 encoded: "zpHOmM6Xzp3OkTvOkc6ZzpPOkc6bzpXOqTsxMjI0MjszNy45OTQ1MjsyMy42ODg5"
3. first part (the equivalent of "ΑΘΗΝΑ") base64 encoded before splitting: "zpHOmM6Xzp3OkQ=="
4. first part ($arr[0] after splitting): "ΑΘΗÎΑ"
5. first part after splitting base64 encoded: "77u/zpHOmM6Xzp3OkQ=="

好的,这样做后,第3和第5之间似乎存在一个77u/的差异,根据这篇文章,这是一个UTF-8 BOM标记。那么我该如何避免它呢?

更新2:今天早上我精神焕发,记住了你们的建议,再次尝试了一下。看起来$line=fgets($file)可以正确读取第一行(没有损坏的字符),但对于每个后续行都失败了。所以我将第一行和第二行进行了base64_encoded,然后只有第一行的base64字符串出现了77u/ bom。然后我在vim中打开了有问题的文件,并输入了:set nobomb :w以不带bom的方式保存文件。再次启动php时,发现第一行现在也被损坏了。基于@hakre的remove_utf8_bom,我添加了它的补充函数。

function add_utf8_bom($str){
    $bom= "\xEF\xBB\xBF";
    return substr($str,0,3)===$bom?$str:$bom.$str;
}

神奇地,每一行现在都被正确读取了。

我并不太喜欢这个解决方案,因为它看起来非常非常hackish(我不敢相信整个框架/语言都没有提供处理nobombed字符串的方法)。那么你知道其他替代方法吗?否则我将继续上述方法。

感谢@chris、@hakre和@jacob的时间!

更新3(解决方案):结果证明这是一个浏览器问题:添加header('Content-type: text/html; charset=UTF-8')以及类似于<meta http-equiv="Content-type" value="text/html; charset=UTF-8" />的meta标签是不够的。它还必须正确地包含在<html><body>部分中,否则浏览器无法正确理解编码。感谢@jake的建议。

故事寓意:在尝试为浏览器编写代码之前,我应该更多地学习html。感谢大家的帮助和耐心。


1
我建议您发布样本字符串(拆分前和拆分后)供人们检查。为了保持它们的二进制安全性,可以使用base64_encode()进行编码,否则在Web浏览器和StackOverflow等平台上可能无法保留细节。 - goat
@chris +1 看起来你用 base64 可能有点头绪。 - bottlenecked
验证 utf-8 的 http 头部是否被发送到浏览器。使用 Firebug 或其他 Firefox 插件进行检查。 - goat
1
@bottlenecked:我不知道你是否已经这样做了,但是尝试在你的test.php文件中输出有效的HTML,即在写入echo $line之前,写入类似于echo '<!DOCTYPE html><html><head><meta charset=utf-8><title>Test Page</title></head><body>';的内容。 - Jakob Egger
@jakob,将php代码/echo语句包含在doctype/html/body中似乎解决了问题。我会写一个更新来说明解决方案。你能否在你的回答中加入你的建议,因为我无法将评论标记为已接受的答案? - bottlenecked
显示剩余4条评论
4个回答

4

UTF-8有一个非常好的特性,那就是它与ASCII兼容。这意味着:

  • 当编码为UTF-8时,ASCII字符保持不变
  • 没有其他字符会被编码为ASCII字符

这意味着,当您尝试使用分号字符;(一个ASCII字符)拆分UTF-8字符串时,您可以使用标准的单字节字符串函数。

在您的示例中,您只需使用explode(';',$utf8encodedText),一切都应该按预期工作。

PS:由于UTF-8编码是无前缀的, 您实际上可以使用任何UTF-8编码的分隔符来使用explode()

PPS:看起来您正在尝试解析CSV文件。请查看fgetcsv()函数。只要使用ASCII字符作为分隔符、引号等,它应该在UTF-8编码的字符串上完美工作。


实际上,我最初使用的是explode函数,但当我无法使其正常工作时,我开始阅读有关mbstrings的内容。 - bottlenecked
那么你的问题可能是HTML页面的输出编码不是UTF-8。检查一下页面头部是否有<meta charset=utf-8> - Jakob Egger
我尝试过了(在冗长的问题陈述中也提到了),但还是没有结果。我还更新了问题,并发现了新的情况。 - bottlenecked

1

mb_split文档函数应该没问题,但你也应该使用mb_regex_encoding文档定义它所使用的字符集:

mb_regex_encoding('UTF-8');

关于mb_detect_encoding文档:它可能会失败,但这只是因为你永远无法检测到编码。你要么知道它,要么尝试一下,但仅此而已。编码检测大多是一个赌博游戏,但你可以使用该函数的严格参数并指定你要查找的编码。 如何去除BOM掩码: 你可以过滤字符串输入并使用一个小的辅助函数来删除UTF-8 bom:
/**
 * remove UTF-8 BOM if string has it at the beginning
 *
 * @param string $str
 * @return string
 */
function remove_utf8_bom($str)
{
   if ($bytes = substr($str, 0, 3) && $bytes === "\xEF\xBB\xBF") 
   {
       $str = substr($str, 3);
   }
   return $str;
}

使用方法:

$line = remove_utf8_bom($line);

可能有更好的方法,但这应该可以工作。


我对你的字符串没有问题,实际上即使是使用UTF-8编码的字符串,一个简单的explode也可以起作用。请参见http://codepad.viper-7.com/eODqA5 - 看起来你将结果视为ISO-8859-*。 - hakre
使用 add_utf8_bom,explode 对每一行都按预期工作。如果没有更好(即不太糟糕的)的解决方案出现,我将接受这个答案。 - bottlenecked
较少hacky的方法是保存file.txt时不带BOM。这是针对此类问题首先建议的方法,请参见http://unicode.org/faq/utf_bom.html#BOM。另外,学习在vim中如何删除文件中已有的BOM。在我看来,`mb_split`工作得很好,因为它应该保留BOM,因为它也是一个有效的Unicode代码点:http://www.fileformat.info/info/unicode/char/feff/index.htm - 所以最好给你的应用程序正确编码的字符串,或者在解析之前修复它,或者继续使用hack ;) - hakre

1

编辑,我刚刚仔细阅读了您的帖子。您建议这应该输出false,因为您建议mb_split()引入了BOM。

header('content-type: text/plain;charset=utf-8');
$s = "zpHOmM6Xzp3OkTvOkc6ZzpPOkc6bzpXOqTsxMjI0MjszNy45OTQ1MjsyMy42ODg5";
$str = base64_decode($s);

$peices = mb_split(';', $str);

var_dump(substr($str, 0, 10) === $peices[0]);
var_dump($peices);

是吗?对我来说它的工作正常(布尔值为真,数组中的字符串也正确)


是的,它正如你所说的那样工作。问题似乎出现在从文件本身读取相同行时。 - bottlenecked
你确定在发布base64编码的字符串时没有犯错吗?因为原始的base64字符串没有BOM,我认为它应该是直接从fgets返回的值,第一行也是如此。 - goat
是的,犯了错。那是一种“从编辑器手动复制行,然后作为参数粘贴到PHP文件中进行base64编码”的操作,因为当时我没有完全理解这样做的全部含义。很抱歉造成了不必要的困扰 :( - bottlenecked

1
当你编写 PHP 的调试/测试脚本时,请确保输出一个或多个有效的 HTML 页面。
我喜欢使用类似下面的 PHP 文件:
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <title>Test page for project XY</title>
  </head>
  <body>
     <h1>Test Page</h1>
     <pre><?php
        echo print_r($_GET,1);
     ?></pre>
  </body>
</html>

如果您不包含任何HTML标签,浏览器可能会将文件解释为文本文件,并且可能发生各种奇怪的事情。在您的情况下,我假设浏览器将文件解释为Latin1编码的文本文件。我假设它使用了BOM,因为只要存在BOM,浏览器就会将文件识别为UTF-8文件。

猜想那就是它!我现在更聪明了 :P - bottlenecked

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