PHP: 在不知道原始字符集的情况下将任何字符串转换为 UTF-8,或者至少尝试一下

我有一个处理来自世界各地的客户端的应用程序,自然,我希望进入我的数据库的所有内容都是 UTF-8编码的。

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

我需要的是一个函数或类,确保进入我的数据库的内容尽可能是 UTF-8编码的。我试过 iconv(mb_detect_encoding($text), "UTF-8", $text); 但是这样有问题(如果输入是“未婚妻”,它会返回“未婚妻”)

对于文件上传,我喜欢要求终端用户指定他们使用的编码,并向他们展示输出的预览,但这并不能帮助对付讨厌的黑客(事实上,它可以使他们的生活更容易一点)。

我已经阅读了关于这个主题的其他 Stack Overflow 问题,但它们似乎都有细微的差别,比如“我需要解析 RSS提要”或“我从网站上获取数据”(或者,实际上,“你不能”)。

但是,必须有一些至少有一个良好的 试试看

339021 次浏览

你的要求非常艰难。如果可能的话,让用户指定编码是最好的。防止攻击不应该是更容易或更难的方式。

然而,你可以尝试这样做:

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

将它设置为严格可能会帮助您获得更好的结果。

您可以设置一组指标来尝试猜测正在使用哪种编码。同样,它也不是完美的,但是它可能会捕获 mb _ Detect_ coding ()遗漏的一些内容。

只要使用 编码函数。它将尝试自动检测所提供文本的字符集,或者您可以将列表传递给它。

而且,我试图逃跑:

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

两者的结果是一样的。

在祖国俄罗斯,我们有四种流行的编码,所以你的问题在这里很受欢迎。

只有通过符号的字符代码才能检测到编码,因为代码页是相交的。一些不同语言的代码页甚至有完全的交集。那么,我们需要另一种方法

处理未知编码的唯一方法就是处理概率。因此,我们不想回答“这个文本的编码是什么”这个问题,我们正在试图理解“ 这个文本最可能的编码是什么?”。

一个人在俄罗斯科技博客上发明了这种方法:

在您希望支持的每种编码中构建字符代码的概率范围。你可以使用你的语言中的一些大文本来构建它(例如,一些小说,英语使用 莎士比亚,俄语使用 托尔斯泰,LOL)。你会得到这样的东西:

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

接下来,使用未知编码获取文本,对于“概率字典”中的每个编码,搜索未知编码文本中每个符号的频率。把符号的概率加起来。使用较大等级的编码可能是赢家。对于更大的文本有更好的结果。

顺便说一句,Mb _ 阶段检测 _ 编码肯定不能用,是的,根本不能用,请看一下“ ext/mbstring/libmbfl/mbfl/mbfl _ id.c”中的 mb _ check _ coding 源代码。

无法标识完全准确的字符串字符集。

有一些方法可以尝试猜测字符集。其中一种方法是 Mb _ 阶段检测 _ 编码,它可能是/目前 PHP 中最好的方法。这将扫描您的字符串,并寻找特定字符集所特有的东西。根据字符串的不同,可能不会出现这种可区分的情况。

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

只有几个不同的字符,更糟糕的是,它们用相同的字节表示。在不知道字符串编码的情况下,无法检测字节0xA4是应该表示字符串还是应该表示字符串中的 something,因此无法知道它的确切字符集。

(注意: 你可以添加一个人为因素,或者一个更高级的扫描技术(例如,什么是 Oroboros102建议) ,试图根据周围的环境来判断字符是否应该是 something,尽管这似乎有点太过了

比如 UTF-8和 ISO-8859-1之间有更多的区别,所以当你不确定的时候,仍然值得尝试去弄清楚它,尽管你可以也不应该依赖它是正确的。

有趣的阅读: 如何确定字符串的字符集/编码

不过,还有其他确保正确字符集的方法。关于表单,尽可能地强制使用 UTF-8(查看“雪人”以确保在每个浏览器中提交的内容都是 UTF-8: Rails 和雪人)

这样做,至少您可以确保通过您的表单提交的每个文本是 Utf _ 8。关于上传的文件,尝试通过运行 Unix‘ file-i’命令,例如,执行()(如果可能的话,在您的服务器上)来帮助检测(使用文档的 BOM)。

关于刮取数据,您可以读取 HTTP 头,它通常指定字符集。解析 XML 文件时,请查看 XML 元数据是否包含字符集定义。

不要试图自动猜测字符集,你应该首先尽可能地确保你自己有一个特定的字符集,或者试图从你得到它的源(如果适用的话)中获取一个定义,然后再进行检测。

对我来说,主要的问题是我不知道任何字符串的源代码是什么——它可能来自一个文本框(使用只有在用户实际提交表单时才有用) ,或者它可能来自一个上传的文本文件,所以我真的无法控制输入。

我觉得没问题。应用程序知道输入源。如果它来自某个表单,则在本例中使用 UTF-8编码。这样就行了。只需验证提供的数据是否被正确编码(验证)。请记住,并非所有数据库都全面支持 UTF-8。

如果它是一个文件,你不会保存它的 UTF-8编码到数据库,但在二进制形式。当您再次输出文件时,也使用二进制输出,那么这是完全透明的。

你的想法是好的,用户可以告诉编码,无论他/她可以告诉后下载的文件,因为它是二进制。

所以我必须承认,我没有看到你的问题提出了一个具体的问题。

如果你愿意“把这个带到控制台”,我推荐 enca。与相当简单的 mb_detect_encoding不同,它使用“解析、统计分析、猜测和黑魔法的混合来确定它们的编码”(lol-see 手册)。但是,如果要检测这种国家特有的编码,通常必须传递输入文件的语言。(然而,mb_detect_encoding本质上具有相同的要求,因为编码必须在传递的编码列表中出现在“正确的位置”才能被检测到。)

enca也出现在这里: 如何通过脚本在 Unix 中找到文件的编码

这里有一些非常好的答案和尝试来回答你的问题。我不是一个编码大师,但我理解您的愿望,有一个 纯洁 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-1ASCII

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

    检测转换值的编码

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

    否则,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 _ * 函数)。没有进一步的研究,我无法知道这是否会伤害某些人群(或者,我是否正在失去重要的信息)。所以,我需要了解更多。我找到了这篇文章。

每个程序员在处理文本时绝对需要了解的编码和字符集

此外,当加密数据添加到我的电子邮件注册链接(使用 OpenSSLmcrypt)会发生什么?这会影响解码吗?视窗 -1252呢?安全隐患呢?在 Sanitizer::isUTF8中使用 utf8_decode()utf8_encode()是值得怀疑的。

人们已经指出了 PHP mb _ * 函数的缺点。我从未花时间研究过 iconv,但如果它比 mb _ * 函数工作得更好,请告诉我。

看来你的问题已经得到了很好的回答,但是我有一个办法可以简化你的情况:

我在尝试从 MySQL 返回字符串数据时遇到了类似的问题,甚至将数据库和 PHP 都配置为返回格式为 UTF-8的字符串。我得到这个错误的唯一方法就是从数据库中返回它们。

最后,在浏览网页的过程中,我找到了一个非常简单的方法来处理这个问题:

假设您可以在 MySQL 中以不同的格式和排序规则保存所有这些类型的字符串数据,那么您只需要在 php 连接文件中将排序规则设置为 UTF-8,如下所示:

$connection = new mysqli($server, $user, $pass, $db);
$connection->set_charset("utf8");

这意味着首先以任何格式或排序方式保存数据,然后只在返回到 PHP 文件时才将其转换。

如果文本是从 MySQL 数据库中检索到的,您可以尝试在数据库连接之后添加该文本。

mysqli_set_charset($con, "utf8");

Set _ charset

有几个库,检测编码看起来很有前途,它声称比 Mb _ 阶段检测 _ 编码做得更好

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

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

简单地检测编码:

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

因为 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);
}

简单,安全,快捷。