每个软件开发人员绝对必须了解 Unicode 和字符集的绝对最低限度(没有任何借口!

有没有想过那个神秘的 Content-Type 标签?你知道,你应该放在HTML中的那个,你永远不知道它应该是什么?

你有没有收到过朋友发来的邮件,主题是“???? ?????? ??? ????”?

我沮丧地发现,有多少软件开发人员并没有真正完全了解字符集、编码、Unicode 等神秘世界。几年前,FogBUGZ的Beta测试人员想知道它是否可以处理日语传入的电子邮件。日语?他们有日语电子邮件吗?我不知道。当我仔细查看我们用于分析 MIME 电子邮件的商业 ActiveX 控件时,我们发现它对字符集做了完全错误的事情,因此我们实际上必须编写英雄代码来撤消它所做的错误转换并正确地重做它。当我查看另一个商业库时,它也有一个完全损坏的字符代码实现。我与该软件包的开发人员通信,他有点认为他们“对此无能为力”。像许多程序员一样,他只是希望一切都能以某种方式结束。

但事实并非如此。当我发现流行的 Web 开发工具 PHP 几乎完全无视字符编码问题时,我轻率地使用 8 位字符,几乎不可能开发出好的国际 Web 应用程序,我想,够了

所以我要宣布:如果你是 2003 年的一名程序员,你不知道字符、字符集、编码和 Unicode 的基础知识,而我抓住了你,我会惩罚你,让你在潜艇里剥洋葱 6 个月。我发誓我会的。

还有一件事:

这并不难。

在这篇文章中,我将向你介绍每个在职程序员都应该知道的内容。所有关于“纯文本 = ascii = 字符是 8 位”的东西不仅是错误的,而且是无可救药的错误,如果你仍然以这种方式编程,你并不比不相信细菌的医生好多少。在阅读完本文之前,请不要再编写一行代码。

在我开始之前,我应该警告你,如果你是那些了解国际化的少数人之一,你会发现我的整个讨论有点过于简单化了。我真的只是想在这里设定一个最低标准,以便每个人都能理解正在发生的事情,并可以编写代码,希望能够处理除英语子集以外的任何语言的文本,这些语言不包括带重音的单词。我应该警告你,字符处理只是创建国际通用软件所需的一小部分,但我一次只能写一件事,所以今天是字符集。

历史视角

理解这些东西的最简单方法是按时间顺序排列。

你可能会认为我要在这里谈论像 EBCDIC 这样的非常古老的字符集。好吧,我不会。EBCDIC与您的生活无关。我们不必回到那么久以前。

ASCII 表

回到半古代,当Unix被发明出来,K&R正在编写C编程语言时,一切都非常简单。EBCDIC正在退出。唯一重要的字符是旧的无重音英文字母,我们有一个称为 ASCII 的代码,它能够使用 32 到 127 之间的数字来表示每个字符。空格是 32,字母“A”是 65,等等。这可以方便地以 7 位存储。当时大多数计算机都使用 8 位字节,所以你不仅可以存储所有可能的 ASCII 字符,而且你有一整块空余,如果你是邪恶的,你可以将其用于你自己的狡猾目的:WordStar 的昏暗灯泡实际上打开了高位来指示单词中的最后一个字母, 谴责 WordStar 仅提供英文文本。低于 32 的代码称为不可打印,用于 cusing。开玩笑。它们用于控制字符,例如 7 会使您的计算机发出哔哔声,而 12 会导致当前页面的纸张从打印机中飞出并送入新页面。

一切都很好,假设你是一个说英语的人。

因为字节最多有 128 位的空间,所以很多人开始想,“天哪,我们可以将代码 255-128 用于我们自己的目的。麻烦的是,很多人同时有这个想法,他们对从255到8088的空间应该去哪里有自己的想法。IBM-PC 有一个后来被称为 OEM 字符集的东西,它为欧洲语言提供了一些重音字符和一堆线条绘制字符......单杠、竖杠、右边挂着小叮叮当当的小杠铃等,你可以用这些画线字符在屏幕上制作漂亮的方框和线条,你仍然可以在干洗店的 128 电脑上看到它们运行。事实上,一旦人们开始在美国以外的地方购买 PC,各种不同的 OEM 角色集就被想象出来,它们都使用前 130 个角色来达到自己的目的。例如,在某些 PC 上,字符代码 128 将显示为 é,但在以色列销售的计算机上,它是希伯来字母 Gimel (ג),因此当美国人将他们的简历发送到以色列时,他们会以 rגsumגs 的形式到达。在许多情况下,例如俄语,对于如何处理上面的 <> 个字符有很多不同的想法,因此您甚至无法可靠地交换俄语文档。

最终,这种 OEM 免费产品被编入 ANSI 标准。在 ANSI 标准中,每个人都同意在 128 以下做什么,这与 ASCII 几乎相同,但有很多不同的方法可以处理 128 及以上的字符,具体取决于您居住的地方。这些不同的系统称为代码页。例如,在以色列,DOS 使用一个名为 862 的代码页,而希腊用户使用 737。它们在 128 以下相同,但与 128 以上不同,所有有趣的字母都驻留在那里。MS-DOS的国家版本有几十个这样的代码页,可以处理从英语到冰岛语的所有内容,他们甚至有一些“多语言”代码页,可以在同一台计算机上使用世界语和加利西亚语!哇!但是,除非您编写自己的自定义程序来使用位图图形显示所有内容,否则在同一台计算机上获取希伯来语和希腊语是完全不可能的,因为希伯来语和希腊语需要不同的代码页,对高数字有不同的解释。

与此同时,在亚洲,更疯狂的事情正在发生,因为亚洲字母表有数千个字母,而这些字母永远无法容纳 8 位。这通常是通过称为DBCS的混乱系统来解决的,DBCS是“双字节字符集”,其中一些字母存储在一个字节中,而其他字母则存储在两个字节中。在一根绳子中向前移动很容易,但向后移动几乎是不可能的。鼓励程序员不要使用 s++ 和 s– 来前后移动,而是调用 Windows 的 AnsiNext 和 AnsiPrev 等函数,它们知道如何处理整个混乱。

但是,大多数人只是假装一个字节是一个字符,一个字符是 8 位,只要你从不将字符串从一台计算机移动到另一台计算机,或者说一种以上的语言,它就会一直有效。但是,当然,一旦互联网出现,将字符串从一台计算机移动到另一台计算机就变得司空见惯,整个混乱局面就崩溃了。幸运的是,Unicode已经被发明出来了。

统一码

Unicode是一个勇敢的努力,它创造了一个单一的字符集,包括地球上所有合理的书写系统,以及一些虚构的书写系统,如克林贡语。有些人误以为 Unicode 只是一个 16 位代码,其中每个字符需要 16 位,因此有 65,536 个可能的字符。实际上,这是不正确的。这是关于Unicode最常见的神话,所以如果你这么想,不要感到难过。

事实上,Unicode对字符的思考方式是不同的,你必须理解Unicode对事物的思考方式,否则什么都不会有意义。

到目前为止,我们假设一个字母映射到一些可以存储在磁盘或内存中的位:

A -> 0100 0001

在Unicode中,一个字母映射到一个叫做码位的东西,这仍然只是一个理论概念。该代码点如何在内存或磁盘上表示是一个完整的故事。

在Unicode中,字母A是柏拉图式的理想。它只是漂浮在天堂:

一个

这个柏拉图式的 A 与 B 不同,与 a 不同,但与 A 和 A 和 A 相同。Times New Roman 字体中的 A 与 Helvetica 字体中的 A 是相同的字符,但与小写的“a”不同,这种想法似乎没有太大争议,但在某些语言中,仅仅弄清楚字母是什么就会引起争议。德语字母 ß 是真正的字母还是只是一种花哨的 ss 书写方式?如果一个字母的形状在单词末尾发生了变化,那是一个不同的字母吗?希伯来语说“是”,阿拉伯语说“不是”。无论如何,Unicode联盟的聪明人在过去十年左右的时间里一直在弄清楚这一点,伴随着大量高度政治化的辩论,你不必担心。他们已经想通了。

每个字母表中的每个柏拉图字母都由Unicode联盟分配一个幻数,其写法如下:U+0639。这个幻数称为码位。U+ 表示“Unicode”,数字是十六进制的。U+0639 是阿拉伯字母 Ain。英文字母 A 是 U+0041。您可以使用 Windows 2000/XP 上的 charmap 实用程序或访问 Unicode 网站找到它们。

Unicode 可以定义的字母数量没有真正的限制,事实上它们已经超过了 65,536 个,因此并非每个 Unicode 字母都可以真正压缩成两个字节,但这无论如何都是一个神话。

好的,假设我们有一个字符串:

你好

在 Unicode 中,它对应于以下五个代码点:

U+0048 U+0065 U+006C U+006C U+006F。

只是一堆代码点。数字,真的。我们还没有说任何关于如何将其存储在内存中或在电子邮件中表示它的任何内容。

编码

这就是编码的用武之地。

Unicode编码的最早想法,导致了关于两个字节的神话,是,嘿,让我们把这些数字分别存储在两个字节中。所以你好变成了

00 48 00 65 00 6C 00 6C 00 6F

右?没那么快!难道不能也是:

48 00 65 00 6C 00 6C 00 6F 00 ?

嗯,从技术上讲,是的,我确实相信它可以,事实上,早期的实现者希望能够以高端或低端模式存储他们的 Unicode 码位,无论他们的特定 CPU 在哪个模式下最快,瞧,现在是晚上和早上,已经有两种方法来存储 Unicode。因此,人们被迫想出一个奇怪的约定,即在每个Unicode字符串的开头存储一个FE FF;这称为 Unicode 字节顺序标记,如果您要交换高字节和低字节,它看起来像 FF FE,读取字符串的人会知道他们必须每隔一个字节交换一次。唷。并非每个 Unicode 字符串的开头都有一个字节顺序标记。

有一段时间,这似乎已经足够好了,但程序员们却在抱怨。“看看那些零!”他们说,因为他们是美国人,他们看到的是英文文本,很少使用U+00FF以上的代码点。他们也是加利福尼亚的自由派嬉皮士,他们想要保护(冷笑)。如果他们是德克萨斯人,他们不会介意消耗两倍的字节数。但是那些加利福尼亚的懦夫无法忍受将字符串所需的存储量增加一倍的想法,无论如何,已经有所有这些使用各种 ANSI 和 DBCS 字符集的狗狗文档,谁来转换它们?莫伊?仅出于这个原因,大多数人决定忽略Unicode好几年,与此同时,情况变得更糟。

因此发明了 UTF-8 的绝妙概念。UTF-8 是另一个系统,用于使用 8 位字节将 Unicode 码位字符串(即那些神奇的 U+ 数字)存储在内存中。在 UTF-8 中,从 0 到 127 的每个码位都存储在一个字节中。只有 128 及以上的代码点使用 2、3 存储,实际上最多存储 6 个字节。

UTF-8 的工作原理

这有一个巧妙的副作用,即英语文本在 UTF-8 中看起来与在 ASCII 中完全相同,因此美国人甚至不会注意到任何错误。只有世界其他地方必须跳过铁环。具体来说,U+0048 U+0065 U+006C U+006C U+006F 的 Hello 将存储为 48 65 6C 6C 6F,看哪!与存储在 ASCII 和 ANSI 以及地球上的每个 OEM 字符集中的相同。现在,如果你胆大妄为地使用重音字母、希腊字母或克林贡字母,你将不得不使用几个字节来存储一个码位,但美国人永远不会注意到。(UTF-8 还具有一个很好的属性,即想要使用单个 0 字节作为 null 终止符的无知旧字符串处理代码不会截断字符串)。

到目前为止,我已经告诉了您三种编码Unicode的方法。传统的 save-it-in-two-byte 方法称为 UCS-2(因为它有两个字节)或 UTF-16(因为它有 16 位),您仍然需要弄清楚它是高端 UCS-2 还是低端 UCS-2。还有流行的新 UTF-8 标准,它有一个很好的特性,如果你有幸巧合地使用英语文本和脑死亡程序,而这些程序完全不知道除了 ASCII 之外还有任何东西,它也可以很好地工作。

实际上还有很多其他的Unicode编码方法。有一种叫做 UTF-7 的东西,它很像 UTF-8,但保证高位永远为零,所以如果你必须通过某种严厉的警察国家电子邮件系统传递 Unicode,认为 7 位就足够了,谢谢你,它仍然可以毫发无损地挤过去。有 UCS-4,它以 4 个字节的形式存储每个码位,它有一个很好的特性,即每个码位都可以存储在相同数量的字节中,但是,天哪,即使是德克萨斯人也不会如此大胆地浪费那么多内存。

事实上,现在你正在考虑由Unicode码位表示的柏拉图式理想字母,这些Unicode码位也可以用任何老式的编码方案进行编码!例如,您可以用 ASCII 编码 Hello 的 Unicode 字符串 (U+0048 U+0065 U+006C U+006C U+006F),或者旧的 OEM 希腊语编码,或希伯来语 ANSI 编码,或者迄今为止发明的数百种编码中的任何一种,但有一个问题:某些字母可能不会显示!如果尝试在尝试表示它的编码中没有等效的 Unicode 码位,通常会得到一个小问号:?或者,如果你真的很好,一个盒子。你得到了哪个?-> �

有数百种传统编码只能正确存储一些码位,并将所有其他码位更改为问号。一些流行的英语文本编码是 Windows-1252(西欧语言的 Windows 9x 标准)和 ISO-8859-1,又名拉丁语 1(也适用于任何西欧语言)。但是尝试将俄语或希伯来语字母存储在这些编码中,你会得到一堆问号。UTF 7、8、16 和 32 都具有能够正确存储任何代码点的良好属性。

关于编码的最重要的一个事实

如果你完全忘记了我刚才解释的一切,请记住一个极其重要的事实。在不知道字符串使用什么编码的情况下拥有字符串是没有意义的。您不能再把头埋在沙子里,假装“纯”文本是 ASCII。

没有纯文本这样的东西。

如果在内存中、文件或电子邮件中有一个字符串,则必须知道它采用的编码,否则无法正确解释它或向用户显示它。

几乎每一个愚蠢的“我的网站看起来像胡言乱语”或“当我使用口音时她无法阅读我的电子邮件”的问题都归结为一个天真的程序员,他不明白一个简单的事实,即如果你不告诉我特定字符串是使用 UTF-8 还是 ASCII 或 ISO 8859-1(拉丁语 1)或 Windows 1252(西欧)编码的, 您根本无法正确显示它,甚至无法弄清楚它的结束位置。有超过一百种编码,在代码点 127 以上,所有赌注都已关闭。

我们如何保留有关字符串使用的编码的信息?嗯,有标准的方法可以做到这一点。对于电子邮件,表单的标题中应包含一个字符串

内容类型:text/plain;字符集=“UTF-8”

对于网页,最初的想法是 Web 服务器将返回一个类似的 Content-Type http 标头以及网页本身——不是在 HTML 本身中,而是作为在 HTML 页面之前发送的响应标头之一。

这会导致问题。假设您有一个大型 Web 服务器,其中包含许多站点和数百个页面,这些页面由许多人以多种不同的语言贡献,并且所有页面都使用他们的 Microsoft FrontPage 副本认为适合生成的任何编码。Web 服务器本身并不知道每个文件是用什么编码编写的,因此它无法发送 Content-Type 标头。

如果您可以使用某种特殊标签将 HTML 文件的 Content-Type 直接放在 HTML 文件本身中,那就太方便了。当然,这让纯粹主义者发疯了......你怎么能HTML文件,直到你知道它是什么编码?!幸运的是,几乎所有常用的编码都对 32 到 127 之间的字符执行相同的操作,因此您始终可以在 HTML 页面上走到这一步,而无需开始使用有趣的字母:

<html>
<head>
<meta http-equiv=“内容类型” content=“text/html;字符集=UTF-8“>

但是这个元标记实际上必须是<head>部分的第一件事,因为一旦 Web 浏览器看到这个标签,它就会停止解析页面,并在使用您指定的编码重新解释整个页面后重新开始。

如果 Web 浏览器在 http 标头或 meta 标记中找不到任何 Content-Type,它们会怎么做?Internet Explorer 实际上做了一些非常有趣的事情:它试图根据各种语言的典型编码中各种字节在典型文本中出现的频率来猜测使用了哪种语言和编码。因为各种旧的 8 位代码页倾向于将其国家字母放在 128 到 255 之间的不同范围内,并且因为每种人类语言都有不同的字母使用特征直方图,这实际上有机会奏效。这确实很奇怪,但它似乎确实经常起作用,以至于天真的网页作者从来不知道他们需要 Content-Type 标头,在 Web 浏览器中查看他们的页面,它看起来还不错,直到有一天,他们写的东西不完全符合他们母语的字母频率分布,而 Internet Explorer 确定它是韩语并这样显示它, 我认为,证明波斯特尔定律关于“在你发出的东西上保守,在你接受的东西上自由”的观点,坦率地说,这不是一个好的工程原则。无论如何,这个网站是用保加利亚语写的,但似乎是韩国人(甚至没有凝聚力的韩国人),可怜的读者在做什么?他使用 View |编码菜单并尝试一堆不同的编码(东欧语言至少有十几种),直到图片更清晰。如果他知道这样做,大多数人都不知道。

对于我公司发布的最新版本的 CityDesk(网站管理软件),我们决定在内部使用 UCS-2(双字节)Unicode 进行所有操作,这是 Visual Basic、COM 和 Windows NT/2000/XP 用作其本机字符串类型。C++代码中,我们只是将字符串声明为 wchar_t(“宽字符”)而不是 char,并使用 wcs 函数而不是 str 函数(例如,wcscat 和 wcslen 而不是 strcatstrlen)。 要在 C 代码中创建文字 UCS-2 字符串,只需在它前面加上一个 L,如下所示:L“Hello”。

当 CityDesk 发布网页时,它会将其转换为 UTF-8 编码,该编码多年来一直受到 Web 浏览器的良好支持。这就是 Joel on Software 的所有 29 种语言版本的编码方式,我还没有听说过一个人在查看它们时遇到任何问题。

这篇文章越来越长,我不可能涵盖所有关于字符编码和 Unicode 的知识,但我希望如果你已经读到这里,你有足够的知识回到编程,使用抗生素而不是水蛭和咒语,我现在将交给你。