UTF-8贯穿始终

1351

我正在搭建一台新的服务器,希望我的Web应用程序完全支持UTF-8编码。我曾在现有的服务器上尝试过这种设置,但似乎总是不得不退回到ISO-8859-1编码。

我需要在哪些地方设置编码/字符集呢?我知道需要配置Apache、MySQL和PHP来实现这个功能——是否有一份标准的清单可以供我遵循,或者可以通过排除错误的方式找到问题出现的地方呢?

这是一个运行MySQL 5、PHP 5和Apache 2的新Linux服务器。


22
这是有关编码的一般介绍和特别介绍PHP中的编码:每个程序员都必须了解有关编码和字符集以处理文本的内容 - deceze
一些关于PHP 7的最近讨论表明,自2010年以来,“正式被放弃”的立场没有变化... 关于“PHP7和UTF-8”还有什么其他内容吗? - Peter Krauss
这个问题很常见。但是没有捷径的解决方案,您将需要为它们中的每一个单独设置 utf-8 - MySQL 5、PHP 5 或 Apache 2。 - Manish Shrivastava
数据库服务器的时区选择也是一个重要的设置。我建议使用UTC(对于MySQL,set time_zone='+0:00')作为服务器默认值。 - dolmen
13个回答

1142

数据存储:

  • 在数据库中的所有表和文本列上指定utf8mb4字符集。这使得MySQL可以原生地存储和检索以UTF-8编码的值。请注意,如果指定了utf8mb4_*排序规则(没有任何显式字符集),MySQL将隐式使用utf8mb4编码。

  • 在旧版本的MySQL(< 5.5.3)中,你只能使用仅支持Unicode字符子集的utf8,这是不幸的事实。我希望我是在开玩笑。

数据访问:

  • In your application code (e.g. PHP), in whatever DB access method you use, you'll need to set the connection charset to utf8mb4. This way, MySQL does no conversion from its native UTF-8 when it hands data off to your application and vice versa.

  • Some drivers provide their own mechanism for configuring the connection character set, which both updates its own internal state and informs MySQL of the encoding to be used on the connection—this is usually the preferred approach. In PHP:

    • If you're using the PDO abstraction layer with PHP ≥ 5.3.6, you can specify charset in the DSN:

       $dbh = new PDO('mysql:charset=utf8mb4');
      
    • If you're using mysqli, you can call set_charset():

        $mysqli->set_charset('utf8mb4');       // object oriented style
        mysqli_set_charset($link, 'utf8mb4');  // procedural style
      
    • If you're stuck with plain mysql but happen to be running PHP ≥ 5.2.3, you can call mysql_set_charset.

  • If the driver does not provide its own mechanism for setting the connection character set, you may have to issue a query to tell MySQL how your application expects data on the connection to be encoded: SET NAMES 'utf8mb4'.

  • The same consideration regarding utf8mb4/utf8 applies as above.

输出:

  • 应在HTTP头中设置UTF-8,例如Content-Type: text/html; charset=utf-8。您可以通过在php.ini中设置default_charset(首选)或手动使用header()函数来实现。
  • 如果您的应用程序向其他系统传输文本,则它们也需要被告知字符编码。对于Web应用程序,浏览器必须被告知以哪种编码发送数据(通过HTTP响应标头或HTML元数据)。
  • 使用json_encode()进行输出编码时,将JSON_UNESCAPED_UNICODE作为第二个参数添加。

输入:

  • 浏览器将以文档指定的字符集提交数据,因此无需特别处理输入。
  • 如果您对请求编码有疑问(例如可能遭到篡改),则可以在尝试存储或在任何地方使用它之前验证每个接收到的字符串是否为有效的UTF-8。PHP的mb_check_encoding()可以完成这项工作,但您必须坚持使用它。恶意客户端可以以任何编码方式提交数据,因此确实没有绕过此问题的方法,我还没有找到一个可靠的方法让PHP为您完成这项工作。

其他代码注意事项:

  • 显然,您将提供的所有文件(PHP、HTML、JavaScript等)都应该使用有效的UTF-8编码。

  • 确保每次处理UTF-8字符串时,您都可以安全地进行。不幸的是,这是困难的部分。您可能需要广泛使用PHP的mbstring扩展。

  • 默认情况下,PHP内置的字符串操作不是UTF-8安全的。您可以使用普通的PHP字符串操作(如连接)来安全执行某些操作,但对于大多数操作,您应该使用等效的mbstring函数。

  • 为了知道自己在做什么(即:不搞砸),您真的需要了解UTF-8以及它在最低可能级别上的工作原理。请查看utf8.com中的任何链接,获取一些学习所需的好资源。


53
我没有错:COLLATE 暗示了 CHARACTER SET。例如,参见 http://dev.mysql.com/doc/refman/5.0/en/charset-database.html。 - chazomaticus
130
注意,MySQL不使用与其他人相同的语言。当MySQL说“utf8”时,它实际上指的是“奇怪而低智的UTF-8变体,由于某种荒谬的原因仅限于三个字节”。如果您真的想要UTF-8,应该告诉MySQL您想要这个MySQL喜欢称为[utf8mb4]的奇怪东西(http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)。别在意“WTF!”的表达方式。 - R. Martinho Fernandes
@chazomaticus,你认为我应该即使是英文也使用mbstring吗?还是strlen就足够了?那希腊语呢? - Dimitris Papageorgiou
@chazomaticus 我的数据库已经存储了问号而不是希伯来文本,那么我该如何从这些问号中获取我的原始文本? - Bhargav Rangani
1
救了我的命,每次尝试在前端页面输出表情符号时,我总是错过了数据库连接部分。 - Ge Rong
@BhargavRangani 如果你的数据库里真的有问号,那就太晚了。在数据写入数据库之前,你可能已经遇到某种编码错误,导致原始文本永远丢失了。 - Mark Ransom

169

我想在chazomaticus的优秀回答中补充一点内容:

不要忘记META标签(像这样,或者它的HTML4或XHTML版本):

<meta charset="utf-8">

看起来很琐碎,但IE7以前曾给我带来过问题。

我做了所有正确的事情:数据库、数据库连接和Content-Type HTTP标头都设置为UTF-8,在所有其他浏览器中都正常工作,但Internet Explorer仍然坚持使用“西欧”编码。

原来页面缺少META标签。添加后问题得到解决。

编辑:

实际上,W3C有一个相当大的I18N部分。他们有许多与此问题相关的文章 - 描述了HTTP、(X)HTML和CSS方面的内容:

他们建议同时使用HTTP标头和HTML元标签(在作为XML提供的XHTML的情况下使用XML声明)。


74

除了在php.ini中设置default_charset外,您还可以在代码中使用header()在任何输出之前发送正确的字符集:

header('Content-Type: text/html; charset=utf-8');

在PHP中处理Unicode很容易,只要你意识到大多数字符串函数不支持Unicode,并且有些可能会完全破坏字符串。PHP认为“字符”长度为1个字节。有时这没问题(例如,explode() 只查找一个字节序列并将其用作分隔符--所以您查找的实际字符是无关紧要的)。但其他情况下,当函数实际上被设计用于处理字符时,PHP不知道您的文本具有使用Unicode找到的多字节字符。一个好的库是 phputf8。它重新编写了所有“坏”的函数,因此您可以安全地处理UTF8字符串。也有像mb_string扩展这样的扩展程序尝试为您完成此操作,但我更喜欢使用库,因为它更具可移植性(但我编写大众市场产品,所以这对我很重要)。但是,phputf8仍然可以在后台使用mb_string来提高性能。

43

警告:本回答仅适用于PHP 5.3.5及以下版本。请勿在PHP版本5.3.6(于2011年3月发布)或更高版本中使用。

Palec的PDO + MySQL和破损的UTF-8编码的答案进行比较。


我发现有人在使用PDO时出现了问题,解决方法是使用以下PDO连接字符串:

$pdo = new PDO(
    'mysql:host=mysql.example.com;dbname=example_db',
    "username",
    "password",
    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));

1
进一步查看,这仅适用于PHP版本低于5.3.6。另请参见:https://dev59.com/Wm855IYBdhLWcg3waDeR#4361485(尽管他们使用单独的 $dbh->exec("set names utf8");;我更喜欢这里介绍的方法)。顺便说一下,在PHP手册中也有类似的注释:http://php.net/manual/en/pdo.construct.php#96325。 - Marten Koetsier

30
在我的情况下,我正在使用 mb_split 函数,它使用正则表达式。因此,我还需要手动确保正则表达式的编码是UTF-8,通过执行 mb_regex_encoding('UTF-8') 来实现。
另外一点,我还发现通过运行 mb_internal_encoding() ,内部编码不是UTF-8,所以我通过运行 mb_internal_encoding("UTF-8") 进行更改。

27

首先,如果你使用的是 PHP 版本 5.3 之前的话,那就不行。你需要处理大量的问题。

我惊讶地发现没有人提到 intl 库,这个库对于 Unicode图形字符字符串操作本地化 等拥有良好的支持,详情请参见下文。

我将引用 Elizabeth Smith 在 PHPBenelux'14 上的演讲幻灯片中关于 PHP 的 Unicode 支持的一些信息。

INTL

优点:

  • 围绕 ICU 库封装
  • 标准化区域设置,为脚本设置区域设置
  • 数字格式化
  • 货币格式化
  • 消息格式化 (替代 gettext)
  • 日历、日期、时区和时间
  • Transliterator
  • Spoofchecker
  • 资源包
  • 转换器
  • IDN 支持
  • 图形字符
  • 排序
  • 迭代器

缺点:

  • 不支持 zend_multibyte
  • 不支持 HTTP 输入输出转换
  • 不支持函数重载

mb_string

  • 启用 zend_multibyte 支持
  • 支持透明的 HTTP 输入输出编码
  • 提供一些功能的封装,如 strtoupper

ICONV

  • 主要用于字符集转换
  • 输出缓冲区处理程序
  • mime 编码功能
  • 转换
  • 一些字符串辅助函数 (len、substr、strpos、strrpos)
  • 流过滤器 stream_filter_append($fp, 'convert.iconv.ISO-2022-JP/EUC-JP')
  • 数据库

    • MySQL: 在表和连接上设置字符集和校对规则(不是连接的校对规则)。另外,不要使用mysql - mysqli或PDO。
    • postgresql: 使用pg_set_client_encoding。
    • sqlite(3): 确保使用了支持Unicode和intl的编译版本。

    一些其他注意点

    • 在Windows系统中,如果不使用第三方扩展程序,无法使用PHP和Unicode文件名。
    • 如果使用exec、proc_open和其他命令行调用,请以ASCII格式发送所有内容。
    • 纯文本并非真正的纯文本,文件具有编码方式。
    • 您可以使用iconv过滤器即时转换文件。

    2
    是的,没错。Mysqli 和 PDO 可以使用它们的本地驱动程序。此外,如果您使用 --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd 选项编译 php,则可以使用 mysqlnd 驱动程序。 - Alexander Yancharuk

    20

    除了这些精彩的回答,我想强调一下保存文件时需要采用UTF-8编码,因为我发现浏览器接受该属性优于设置UTF-8作为代码编码。任何一个体面的文本编辑器都会显示这个。例如,Notepad++ 有一个文件编码的菜单选项,它会显示当前编码并允许您更改它。对于我所有的PHP文件,我都使用不带BOM的UTF-8。

    以前,有人请我为某人设计的PHP和MySQL应用程序添加UTF-8支持。我发现所有文件都是用ANSI编码的,所以我必须使用iconv转换所有文件,将数据库表更改为使用UTF-8字符集和utf8_general_ci校对,连接后在数据库抽象层中添加'SET NAMES utf8'(如果使用5.3.6或更早版本。否则,您必须在连接字符串中使用charset=utf8),并更改字符串函数以使用PHP多字节字符串功能的等效方式。


    18

    我最近发现使用strtolower()可能会导致数据在特殊字符后被截断。

    解决方案是使用

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

    mb_ 使用 MultiByte。它支持更多的字符,但一般来说速度稍慢。


    13
    在PHP中,你需要使用多字节函数,或者打开mbstring.func_overload。这样,如果你有超过一个字节的字符,像strlen这样的函数就能正常工作。
    你还需要确定你的响应的字符集。你可以使用上面的AddDefaultCharset,或者编写返回头部的PHP代码。(或者你可以在HTML文档中添加META标签。)

    5
    请注意--一些代码可能实际上依赖于标准字符串函数每个字符只占一个字节的特性。 - JW.
    需要注意的是,由于@JW在上面的评论中指出的问题,mbstring.func_overload功能将在PHP 7.2中被弃用。因此,最好的建议是:是的,您应该绝对使用mbstring函数,但不要使用过载功能来使标准函数作为多字节工作。 - Simba

    12

    我刚刚遇到了同样的问题,并在PHP手册中找到了一个好的解决方案。

    我将所有文件的编码都改为UTF8,然后将连接的默认编码也改为UTF8。这解决了所有问题。

    if (!$mysqli->set_charset("utf8")) {
        printf("Error loading character set utf8: %s\n", $mysqli->error);
    } else {
       printf("Current character set: %s\n", $mysqli->character_set_name());
    }
    

    查看源代码


    2
    我花了一个小时来解决我正在处理的页面上的编码问题,而我通常很擅长解决这些问题。我总是查阅这个页面,你的答案帮了我很多,我点了赞。在我的情况下,set_charset('utf8mb4') 没有起作用,但是 set_charset("utf8") 起了作用,而其他答案中实际上没有显示这一点。 - Funk Forty Niner
    @FunkFortyNiner 注意:set_charset("utf8") 可能会起作用,但行为会有所不同(请参阅有关 utf8utf8mb4 之间差异以及 mysql 版本历史的说明)。仅在您知道自己在做什么时使用 utf8 - Martin Hennings
    使用utf8mb4字符集。 - dolmen

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