检测编码并将所有内容转换为UTF-8。

331

我正在从各种RSS源中读取大量文本并将其插入我的数据库。

当然,这些源使用了几种不同的字符编码,例如UTF-8和ISO 8859-1。

不幸的是,有时候文本的编码存在问题。例如:

  1. "Fußball"中的"ß"在我的数据库中应该看起来像这样:"Ÿ"。如果它是"Ÿ",那么显示就正确了。

  2. 有时,在我的数据库中,"Fußball"中的"ß"看起来像这样:"ß"。然后当然会显示错误。

  3. 在其他情况下,"ß"保存为"ß"——没有任何更改。然后它也会显示错误。

我该怎么避免第2和第3种情况?

如何使一切都使用相同的编码,最好是UTF-8?我什么时候应该使用utf8_encode(),什么时候应该使用utf8_decode()(它们的效果很清楚,但我什么时候应该使用这些函数?),什么时候不需要对输入进行任何操作?

我如何使一切都使用相同的编码?或许可以使用mb_detect_encoding()函数吗?我能为此编写一个函数吗?所以我的问题是:

  1. 如何找出文本正在使用的编码方式?
  2. 无论旧编码方式是什么,如何将其转换为UTF-8?

类似于这样的函数是否有效?

function correct_encoding($text) {
    $current_encoding = mb_detect_encoding($text, 'auto');
    $text = iconv($current_encoding, 'UTF-8', $text);
    return $text;
}

我已经测试过了,但它不起作用。有什么问题吗?


41
"Fußball" 中的 "ß" 在我的数据库中应该是这样的:"Ÿ"。不,它应该看起来像 ß。确保您的排序规则和连接设置正确。否则,您的排序和搜索将会出现问题。 - Rich Bradshaw
5
你的数据库设置有问题。如果想要存储Unicode内容,只需为其配置即可。因此,不要试图在PHP代码中解决这个问题,你应该先修复数据库。 - dolmen
2
使用:$from = mb_detect_encoding($text); $text = mb_convert_encoding($text,'UTF-8',$from); - Informate.it
26个回答

386
如果您对已经是UTF-8字符串应用utf8_encode(),它将返回乱码的UTF-8输出。
我编写了一个处理所有这些问题的函数,它叫做Encoding::toUTF8()
您不需要知道字符串的编码方式。它可以是Latin1(ISO 8859-1Windows-1252)或UTF-8,或者字符串可以混合使用它们。 Encoding::toUTF8()将转换为UTF-8。
我这样做是因为某个服务向我提供的数据源全部搞砸了,在同一字符串中混合使用UTF-8和Latin1。
用法:
require_once('Encoding.php');
use \ForceUTF8\Encoding;  // It's namespaced now.

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

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

下载:

https://github.com/neitanod/forceutf8

我已经添加了另一个函数Encoding::fixUFT8(),它可以修复所有看起来混乱的UTF-8字符串。

用法:

require_once('Encoding.php');
use \ForceUTF8\Encoding;  // It's namespaced now.

$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

我已经将函数(forceUTF8)转换为一个名为Encoding的类上的一组静态函数。新函数为Encoding::toUTF8()

3
这个转换如何将非UTF8字符转换为UTF8字符,而不需要事先知道无效字符的编码是什么? - philfreo
4
它假设使用ISO-8859-1编码,答案已经说了这一点。 forceUTF8()和utf8_encode()之间唯一的区别是forceUTF8()会识别UTF8字符并保持它们不变。 - Sebastián Grignoli
31
"你不需要知道你的字符串编码是什么。"——我非常不同意。猜测和尝试可能有效,但你迟早会遇到无法解决的边缘情况。" - deceze
4
我完全同意。事实上,我并不是想要陈述一个普遍规则,只是想说明,如果你碰巧处于这种情况,这门课程可能会对你有所帮助。 - Sebastián Grignoli
1
我的正则表达式检查一个字符串是否由UTF-8字符从开头到结尾组成。这与你所做的不同,但我认为允许混合编码不是一个好主意。 - Walter Tross
显示剩余20条评论

79
你首先需要检测所使用的编码方式。由于你可能是通过HTTP解析RSS源,因此应该从Content-Type HTTP header fieldcharset参数中读取编码方式。如果没有出现该参数,则应从XML processing instructionencoding属性中读取编码方式。如果那也不存在,则应按照规范使用UTF-8编码

这是我可能会做的:

我会使用cURL发送和获取响应。这样可以设置特定的头字段并获取响应头。在获取响应后,您需要解析HTTP响应并将其拆分为头和正文。头应该包含Content-Type头字段,其中包含MIME类型和(希望)编码/字符集的charset参数。如果没有,则我们将分析XML PI以检查encoding属性是否存在,并从中获取编码。如果也缺失了,XML规范定义使用UTF-8作为编码。

$url = 'http://www.lr-online.de/storage/rss/rss/sport.xml';

$accept = array(
    'type' => array('application/rss+xml', 'application/xml', 'application/rdf+xml', 'text/xml'),
    'charset' => array_diff(mb_list_encodings(), array('pass', 'auto', 'wchar', 'byte2be', 'byte2le', 'byte4be', 'byte4le', 'BASE64', 'UUENCODE', 'HTML-ENTITIES', 'Quoted-Printable', '7bit', '8bit'))
);
$header = array(
    'Accept: '.implode(', ', $accept['type']),
    'Accept-Charset: '.implode(', ', $accept['charset']),
);
$encoding = null;
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$response = curl_exec($curl);
if (!$response) {
    // error fetching the response
} else {
    $offset = strpos($response, "\r\n\r\n");
    $header = substr($response, 0, $offset);
    if (!$header || !preg_match('/^Content-Type:\s+([^;]+)(?:;\s*charset=(.*))?/im', $header, $match)) {
        // error parsing the response
    } else {
        if (!in_array(strtolower($match[1]), array_map('strtolower', $accept['type']))) {
            // type not accepted
        }
        $encoding = trim($match[2], '"\'');
    }
    if (!$encoding) {
        $body = substr($response, $offset + 4);
        if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $body, $match)) {
            $encoding = trim($match[1], '"\'');
        }
    }
    if (!$encoding) {
        $encoding = 'utf-8';
    } else {
        if (!in_array($encoding, array_map('strtolower', $accept['charset']))) {
            // encoding not accepted
        }
        if ($encoding != 'utf-8') {
            $body = mb_convert_encoding($body, 'utf-8', $encoding);
        }
    }
    $simpleXML = simplexml_load_string($body, null, LIBXML_NOERROR);
    if (!$simpleXML) {
        // parse error
    } else {
        echo $simpleXML->asXML();
    }
}

谢谢。这很容易。但它真的能行吗?在HTTP头或XML属性中经常会出现错误的编码。 - caw
26
再说一遍:那不是你的问题。标准的制定是为了避免这类麻烦。如果别人没有遵守标准,那就是他们的问题,与你无关。 - Gumbo
首先,您正在进行两个请求,一个是HTTP头部,另一个是数据。其次,您正在寻找任何出现charset=encoding=而不仅仅是在适当的位置。第三,您没有检查声明的编码是否被接受。 - Gumbo
如果 $match[2] 已设置,则一切正常。但如果 $match[2] 未设置,该怎么办?返回 false 吗? - caw
但是它不能防止块错误的编码/字符集,因为下面这行不是elseif而是普通的if,对吧?那么这行可以被删除而不改变任何东西,对吗? - caw
显示剩余9条评论

44
检测编码很困难。 mb_detect_encoding 函数是通过猜测一系列候选项来判断编码的。在某些编码中,某些字节序列是无效的,因此它可以区分各种候选项。不幸的是,有许多编码使用相同的字节但是仍然是有效编码。在这些情况下,没有办法确定编码;您可以实现自己的逻辑来进行猜测。例如,来自日本网站的数据更可能具有日语编码。
只要您处理的是西欧语言,需要考虑的三种主要编码是utf-8iso-8859-1cp-1252。由于这些是许多平台的默认值,因此错误报告最有可能。例如,如果人们使用不同的编码,他们通常会坦诚,因为否则他们的软件会经常出错。因此,一个好的策略是信任提供者,除非编码报告为其中三个之一。您仍应该使用mb_check_encoding双重检查其是否有效(注意有效存在不同-相同的输入可能对应多种编码)。如果是其中之一,则可以使用mb_detect_encoding来区分它们。幸运的是,这是相当确定性的;您只需要使用适当的检测序列,即UTF-8,ISO-8859-1,WINDOWS-1252
检测到编码后,需要将其转换为内部表示形式(UTF-8是唯一明智的选择)。函数utf8_encodeISO-8859-1转换为UTF-8,因此只能用于该特定输入类型。对于其他编码,请使用mb_convert_encoding

8
我刚刚看到:mb-detect-encoding() 毫无用处。它只支持 UTF-8、UTF-7、ASCII、EUC-JP、SJIS、eucJP-win、SJIS-win、JIS 和 ISO-2022-JP 这些编码方式。对我来说最重要的 ISO-8859-1 和 WINDOWS-1252 并不被支持。因此,我无法使用 mb-detect-encoding()。 - caw
1
我的确很久没有使用它了,你说得对。你将不得不编写自己的检测代码,或者使用外部实用程序。UTF-8 可以相当可靠地确定,因为其转义序列非常特征化。wp-1252 和 iso-8859-1 可以区分,因为 wp-1252 可能包含在 iso-8859-1 中不合法的字节。使用维基百科获取详细信息,或查看 php.net 的注释部分,在各种字符集相关函数下。 - troelskn
似乎 mb_detect_encoding 支持 ISO-8859-* 和 Windows-1252。 - chim
除非你知道更好的方法,否则请测试你的输入是否为有效的UTF-8字符串,如果不是,则盲目地从Windows-1252转换为UTF-8。这通常适用于西欧语言,因为如果输入恰好是ISO-8859-1,则它是Windows-1252的子集,转换将是正确的。唯一真正棘手的问题是ISO-8859-15,在该编码中,欧元符号(“€”)位于0xA4位置,而Windows-1252在相同位置具有通用货币符号(“¤”)。你可以应用一些启发式算法来决定ISO-8859-15和Windows-1252之间的区别,但你永远无法确定。 - Mikko Rantalainen
@MikkoRantalainen windows-1252虽然与iso-8859-1几乎相同,但并不是其子集(Notably一些引号字符)。 - troelskn
显示剩余8条评论

14
这份备忘录列出了 PHP 中与 UTF-8 处理相关的一些常见注意事项: http://developer.loftdigital.com/blog/php-utf-8-cheatsheet

此函数能够检测字符串中的多字节字符,可能会有所帮助(source)。


function detectUTF8($string)
{
    return preg_match('%(?:
        [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
        |\xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
        |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
        |\xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
        |\xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
        |[\xF1-\xF3][\x80-\xBF]{3}         # planes 4-15
        |\xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
        )+%xs', 
    $string);
}


2
我认为这个不正确:echo detectUTF8('3٣3'); # 1 - Yousha Aleayoub

11

提醒一下,您说在数据库中“ß”应该显示为“Ÿ”。

这可能是因为您正在使用Latin-1字符编码的数据库,或者可能是您的PHP-MySQL连接设置错误。这意味着PHP认为MySQL已设置为使用UTF-8,因此将数据发送为UTF-8,但是MySQL却认为PHP正在发送作为ISO 8859-1编码的数据,因此可能会再次尝试将发送的数据编码为UTF-8,引起这种问题。

请看一下mysql_set_charset。它可能会有所帮助。


我必须运行 $mysqli->query("SET CHARACTER SET UTF8"); - Jurakin
我必须运行$mysqli->query("SET CHARACTER SET UTF8"); - undefined

6
你的编码看起来像是被编码成了UTF-8 两次; 也就是说,从其他编码形式转换为UTF-8,然后再次转换为UTF-8。就好像你有ISO 8859-1,将其从ISO 8859-1转换为UTF-8,然后将新字符串视为ISO 8859-1再进行另一次转换为UTF-8。
以下是你所做的一些伪代码:
$inputstring = getFromUser();
$utf8string = iconv($current_encoding, 'utf-8', $inputstring);
$flawedstring = iconv($current_encoding, 'utf-8', $utf8string);

您应该尝试:
  1. 使用mb_detect_encoding()或其他喜欢使用的方法检测编码
  2. 如果是UTF-8,转换为ISO 8859-1,并重复第一步
  3. 最后,再转回UTF-8
这是假设在“中间”转换中使用了ISO 8859-1。如果使用了Windows-1252,则将其转换为Windows-1252(Latin1)。原始源编码不重要;您在有缺陷的第二次转换中使用的编码才重要。
这是我猜测发生的事情;除此之外,您几乎无法用一个扩展的ASCII字节得到四个字节。
德语也使用ISO 8859-2Windows-1250(Latin-2)。

5

php.net上可以找到一种非常好的实现isUTF8函数的方法:

function isUTF8($string) {
    return (utf8_encode(utf8_decode($string)) == $string);
}

24
很遗憾,这只适用于字符串仅由ISO-8859-1包含的字符组成。但是,这个方法可以奏效:@iconv('utf-8', 'utf-8//IGNORE', $str) == $str。 - Christian Davén
@Christian:确实,这也是《高性能MySQL》的作者推荐的。 - Alix Axel
1
它不能正常工作:echo (int)isUTF8(' z'); # 1 echo (int)isUTF8(NULL); # 1 - Yousha Aleayoub
1
虽然不完美,但我认为这是一种不错的实现草图式UTF-8检查的方式。 - Mateng
2
mb_check_encoding($string, 'UTF-8') - deceze
6
为了让你明白这将有多么糟糕:ISO 8859-1 中恰好有191个可打印字符;Unicode 13 定义了大约140,000个字符。因此,如果你随机选择一个 Unicode 字符,以正确的 UTF-8 编码传递给这个函数,那么这个函数错误返回的概率超过99%。如果你认为这些是晦涩难懂的字符,请注意 ISO 8859-1 没有欧元符号,所以 isUTF8('€') 也会在这99%里面。 - IMSoP

4
关于 mb_detect_encodingmb_convert_encoding 的有趣之处在于,您建议的编码顺序确实很重要:
// $input is actually UTF-8

mb_detect_encoding($input, "UTF-8", "ISO-8859-9, UTF-8");
// ISO-8859-9 (WRONG!)

mb_detect_encoding($input, "UTF-8", "UTF-8, ISO-8859-9");
// UTF-8 (OK)

所以,当指定预期编码时,您可能希望使用特定的顺序。但是,请记住,这并不是百分百可靠的。

2
这是因为ISO-8859-9实际上会接受任何二进制输入。Windows-1252和其他编码也是如此。您必须首先测试可能无法接受输入的编码。 - Mikko Rantalainen
@MikkoRantalainen,是的,我想文档的这一部分说的类似:http://php.net/manual/en/function.mb-detect-order.php#example-2985 - Halil Özgür
考虑到WHATWG HTML规范将Windows 1252定义为默认编码,因此可以非常安全地假设if ($input_is_not_UTF8) $input_is_windows1252 = true;。另请参见:https://html.spec.whatwg.org/multipage/parsing.html#determining-the-character-encoding - Mikko Rantalainen

2

mb_detect_encoding:

echo mb_detect_encoding($str, "auto");

或者

echo mb_detect_encoding($str, "UTF-8, ASCII, ISO-8859-1");

我不知道具体结果是什么,但我建议你尝试使用不同编码的源数据,并检查mb_detect_encoding是否生效。 auto代表"ASCII,JIS,UTF-8,EUC-JP,SJIS"。该函数将返回检测到的字符集,你可以使用iconv将字符串转换为UTF-8编码。请注意保留HTML标签。
<?php
function convertToUTF8($str) {
    $enc = mb_detect_encoding($str);

    if ($enc && $enc != 'UTF-8') {
        return iconv($enc, 'UTF-8', $str);
    } else {
        return $str;
    }
}
?>

我没有测试过,所以不能保证其准确性。也许有更简单的方法。

谢谢。'auto'和'UTF-8、ASCII、ISO-8859-1'作为第二个参数有什么区别?'auto'是否支持更多的编码方式?那么使用'auto'会更好,对吗?如果真的没有任何错误,那么我只需要将"ASCII"或"ISO-8859-1"更改为"UTF-8"。如何更改? - caw
2
你的函数在所有情况下都不能很好地工作。有时我会收到一个错误提示: 注意:iconv():在...中检测到输入字符串中的非法字符 - caw

2

由于响应可能使用不同的编码进行编码,因此您需要测试输入中的字符集。

我使用以下函数进行检测和转换,强制将所有发送的内容转换为UTF-8:

function fixRequestCharset()
{
  $ref = array(&$_GET, &$_POST, &$_REQUEST);
  foreach ($ref as &$var)
  {
    foreach ($var as $key => $val)
    {
      $encoding = mb_detect_encoding($var[$key], mb_detect_order(), true);
      if (!$encoding)
        continue;
      if (strcasecmp($encoding, 'UTF-8') != 0)
      {
        $encoding = iconv($encoding, 'UTF-8', $var[$key]);
        if ($encoding === false)
          continue;
        $var[$key] = $encoding;
      }
    }
  }
}

这个例程将把来自远程主机的所有 PHP 变量转换为 UTF-8 编码。

如果无法检测或转换编码,则忽略该值。

您可以根据自己的需要进行定制。

在使用变量之前,只需调用它即可。


不传递编码列表时,使用mb_detect_order()的目的是什么? - giorgio79
目的是返回系统配置的有序编码数组,这些编码在php.ini中定义并被使用。mb_detect_encoding需要这个数组来填充第三个参数。 - cavila

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