Unicode
在美国东北部长大,我从未需要使用英语以外的语言。我用英语阅读,用英语交谈,用英语写作,并用英语开展业务。1968 年创建 ASCII 的工程师们也是如此,他们确保 128 个 ASCII 字符足以满足英语文档的需求。只要您坚持使用标准的 ASCII 字符集,您就可以保证将文件从一台计算机移动到另一台计算机,而无需担心文件损坏。
ASCII 在当时很好,但说法语、西班牙语和其他西欧语言的人很快发现它不足以满足他们的需求。毕竟,在计算机上用这些语言写作的人希望使用正确的重音符号。因此,随着时间的推移,7 位 ASCII 码变成了 8 位扩展 ASCII 码,其中包括许多显示西欧文本所必需的特殊字母和符号。
但是,由于扩展 ASCII 从未被声明为标准,因此出现了许多不同的、不兼容的基 ASCII 码扩展,并变得广泛使用。Windows 有自己的扩展,Macintosh 和 NeXTSTEP 操作系统也是如此。因此,尽管您可以使用 Windows 用法语编写文档,但在将其移动到 Macintosh 时需要对其进行翻译。否则,字节将在接收机器上被错误地解释,将您原本出色的法语剧本变成更像法式吐司的东西。
国际标准最终占了上风,至少在某种程度上,一种正式名称为 ISO-8859-1,非正式名称为 Latin-1 的标准。计算机制造商随后可以交换西欧文档,而无需担心内容会变得混乱。当然,这意味着我们正在使用每个字符字节的所有八位,将可用字符的数量从 128 个增加到 256 个。
但是,这并没有解决所有问题。例如,希伯来语使用者有自己的标准 ISO-8859-8,它对于字符 0-127 与 Latin-1 相同,而对于 128-256 则完全不同。用希伯来语编写但在使用 Latin-1 的计算机上显示的文档看起来像是一个字母替换谜题,使用了错误字母表中的字母。
实际上,这意味着您无法使用 ISO-8859 系列标准编写包含英语、希伯来语和法语的文档。事实上,考虑到我们在单个 8 位字节中只有 256 个字符可以使用,这很有道理。但这为我们这些使用两种以上语言的人提出了一些严肃的问题和挑战。
如果您想显示包含英语、法语、希伯来语和中文的页面,情况会变得尤其复杂。毕竟,中文有数万个表意文字,更不用说日语和其他语言了。
Unicode 诞生了,它是下一个世纪的 ASCII 表。与 ASCII 一样,Unicode 为每个字母、数字和符号分配一个数字。与 ASCII 不同,Unicode 包含足够的空间来容纳人类创造的每一个书写符号。这意味着 Unicode 文档可以包含来自任意数量语言的任意数量的字符,而无需担心它们之间发生冲突。Unicode 还处理了 ASCII 从未梦想过的许多问题,包括组合字符(用于重音符号和其他变音符号)和方向问题(用于不从左向右读取的语言)。
Unicode 已经存在大约十年了,但直到现在才开始流行并被 Web 应用程序支持。本月,我们将探讨 Unicode 对 Web 开发人员的影响。您应该考虑什么?您需要担心什么?以及,您如何绕过与 Unicode 相关的问题?
Unicode,像 ASCII 一样,为每个字母、数字、符号和控制字符分配一个唯一的数字。如上所示,Unicode 扩展到每个曾经创建的符号和字符集。因此,使用 Unicode,您可以创建一个使用英语、俄语、日语和阿拉伯语的文档,其中每个字符都与其他字符明显不同。
我们如何将这些唯一的数字(在 Unicode 世界中称为代码点)转换为比特和字节?ASCII 的编码非常简单;只有 127 个字符(如果包括各种扩展,则为 256 个),每个 ASCII 字符都将适合单个字节。事实上,C 程序员知道 char 数据类型是一个 8 位整数。
最明显的解决方案是为我们的 Unicode 字符分配一个固定的多字节编码。事实上,UCS-2 就是这样一种编码,它使用两个字节来描述所有基本的 65,536 个 Unicode 字符。(有些扩展字符需要额外的字节,但我们不会深入探讨。)UCS-2 为每个字符分配一个 2 字节的代码。因此,无论以何种语言书写,文档的长度都相同,并且程序可以通过将字符数加倍来轻松计算它们需要的字节数。微软的现代操作系统使用 UCS-2,如果您与这些系统的用户交换任何文档,您可能会注意到这一点。
但是 UCS-2 存在一个基本问题,即它与 ASCII 不兼容。如果您有 100,000 个用 ASCII 编写的文档,您将必须将它们翻译成 UCS-2 才能准确读取它们。鉴于大多数现代程序都使用 ASCII,这种缺乏向后兼容性是一个相当大的问题。
UTF-8 应运而生,它是一种可变长度的 Unicode 编码。正如罗马数字和阿拉伯数字以不同的方式表示相同的数字一样,UTF-8 和 UCS-2 只是同一底层 Unicode 字符集的不同编码。但是,虽然每个 UCS-2 字符都需要两个字节,但 UTF-8 字符可能需要一到四个字节。单字节 UTF-8 字符与 ASCII 中的字符相同,这意味着合法的 ASCII 文档也是合法的 UTF-8 文档。但是,Latin-1 和其他 8 位字符集与 UTF-8 不兼容;现有的 Latin-1 文档不仅需要转换,而且大小可能会翻倍。
UTF-8 是 UNIX 和 Linux 系统以及我倾向于使用的大多数标准和开源软件的首选编码。Perl、Python、Tcl 和 Java 都以 UTF-8 编码字符串。PostgreSQL 多年来一直支持 UTF-8,并且 Unicode 支持显然已添加到 MySQL 4.1 中,该版本将在未来几个月内发布 alpha 版本。
为现有系统添加 Unicode 支持是一项艰巨的任务,为此应给予各位开发人员高度赞扬。开发人员不仅需要添加对多字节字符的支持,而且数据库和语言还需要支持正则表达式和排序运算符,这两者都不容易做到。
既然我们已经了解了基本知识,接下来让我们考虑如何通过 Web 传输 Unicode 文档。基本问题是:当您的浏览器收到文档时,它如何知道应该将字节解释为 Latin-1、Big-5 中文还是 UTF-8?
答案在于 Content-type HTTP 标头。每次 HTTP 服务器向浏览器发送文档时,它都会使用 MIME 样式指定来标识它正在发送的内容类型,例如 text/html、image/png 或 application/msword。如果您收到 JPEG 图像 (image/jpeg),则只有一种表示图像的方式。但是,如果您收到 HTML 文档 (text/html),则 Content-type 标头必须指示正在使用的字符集和/或编码。我们通过在标头末尾添加 charset= 指定来做到这一点,将类型与字符集分开。例如
Content-type: text/html; charset=utf-8
纯粹主义者正确地指出,UTF-8 是一种编码而不是字符集。不幸的是,现在做任何事情都为时已晚。这类似于“referrer”一词在 HTTP 规范中被错误拼写为“referer”;每个人都知道这是错误的,但害怕破坏现有软件。
如果未指定 Content-type,则假定为 Latin-1。此外,如果未指定 Content-type,则单个文档可以在元标记内设置(或覆盖)该值。但是,元标记无法覆盖字符集的显式设置。
当您开始使用不同的编码时,您无疑会发现 HTTP 服务器配置不正确,并且在 Content-type 标头中声明了错误的字符集。检查这一点的一种简单方法是使用 Perl 的 LWP(Web 编程库),其中包括许多对 Web 开发人员有用的命令行程序,例如
$ HEAD http://yad2yad.huji.ac.il/
在我的 Linux 机器上键入上述内容会返回来自指定站点的 HTTP 响应标头
200 OK Cache-Control: max-age=0 Connection: close Date: Tue, 10 Dec 2002 08:38:37 GMT Server: AOLserver/3.3.1+ad13 Content-Type: text/html; charset=utf-8如您所见,Content-type 标头声明文档为 UTF-8。
Mozilla 和其他现代浏览器允许用户覆盖显式声明的编码。虽然对于最终用户来说通常没有必要这样做,但在开发站点时,我经常发现此功能很有用。
虽然很高兴知道我们可以通过 HTTP 传输 UTF-8 文档,但我们首先需要一些要发送的 UTF-8 文档。鉴于 ASCII 文档也都是 UTF-8 文档,因此创建有效的 UTF-8 文档很容易,只要它们仅包含 ASCII 字符。但是,如果您想创建包含希伯来语或希腊语的 HTML 页面会发生什么?那么事情开始变得有趣和困难。
基本上有两种方法可以在 HTML 文档中包含 Unicode 字符。第一种是使用可以处理 UTF-8 的编辑器键入字符本身。例如,GNU Emacs 允许我使用各种键盘选项输入文本,然后以我选择的编码(包括 UTF-8)保存我的文档。如果我尝试以 Latin-1 编码保存中文文档,Emacs 将拒绝遵守,并警告我该文档包含 Latin-1 中不存在的字符。不幸的是,对于像我这样想使用希伯来语的人来说,Emacs 尚未处理从右到左的输入。
一个更好的选择,并且一直以来都越来越令人印象深刻的选择是 Yudit,它是一个开源的、符合 UTF-8 标准的编辑器,可以处理许多不同的语言和方向。学习使用 Yudit 可能需要一段时间,但它确实有效。Yudit 与 Emacs 一样,允许您输入任何您想要的字符,即使您的操作系统或键盘不直接支持所有所需的语言。
如果您在 Linux 上工作,如果您愿意进行一些调整,并且如果您不介意手动编写 HTML,那么 Emacs 和 Yudit 都是不错的选择。但是我认识的几乎所有图形设计师都在其他平台上工作,让他们使用使用 UTF-8 的 HTML 编辑器一直相当困难。
幸运的是,Mozilla 不仅附带了 Web 浏览器,还附带了功能齐全的 HTML 编辑器。正如您可能期望的那样,Mozilla 的 Composer 模块有点粗糙,但可以很好地处理大多数任务。
另一种选择是使用 HTML 实体。最著名的实体是 <、> 和 &,它们使得可以将 <、> 和 & 符号插入 HTML 文档中,而无需担心它们会被解释为标签。
现代浏览器不仅理解 ©(版权符号)等实体,还包括完整的 Unicode 字符列表。因此,您可以通过在文档中插入 &#XXXX; 来引用 Unicode 字符,输入字符的十进制代码而不是 XXXX。例如,以下 HTML 文档使用 Unicode 实体以希伯来语显示我的名字
<html> <head><title>Reuven's name</title></head> <body><p>ראובן</p> </body> </html>
创建上述文档不需要符合 Unicode 标准的编辑器,并且它将在任何现代浏览器中正常呈现,无论 HTTP 响应标头中声明了什么 Content-type。但是,以这种方式编辑使用实体的文件既繁琐又困难。不幸的是,国际版 Microsoft Word 中的另存为 HTML 功能广泛使用了此功能,这使得 Word 用户可以轻松创建符合 Unicode 标准的文档,但其他人以后难以编辑它们。
正如我之前指出的,Unicode 是一个复杂的标准,不同的语言和技术需要一些时间才能支持它。例如,Perl 5.6.x 在内部使用了 Unicode,但输入和输出操作无法轻松使用它,这使得这种支持基本上毫无用处。相比之下,Perl 5.8 具有出色的 Unicode 支持,允许开发人员编写依赖于 Unicode 属性的正则表达式。
但是,仍然存在一些问题。开发人员必须处理的一个主要问题是输入编码与存储编码的问题,例如当您的终端可能使用 Latin-1 但后端可能使用 UTF-8 时。这种安排意味着您可以继续使用旧的(非 Unicode)终端程序和字体,但连接并使用符合 Unicode 标准的后端程序。
各种实现也存在一些漏洞,当您刚开始从事项目时,这些漏洞可能并不明显。例如,我最近参与了一个 J2EE 项目,该项目在其后端使用了 PostgreSQL,并将所有字符存储在 Unicode 中。一切都很好,直到我们决定以不区分大小写的方式将用户的输入字符串与数据库中的文本进行比较。不幸的是,我们使用的 PostgreSQL 函数无法正确处理 Unicode 字符串的不区分大小写。我们最终找到了一个解决方法,但是遇到这种情况既令人尴尬又令人沮丧。
排序也是一个难题,它让我多次感到困扰。Unicode 定义了一个字符集,但它没有指示该集合中的字符应以何种顺序排序。例如,直到最近,在西班牙语国家,“ch”才被排序为自己的独立字母;这对于说英语、德语和法语的人来说并非如此。因此,排序顺序不仅取决于字符集,还取决于应用字符集的区域设置。您可能需要尝试 LANG 和 LC_ALL 环境变量(以及其他变量)才能使事情按您预期的方式工作。
Unicode 显然是未来的发展方向;大多数操作系统现在都在一定程度上支持它,并且它正在成为计算机世界中根深蒂固的标准。不幸的是,Unicode 需要摒弃将字符和字节等同起来的旧做法,并处理大量新的复杂性和问题。
如果您的网站只需要使用一种语言,那么您就应该感到幸运。但是,如果您想使用哪怕一个非 ASCII 字符,您很快就会发现自己沉浸在 Unicode 的世界中。考虑到它正在缓慢但肯定地进入几乎每个开源系统和标准,因此尽早了解这项技术是值得的。
Reuven M. Lerner (reuven@lerner.co.il) 是一位专门从事 Web/数据库技术的顾问。他的第一本书《Core Perl》于 2002 年 1 月由 Prentice Hall 出版。他的下一本书,关于开源 Web/开发环境,将于 2003 年末由 Apress 出版。Reuven 与他的妻子和女儿住在以色列的莫迪因。