Unicode 很好。如果您正在设计包含文本字段的数据结构或协议,它们应该包含以 UTF-8 编码的 Unicode 字符。不过,还有一个问题:“哪些Unicode 字符?” 答案是“不是全部,请排除一些。”
这个问题一直存在,所以我和 Paul Hoffman 一起向 IETF 提交了一份独立草案,现在(我说的“现在”指的是“两年后”),它已经以RFC 9839 的形式发布。它解释了哪些字符是不合适的,以及为什么,然后提供了三个可能不太合适的子集,你可以参考一下。这里先介绍一下背景知识,但是……
请
如果您正在开发包含文本字段的新项目,请阅读 RFC。它只有十页,而且包含了所有 IETF 样板文件。它是专门为软件和网络人员编写的。
确凿证据
9839 关注的问题是“有问题的字符”,所以让我们先举一个痛苦的例子来解释一下它的含义。假设你正在设计一个使用 JSON 的协议,并且你的一个构造函数包含一个username
段。假设你收到这条消息(我省略了所有非username
名字段)。这是一个完全合法的 JSON 文本:
{ “用户名” : “\u0000\u0089\uDEAD\uD9BF\uDFFF” }
解压所有 JSON 转义乱码后发现, username
段的值包含四个标识 Unicode 字符的数字“代码点”:
-
第一个代码点是零,用 Unicode 术语来说
U+0000
。在人类可读的文本中,它没有任何意义,但它会干扰某些编程语言的运行。 -
接下来是 Unicode
U+0089
,正式名称为“带对齐的字符制表符”。它是 Unicode 所称的C1 控制码,继承自 ISO/IEC 6429:1992,采纳自ECMA 48 (1991),后者称之为“HTJ”,其含义为: HTJ 使活动字段(呈现组件中包含活动呈现位置的字段)的内容前移,使其结束于下一个字符制表符停止位之前的字符位置。活动呈现位置移动到下一个字符制表符停止位。移位字符串开头之前的字符位置将被置于擦除状态。祝你好运。
-
第三个码位
U+DEAD
,用 Unicode 术语来说,是“非成对代理”。要理解它,你必须了解 Unicode 中那个被人诟病的UTF-16编码是如何运作的。我建议你还是别费心了。你需要知道的是,代理只有在 UTF-16 编码文本中成对出现时才有意义。实际上,网络上不存在这样的文本,因此没有理由容忍数据中存在代理。事实上,UTF-8 规范规定,你不能使用 UTF-8 编码代理。但真正的问题是,不同编程语言的不同库在遇到这种恶臭的“闯入者”时,处理方式并不总是相同的。
-
最后,
\uD9BF\uDFFF
是代码点U+7FFFF
的 JSON 格式。Unicode 中有一个类别叫做“非字符”,其中包含几十个代码点,由于各种原因,有些是好的,有些则不代表任何内容,因此不能在网络上互换。UU+7FFFF
就是其中之一。
示例中的四个代码点显然都是有问题的。刚刚发布的 RFC 9839 正式定义了“有问题”的概念,并提供了易于引用的语言,说明了哪些有问题的类型需要从文本字段中排除。如果你打算使用 JSON,那么你应该这样做。
不要责怪道格
我指的是 Doug Crockford,JSON 的发明者。如果他(或者我,或者任何认真的人)在 Unicode 已经成熟的现在发明 JSON,他会对它的字符库更加挑剔。话虽如此,我们永远只能使用 JSON 的原样,所以我们需要一个好的方法来指定哪些有问题的字符需要排除,即使 JSON 允许这些字符。
精确
你可能会好奇,为什么 IETF 要等到 2025 年才提供针对“糟糕的 Unicode”问题的帮助。它并没有这样做;这里有RFC 8264 : PRECIS 框架:应用协议中国际化字符串的准备、执行和比较;第一个 PRECIS 前身发布于 2002 年。8264 长达 43 页,对比 9839 更多的潜在“糟糕的 Unicode”问题进行了非常详尽的讨论。
与 9839 一样,PRECIS 指定了 Unicode 字符集的子集,并更进一步,提供了定义更多内容的机制。
话虽如此,PRECIS 似乎并没有被那些定义新数据结构和协议的人广泛使用。我个人认为,有两个问题导致它难以采用。首先,它庞大而复杂,包含许多可变部分,需要仔细研究才能理解。开发人员(有充分的理由)很懒惰。
其次,使用 PRECIS 会将你绑定到特定版本的 Unicode。具体来说,它禁止使用(近百万个)未分配的代码点。由于 Unicode 的每个版本都包含新的代码点分配,这意味着如果发送方和接收方想要实现可靠的互操作行为,他们需要就他们将要使用的 Unicode 版本达成一致。这使得编写用于各种不同应用程序的通用代码变得困难。
我个人认为,人们想要使用的 Unicode 的唯一版本是“尽可能新”,这样他们就可以确信拥有所有最新的表情符号。
无论如何,9839 比 PRECIS 更简单、更笨拙。但我认为有些人会觉得它有用,而且现在 IETF 也同意这一点。
源代码
我编写了一个 Go 语言的小库,用于根据 9839 指定的三个子集分别验证传入的文本字段,具体见这里。我不敢说它是最优的,但它确实经过了充分的测试。
目前它还没有版本号或发布版本,我会等到一些人有机会发现我可能犯的任何愚蠢错误。
细节
以下是存在问题的 Unicode 代码点、数据格式和标准的简要概述。
问题类别被排除? | |||
---|---|---|---|
代理人 | 旧版控件 | 非角色 | |
CBOR | 是的 | 不 | 不 |
I-JSON | 是的 | 不 | 是的 |
JSON | 不 | 不 | 不 |
Protobufs | 不 | 不 | 不 |
托米 | 是的 | 不 | 不 |
XML | 是的 | 部分 [1] | 部分 [2] |
YAML | 是的 | 大多 [3] | 部分 [2] |
RFC 9839 子集 | |||
标量 | 是的 | 不 | 不 |
XML | 是的 | 部分的 | 部分的 |
可分配项 | 是的 | 是的 | 是的 |
笔记:
[1] XML 允许 C1 控件。
[2] XML 和 YAML 不排除基本多语言窗格之外的非字符。
[3] YAML 排除了所有遗留控件,除了几乎无害的U+0085
(IBM 大型机文档中使用的\n
的另一个版本)。
谢谢!
9839 并非我的个人作品。它得到了众多睿智且见多识广人士的深入讨论和改进,经过 15 次修改后,最终发布的版本比我的初稿有了显著的提升。我衷心感谢我的联合编辑 Paul Hoffman 以及 RFC 致谢部分中提到的所有人。
关于个人提交
9339 是我向 IETF 提交的第二个“单独提交”的 RFC(另一个是RFC 7725 ,它注册了 HTTP 451 状态码)。虽然决定某个东西值得标准化并最终实现它是一件好事,但这确实需要大量工作。有些工作甚至很烦人。
我曾以工作组成员、工作组主席和工作组规范编辑的身份参与过其他工作,我可以权威地报告,通过工作组以传统方式创建 RFC 更容易、更好。
我不太愿意劝别人不要追随我的脚步,但在这种情况下,我认为这是正确的建议。
原文: https://www.tbray.org/ongoing/When/202x/2025/08/14/RFC9839