UTF8编码问题 - 附带好的例子

28

我遇到以下字符编码问题,不知何故我已经成功将不同字符编码的数据保存到我的数据库(UTF8)中。下面的代码和输出显示了2个示例字符串以及它们的输出。其中1个需要更改为UTF8,另一个已经是UTF8。

我该如何检查是否应该对字符串进行编码?例如,我需要每个字符串正确输出,那么如何检查它是否已经是utf8或者是否需要转换为utf8?

我正在使用PHP 5.2,mysql myisam表:

CREATE TABLE IF NOT EXISTS `entities` (
  ....
  `title` varchar(255) NOT NULL
  ....
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

<?php
$text = $entity['Entity']['title'];
echo 'Original : ', $text."<br />";
echo 'UTF8 Encode : ', utf8_encode($text)."<br />";
echo 'UTF8 Decode : ', utf8_decode($text)."<br />";
echo 'TRANSLIT : ', iconv("ISO-8859-1", "UTF-8//TRANSLIT", $text)."<br />";
echo 'IGNORE TRANSLIT : ', iconv("ISO-8859-1", "UTF-8//IGNORE//TRANSLIT", $text)."<br />";
echo 'IGNORE   : ', iconv("ISO-8859-1", "UTF-8//IGNORE", $text)."<br />";
echo 'Plain    : ', iconv("ISO-8859-1", "UTF-8", $text)."<br />";
?>

输出 1:

Original : France Télécom
UTF8 Encode : France Télécom
UTF8 Decode : France T�l�com
TRANSLIT : France Télécom
IGNORE TRANSLIT : France Télécom
IGNORE : France Télécom
Plain : France Télécom

输出2:###

Original : Cond� Nast Publications
UTF8 Encode : Condé Nast Publications
UTF8 Decode : Cond?ast Publications
TRANSLIT : Condé Nast Publications
IGNORE TRANSLIT : Condé Nast Publications
IGNORE : Condé Nast Publications
Plain : Condé Nast Publications

感谢您抽出时间来处理这个问题。字符编码和我没有很好的相处!

更新:

echo strlen($string)."|".strlen(utf8_encode($string))."|";
echo (strlen($string)!==strlen(utf8_encode($string))) ? $string : utf8_encode($string);
echo "<br />";
echo strlen($string)."|".strlen(utf8_decode($string))."|";
echo (strlen($string)!==strlen(utf8_decode($string))) ? $string : utf8_decode($string);
echo "<br />";

23|24|CondNast Publications
23|21|CondNast Publications

16|20|France Télécom
16|14|France Télécom

从外观上看,第一个字符串已经是UTF-8格式了,而第二个字符串则是ISO-8859-1格式。但是您的问题是什么? - Pekka
我希望每个字符串都能正确输出,那么我该如何检查它是否已经是utf8格式或者需要进行转换呢? - Lizard
2
不确定,但可以在这里看一下 - http://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_charset - 通过良好的MySQL函数组合,您可以只使用单个更新查询来完成所需操作。 - Richard Knop
1
我也认为修复数据库一次比在每个请求上重新编码字符串更好。 - Dr.Molle
6个回答

30

这可能需要使用mb_detect_encoding()函数。

根据我的有限经验,在作为通用的“编码嗅探器”时,它并不是100%可靠的-它检查特定字符和字节值的存在来进行推测-但在这种狭窄情况下(仅需区分UTF-8和ISO-8859-1),它应该可以工作。

<?php
$text = $entity['Entity']['title'];

echo 'Original : ', $text."<br />";
$enc = mb_detect_encoding($text, "UTF-8,ISO-8859-1");

echo 'Detected encoding '.$enc."<br />";

echo 'Fixed result: '.iconv($enc, "UTF-8", $text)."<br />";

?>

如果字符串不包含特殊字符,您可能会得到不正确的结果,但这并不是问题。


根据我的经验,mb_detect_encoding() 完全不可靠。我过去尝试使用它,但它为许多字符串返回完全错误的编码。 - Richard Knop
@Richard,它应该可以使用这样一组可能编码的窄范围(UTF-8相对容易与ISO区分开来)...我们将看看它的运作情况。 - Pekka
5
根据我的经验,编码列表的顺序很重要。"UTF-8,ISO-8859-1"和"ISO-8859-1,UTF-8"会得到不同的结果。 - Dr.Molle
我因此而疯狂,非常感谢@Pekka 웃提供的解决方案。 - CIRCLE
1
对我而言,这是唯一的解决方案。在使用 SQL Server 数据库时遇到了一些问题。感谢 @Pekka웃 的分享! - Mário Rodrigues

9
我创建了一个函数来解决所有这些问题。它被称为Encoding::toUTF8()。
<?php
$text = $entity['Entity']['title'];
echo 'Original : ', $text."<br />";
echo 'Encoding::toUTF8 : ', Encoding::toUTF8($text)."<br />";
?>

输出:

Original : France Télécom
Encoding::toUTF8 : France Télécom

Original : Cond� Nast Publications
Encoding::toUTF8 : Condé Nast Publications

只要你知道字符串的编码方式是 Latin1(ISO 8859-1)、Windows-1252 或 UTF8 中的一种,就不需要知道字符串的编码方式。这个字符串也可以混合使用这些编码方式。

Encoding::toUTF8()会将所有内容转换为UTF8编码。

我这样做是因为一个服务给我提供了一条混杂着UTF8和Latin1的数据流。

用法:

$utf8_string = Encoding::toUTF8($utf8_or_latin1_or_mixed_string);

$latin1_string = Encoding::toLatin1($utf8_or_latin1_or_mixed_string);

下载:

http://dl.dropbox.com/u/186012/PHP/forceUTF8.zip

我又添加了一个函数 Encoding::fixUFT8(),可以修复所有看起来乱码的UTF8字符串。

用法:

$utf8_string = Encoding::fixUTF8($garbled_utf8_string);

示例:

echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("FÃÂédÃÂération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");

将输出:

Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football

谢谢!你能在GitHub上创建一个项目吗?我很乐意进行一些升级。 - danielpopa

6

另一种方式,也许更快且不太容易出错:

echo (strlen($str)!==strlen(utf8_decode($str)))
  ? $str                //is multibyte, leave as is
  : utf8_encode($str);  //encode

该代码比较原始字符串的长度和utf8解码后的字符串长度。 包含多字节字符的字符串,其strlen与类似的单字节编码strlen不同。

例如:

strlen('Télécom') 

应该在Latin1中返回7,在UTF8中返回9。

是的,这似乎是最好的选择。但在进行任何操作之前,他应该备份他的数据库 :) - Richard Knop
这种方法也可以直接应用于数据库中,通过即时转换字符集并比较字节长度来实现(我认为mySQL有一个函数可以做到这一点)...只是一个更快地修复数据库的想法。 - Pekka
@Lizard 我认为你实现错误了。你需要输出一个 utf8_decode 来查看它是否成功(你正在输出两次编码版本)。 - Pekka

1

我编写了这两个小函数,可以很好地处理UTF-8和ISO-8859-1的检测/转换...

function detect_encoding($string)
{
    //http://w3.org/International/questions/qa-forms-utf-8.html
    if (preg_match('%^(?: [\x09\x0A\x0D\x20-\x7E] | [\xC2-\xDF][\x80-\xBF] | \xE0[\xA0-\xBF][\x80-\xBF] | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} | \xED[\x80-\x9F][\x80-\xBF] | \xF0[\x90-\xBF][\x80-\xBF]{2} | [\xF1-\xF3][\x80-\xBF]{3} | \xF4[\x80-\x8F][\x80-\xBF]{2} )*$%xs', $string))
        return 'UTF-8';

    //If you need to distinguish between UTF-8 and ISO-8859-1 encoding, list UTF-8 first in your encoding_list.
    //if you list ISO-8859-1 first, mb_detect_encoding() will always return ISO-8859-1.
    return mb_detect_encoding($string, array('UTF-8', 'ASCII', 'ISO-8859-1', 'JIS', 'EUC-JP', 'SJIS'));
}

function convert_encoding($string, $to_encoding, $from_encoding = '')
{
    if ($from_encoding == '')
        $from_encoding = detect_encoding($string);

    if ($from_encoding == $to_encoding)
        return $string;

    return mb_convert_encoding($string, $to_encoding, $from_encoding);
}

如果您的数据库包含两种不同字符集的字符串,我建议您编写一个“一次性”脚本,而不是在所有应用程序代码中添加字符集检测/转换。该脚本将读取所有表记录并将其字符串更新为正确的格式(如果我是您,我会选择UTF-8)。这样,您的代码将更加清晰简洁,易于维护。
只需循环遍历数据库中每个表中的记录,并像这样转换字符串:
//if the 3rd param is not specified the "from encoding" is detected automatically
$newString = convert_encoding($oldString, 'UTF-8');

0

我没有在这里尝试过你的样例,但是从过去的经验来看,这个问题有一个快速解决方案。在数据库连接之后,在运行任何其他查询之前执行以下查询:

SET NAMES UTF8;

这是符合SQL标准的,可以与其他数据库(如Firebird和PostgreSQL)很好地配合使用。

但请记住,在其他位置上也要确保UTF-8声明,以使您的应用程序正常工作。遵循一个快速检查清单。

  • 所有文件都应保存为UTF-8(最好没有BOM [字节顺序掩码])
  • 您的HTTP服务器应发送编码头UTF-8。使用Firebug或Live HTTP Headers进行检查。
  • 如果您的服务器压缩和/或令牌化响应,则可以将标题内容显示为分块或gzip。如果您将文件保存为UTF-8并在HTML头中声明编码,则不会出现问题。
  • 在整个应用程序(套接字、文件系统、数据库等)中,不要忘记随时标记UTF-8。在打开数据库连接等操作时执行此操作可帮助您避免一直进行编码/解码/调试。抓住它们的根源。

不涉及问题的关键点。他有一个包含两种混合编码的数据集,而他不知道哪一行是哪种编码。 - Pekka
我明白了...我给出了一个总体的答案。对于这个情况来说不是很好,并且没有真正解决@Lizard的问题。@Pekka和@Dr.Molle的回答是正确的方向。需要一个函数来检测并根据需要进行转换。 - Davis Peixoto

-1
  1. 你使用哪个数据库?
  2. 在将原始字符串转换为utf-8之前,您需要知道其字符集,如果它是ISO-8859-1(latin1),则utf8_encode()是最简单的方法,否则您需要使用icov或mbstring库进行转换,并且这两者都需要知道输入的字符集才能正确转换。
  3. 插入/选择数据时,您是否告诉数据库字符集?

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