确保PHP中的有效UTF-8

26

我正在使用PHP处理来自各种来源的文本。我预计这些文本都是UTF-8、ISO 8859-1或者Windows-1252编码。如果它们不是这三种编码之一,我只需要确保该文本转换为有效的UTF-8字符串,即使存在某些字符丢失的情况。iconv中的//TRANSLIT选项能够解决这个问题吗?

例如,以下代码是否能确保将一个字符串安全地插入到UTF-8编码的文档(或数据库)中?

function make_safe_for_utf8_use($string) {

    $encoding = mb_detect_encoding($string, "UTF-8,ISO-8859-1,WINDOWS-1252");

    if ($encoding != 'UTF-8') {
        return iconv($encoding, 'UTF-8//TRANSLIT', $string);
    }
    else {
        return $string;
    }
}
5个回答

40

UTF-8可以存储任何Unicode字符。如果您的编码是其他任何编码,包括ISO-8859-1或Windows-1252,UTF-8都可以存储其中的每个字符。因此,当您将字符串从任何其他编码转换为UTF-8时,您不必担心会丢失任何字符。

此外,ISO-8859-1和Windows-1252都是单字节编码,其中任何字节都是有效的。在技术上无法区分它们。我建议您将Windows-1252选择为非UTF-8序列的默认匹配项,因为唯一解码不同的字节是范围0x80-0x9F。这些解码为各种字符,如Windows-1252中的智能引号和欧元符号,而在ISO-8859-1中,它们是几乎从不使用的不可见控制字符。Web浏览器有时可能会说它们正在使用ISO-8859-1,但通常它们实际上正在使用Windows-1252。

这段代码是否确保字符串安全插入到UTF-8编码的文档中?

为此,您肯定要将可选参数“strict”设置为TRUE。但我不确定这是否实际上涵盖了所有无效的UTF-8序列。该函数没有明确声明检查字节序列的UTF-8有效性。以前已知mb_detect_encoding会在严格模式下错误地猜测UTF-8,尽管我不知道现在是否仍然可能发生。

如果您想确保,请使用W3推荐的正则表达式自行检查:W3-recommended regex

if (preg_match('%^(?:
      [\x09\x0A\x0D\x20-\x7E]            # ASCII
    | [\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))
    return $string;
else
    return iconv('CP1252', 'UTF-8', $string);

1
它会返回一个UTF-8安全的字符串,但非ASCII字符会显示错误字符,不过不会有危险。 - bobince
3
实际上,这个正则表达式是错误的。它将无法匹配有效的UTF-8代码点(例如chr(0))。对于可打印字符来说还好,但不适用于通用的UTF-8... - ircmaxell
请注意,由于复杂的正则表达式导致PCRE崩溃,此答案可能会引起许多情况的问题:https://bugs.php.net/bug.php?id=36463。它是正确的,但有时候不起作用。对我来说没有用,使用ini_set('mbstring.substitute_character',"none"); $utf8_string = mb_convert_encoding($string,'UTF-8','UTF-8'); - redreinard
@redreinard:哇,这很令人惊讶。虽然表达式看起来很棘手,但从正则表达式的角度来看,它实际上非常简单——没有高级功能,也没有回溯的可能性;不需要递归。有一个关于该错误的评论说,即使是^(a)+$也无法处理203字节的输入……这肯定不能被期望或接受吧?就我所知,在使用PCRE的R中它似乎工作得很好。我认为Rasmus忽略了一个真正的问题。 :-( - bobince
同样遇到这个问题,似乎在处理一些稍微大一点的东西时会失败,比如现代网页的HTML。 - Brian Leishman
显示剩余5条评论

19

使用 mbstring 库,可以使用 mb_check_encoding() 函数。

使用示例:

mb_check_encoding($string, 'UTF-8');

然而,在一个最近的Windows 10系统上,使用PHP 7.1.9版本,正则表达式解决方案现在在任何字符串长度下都优于mb_check_encoding()函数(测试了20000次迭代):

  • 10个字符:正则表达式 => 4毫秒,mb_check_encoding()函数 => 64毫秒
  • 10000个字符:正则表达式 => 125毫秒,mb_check_encoding()函数 => 2.4秒

你的系统必须运行得非常快,因为在一个相当现代的系统上进行7500次迭代时,我需要大约5秒钟的时间(尽管我正在处理一些相当大的字符串,比如一个相当现代网站的HTML代码)。 - Brian Leishman
什么是“正则表达式解决方案”? - AndreKR
Bobince的解决方案 - Maxime Pacary

6

请注意: 不必使用通常推荐的(相当复杂的)W3C正则表达式,只需使用'u'修饰符即可测试字符串是否为UTF-8格式:

<?php
  if (preg_match("//u", $string)) {
      // $string is valid UTF-8
  }

也在早些年,如何检测字符串是否需要应用UTF8解码或编码? - hakre
1
简单的常见情况检查,但不是完全可靠的。它的行为取决于PHP版本,但更重要的是,它允许无效的多字节序列。http://www.phpwact.org/php/i18n/charsets#checking_utf-8_for_well_formedness - Stephen M. Harris

1

回答“iconv是幂等的”:

iconv也不是幂等的。

utf8_encode()iconv()之间的一个重要区别是,即使使用以下代码:

iconv('ISO-8859-1', 'UTF-8'.'//IGNORE', $str)

iconv也可能会出现错误,例如“在输入字符串中检测到不完整的多字节字符”,在上述代码中:

$encoding = mb_detect_encoding($string, "UTF-8,ISO-8859-1,WINDOWS-1252");

你必须了解mb_detect_encoding。它可以识别无效的UTF-8字符串(格式错误的UTF-8)中的uft-8。


0

1
链接似乎已经损坏。 - Peter Mortensen

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