PHP:将任何字符串转换为UTF-8,而无需知道原始字符集,或者至少尝试转换

172

我有一个应用程序需要处理来自世界各地的客户数据,因此我希望数据库中的所有内容都采用UTF-8编码。

对我而言,主要问题是我不知道任何字符串来源的编码方式 - 它可能来自文本框(使用<form accept-charset="utf-8"> 只有在用户实际提交表单时才有用),或者它可能来自上传的文本文件,所以我无法控制输入。

我需要的是一个函数或类,可以确保进入我的数据库的内容尽可能采用UTF-8编码。我尝试过 iconv(mb_detect_encoding($text), "UTF-8", $text); 但这有些问题(如果输入是'fiancée',它会返回'fianc')。我已经尝试了很多方法 =/

对于文件上传,我喜欢让最终用户指定他们使用的编码方式,并显示输出的预览,但这并不能防范恶意黑客(事实上,这可能会让他们的生活更加轻松)。

我已经阅读了其他关于此问题的Stack Overflow问题,但它们似乎都有微妙的差别,例如“我需要解析RSS源”或“我从网站上提取数据”(或者,“你不行”)。

但至少有些东西可以好好“尝试”吧!


7
根据定义,绝对正确是基本不可能的,在现实中猜测未知编码的成功率也不是特别高。可以使用启发式方法,但它的正确率不会达到100%,具体取决于材料,远低于100%。你需要意识到这一点。也许有人能够至少建议一个具有良好启发式方法的库。 - deceze
当然,我知道没有完美的解决方案 - 因此希望有一些至少可以尝试的好东西。 - Grim...
请问客户使用的语言源(即本地化)不是更容易吗?从长远来看,这样可以避免麻烦。 - Alvin K.
当然,问题的一部分是非英语单词在英语文本中会相当频繁地出现(例如,“fiancée”),其他语言也存在同样的问题——我记得在学校时,法国曾经有过一场运动,旨在清除像“le weekend”这样的短语。 - Phil Lello
可能是重复的问题:检测编码并将所有内容转换为UTF-8 - That Brazilian Guy
显示剩余6条评论
12个回答

290

你所要求的内容非常困难。如果可能,最好让用户指定编码方式。这样预防攻击应该不会变得更加容易或更加困难。

不过,你可以尝试以下方法:

iconv(mb_detect_encoding($text, mb_detect_order(), true), "UTF-8", $text);

将其设置为 strict 可能有助于获得更好的结果。


6
请查看您的php发行版中mb_detect_encoding函数的源代码(可能在这里:ext/mbstring/libmbfl/mbfl/mbfl_ident.c)。该函数根本无法正常工作。对于某些编码,它甚至会返回"true",哈哈。其他编码则使用Ctrl+c Ctrl+v函数。这是因为您无法在没有某种字典或统计方法(如我的方法)的情况下检测编码。 - Oroboros102
2
据我理解,mb_detect_encoding会遍历提供的编码列表,并接受第一个在字符串中没有无效字节序列的编码... 对于那些没有无效字节序列的编码,如ISO-8859-1,它总是正确的。没有“智能”启发式算法,结果因传递的编码列表(及其顺序)而大不相同。 - wutz
@Jeff Day - 感谢您。请原谅我的无知,'Setting it to Strict' 是什么意思? - Ash501
Jeff Day正在发送mb_detect_order(),尽管这是该参数的默认值,因为他想将严格编码检测设置为true(第三个参数):) - jave.web
如果我运行建议的 iconv() 命令,然后在编码文本上运行 mb_detect_encoding($encoded_text, mb_detect_order(), true),我仍然得到 ASCII,而 iconv() 命令应该已经将其编码为 UTF-8... - kregus
显示剩余3条评论

31

在祖国俄罗斯,我们有四种流行的编码方式,因此您的问题在这里备受欢迎。

仅凭符号的字符代码无法检测编码方式,因为编码页相互交叉。不同语言中的一些编码页甚至完全相交。因此,我们需要另一种方法

处理未知编码方式的唯一方法是使用概率进行处理。因此,我们不想回答“这个文本的编码方式是什么?”,我们试图理解“这个文本最可能的编码方式是什么?”

在一个流行的俄罗斯技术博客中,有一个人发明了这种方法:

建立每种支持的编码方式中字符代码的概率范围。您可以使用您语言中的一些大型文本(例如某些小说,对于英语使用莎士比亚,对于俄语使用托尔斯泰,哈哈)。您将得到类似于以下内容:

    encoding_1:
    190 => 0.095249209893009,
    222 => 0.095249209893009,
    ...
    encoding_2:
    239 => 0.095249209893009,
    207 => 0.095249209893009,
    ...
    encoding_N:
    charcode => probabilty

接下来,你需要处理一个未知编码的文本,并针对你的“概率字典”中的每种编码搜索未知编码文本中每个符号的频率。将符号的概率相加。得分更高的编码很可能是获胜者。对于更长的文本,结果会更好。

顺便说一句,mb_detect_encoding 绝对不起作用。 这真的是事实。请查看“ext/mbstring/libmbfl/mbfl/mbfl_ident.c”中的 mb_detect_encoding 源代码。


16

可以直接使用mb_convert_encoding函数。它将尝试自动检测文本的字符集,或者您可以传递一个列表。

另外,我尝试运行:

$text = "fiancée";
echo mb_convert_encoding($text, "UTF-8");
echo "<br/><br/>";
echo iconv(mb_detect_encoding($text), "UTF-8", $text);

结果对于两者都是相同的。


在数据库中,看起来 - 我刚试了一下你的代码,我同意。 - Grim...
1
请检查您在表/列上定义的排序规则是否也是UTF-8。 - Alexey Gerasimov
@AlexeyGerasimov 我想我真的需要调查一下 iconv。我试过几乎纯粹使用 mb_* 的方式。你认为呢? - Anthony Rutledge

5

没有一种方法可以准确地识别字符串的字符集。

有一些尝试猜测字符集的方法。其中一种方法,也许是PHP中最好的方法,是mb_detect_encoding。这将扫描您的字符串并查找特定于某些字符集的内容。根据您的字符串,可能没有这样可区分的内容。

ISO-8859-1字符集与ISO-8859-15为例。

只有少量不同的字符,更糟糕的是,它们由相同的字节表示。如果没有知道该字符串的编码方式,就无法确定给定一个字符串中的字节0xA4是表示¤还是€,因此无法知道其精确的字符集。

(注意:您可以添加人为因素,或者更高级的扫描技术(例如Oroboros102建议的),以尝试根据周围环境确定字符应该是¤还是€,但这似乎过于超前。)
例如UTF-8和ISO-8859-1之间有更明显的区别,因此当您不确定时仍然值得尝试弄清楚它,但您永远不能依赖它是正确的。
有趣的阅读:如何确定字符串的字符集/编码? 有其他确保正确字符集的方法。对于表单,尽量使用UTF-8(可以查看雪人以确保您的提交在每个浏览器中都是UTF-8:Rails and Snowmen)。
这样做后,至少可以确定通过您的表单提交的每个文本都是utf_8。关于上传的文件,请尝试通过例如exec()(如果您的服务器允许)运行Unix的'file -i'命令来帮助检测(使用文档的BOM)。
在爬取数据时,您可以读取HTTP标头,通常会指定字符集。在解析XML文件时,请查看XML元数据是否包含字符集定义。
与其尝试自动猜测字符集,您应该首先尝试在可能的情况下确保某种字符集,或在识别之前尝试从获取数据的源(如果适用)中获取定义。

表单和带有加密数据的电子邮件注册链接。这就是我试图让我的输入成为UTF-8或什么都不是的地方。你觉得我的回答怎么样?欢迎提供有用的评论。谢谢。 - Anthony Rutledge

3
这里有一些非常好的回答和尝试回答你的问题。虽然我不是编码大师,但我理解你想要在整个堆栈中使用纯UTF-8字符集的愿望。我一直在使用MySQL的utf8mb4编码来处理表格、字段和连接。
我的情况可以简化为“当数据来自HTML表单或电子邮件注册链接时,我只想让我的过滤器、验证器、业务逻辑和预设语句处理UTF-8”。因此,用我的简单方式开始了这个想法:
  1. 尝试检测编码:$encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];

  2. 如果无法检测到编码,则throw new RuntimeException

  3. 如果输入是UTF-8,则继续。

  4. 否则,如果输入是ISO-8859-1或者ASCII

    a. 尝试转换为 UTF-8(等待,未完成)

    b. 检测转换后值的编码

    c. 如果报告的编码和转换后的值都是UTF-8,则继续。

    d. 否则,throw new RuntimeException

来自我的抽象类Sanitizer

Sanitizer

    private function isUTF8($encoding, $value)
    {
        return (($encoding === 'UTF-8') && (utf8_encode(utf8_decode($value)) === $value));
    }

    private function utf8tify(&$value)
    {
        $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];

        mb_internal_encoding('UTF-8');
        mb_substitute_character(0xfffd); //REPLACEMENT CHARACTER
        mb_detect_order($encodings);

        $stringEncoding = mb_detect_encoding($value, $encodings, true);

        if (!$stringEncoding) {
            $value = null;
            throw new \RuntimeException("Unable to identify character encoding in sanitizer.");
        }

        if ($this->isUTF8($stringEncoding, $value)) {
            return;
        } else {
            $value = mb_convert_encoding($value, 'UTF-8', $stringEncoding);
            $stringEncoding = mb_detect_encoding($value, $encodings, true);

            if ($this->isUTF8($stringEncoding, $value)) {
                return;
            } else {
                $value = null;
                throw new \RuntimeException("Unable to convert character encoding from ISO-8859-1, or ASCII, to UTF-8 in Sanitizer.");
            }
        }

        return;
    }

有人可能会认为我应该将编码问题与我的抽象Sanitizer类分开,并简单地将一个Encoder对象注入到Sanitizer的具体子实例中。然而,我这种方法的主要问题在于,缺乏更多的知识,我只是拒绝我不想要的编码类型(我依赖于PHP mb_*函数)。没有进一步的研究,我无法知道这是否会对某些人口造成伤害或者是否会失去重要信息。因此,我需要学习更多。我发现了这篇文章。

每个程序员都绝对需要了解关于编码和字符集以处理文本的内容

此外,如果在我的电子邮件注册链接中添加了使用OpenSSL或mcrypt加密的数据,会发生什么?这是否会干扰解码?Windows-1252有什么影响?安全方面的影响如何?在Sanitizer :: isUTF8中使用utf8_decode()和utf8_encode()是可疑的。人们指出了PHP mb_*函数的缺点。我从未花时间调查iconv,但如果它比mb_*函数更好,请告诉我。

我发现这个问题的优秀答案在这里:https://dev59.com/V3M_5IYBdhLWcg3wZSE6#3521396,这是库https://github.com/neitanod/forceutf8。 - Llewellyn

2
似乎你的问题已经得到了回答,但我有一个方法可能能简化你的情况:
我曾经遇到过一个类似的问题,尝试从MySQL返回字符串数据,即使将数据库和PHP都配置为返回UTF-8格式的字符串,我仍然会遇到错误。直到我从数据库中返回它们,我才发现这个错误。
最后,通过浏览网页,我找到了一个真正简单的处理方式:
假设你可以在MySQL中以不同的格式和排序保存所有这些类型的字符串数据,那么你只需要在你的php连接文件中设置字符集为UTF-8,像这样:
$connection = new mysqli($server, $user, $pass, $db);
$connection->set_charset("utf8");

这意味着您首先以任何格式或排序保存数据,只有在返回到您的PHP文件时才进行转换。

2
我的主要问题是我不知道任何字符串的源编码将会是什么 - 它可能来自文本框(使用只有在用户实际提交表单时才有用),或者它可能来自上传的文本文件,所以我对输入没有任何控制。
我认为这不是一个问题。应用程序知道输入的来源。如果是来自表单,请在您的情况下使用UTF-8编码。那样可以工作。只需验证提供的数据是否正确编码(验证)。请记住,并非所有数据库都支持UTF-8的全部范围。
如果是文件,则不会将其保存为UTF-8编码到数据库中,而是以二进制形式保存。当您再次输出文件时,也要使用二进制输出,那么这完全透明。
您的想法很好,用户可以告诉编码,但无论如何,在下载文件后他/她都可以告诉,因为它是二进制的。
所以我必须承认,我没有看到您在问题中提出的具体问题。

你能否看出我的回答有什么问题吗?欢迎提供建设性的评论。谢谢。 - Anthony Rutledge

2

由于UTF-8的使用非常普遍,您可以将其视为默认值,如果不是,则尝试猜测并转换编码。以下是代码:

function make_utf8(string $string)
{
    // Test it and see if it is UTF-8 or not
    $utf8 = \mb_detect_encoding($string, ["UTF-8"], true);

    if ($utf8 !== false) {
        return $string;
    }

    // From now on, it is a safe assumption that $string is NOT UTF-8-encoded

    // The detection strictness (i.e. third parameter) is up to you
    // You may set it to false to return the closest matching encoding
    $encoding = \mb_detect_encoding($string, mb_detect_order(), true);

    if ($encoding === false) {
        throw new \RuntimeException("String encoding cannot be detected");
    }

    return \mb_convert_encoding($string, "UTF-8", $encoding);
}

简单、安全、快速。


哈哈,哇,我11年前问过这个问题(而且我真的不记得为什么了)!不过还是谢谢你的回答,很有趣。我有一个问题,只是因为我感兴趣——为什么要用!== false而不是=== true - Grim...
@Grim...,因为\mb_detect_encoding()的返回类型在这种情况下是string|false(即stringfalse),它永远不可能等于true。也许你现在正在使用一种强类型语言编写代码。 ;) - MAChitgarha
哈哈哈,我好久没用 PHP 了!得承认,我可能只会用 if (!$utf8),因为我有点懒 :-) - Grim...

1

有几个相关的库。onnov/detect-encoding 看起来很有前途。它声称比 mb_detect_encoding 做得更好。

将未知字符编码的字符串转换为 UTF-8 的示例用法:

use Onnov\DetectEncoding\EncodingDetector;
$detector->iconvXtoEncoding('Проверяемый текст')

简单检测编码:

$encoding = $detector->getEncoding('Проверяемый текст');

1

你可以设置一组指标来猜测使用的编码方式。虽然不完美,但它可以捕捉到 mb_detect_encoding() 无法识别的一些情况。


说到mb_detect_encoding()的缺陷,你认为我的回答在撒哈拉沙漠的夏天有雪球的机会吗? - Anthony Rutledge

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