RapidJSON 代码剖析(三):Unicode 的编码与解码
根据 RFC-7159:
8.1 Character Encoding
JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The default encoding is UTF-8, and JSON texts that are encoded in UTF-8 are interoperable in the sense that they will be read successfully by the maximum number of implementations; there are many implementations that cannot successfully read texts in other encodings (such as UTF-16 and UTF-32).
翻译:JSON文本应该以UTF-8、UTF-16、UTF-32编码。缺省编码为UTF-8,而且有大量的实现能读取以UTF-8编码的JSON文本,说明UTF-8具互操作性;有许多实现不能读取其他编码(如 UTF-16及UTF-32)
RapidJSON 希望尽量支持各种常用 UTF 编码,用四百多行代码实现了 5 种 Unicode 编码器/解码器,另外加上 ASCII 编码。本文会简单介绍它的实现方式。
(配图为老彼得·布吕赫尔笔下的巴别塔)
回顾 Unicode、UTF 与 C++
Unicode 是一个标准,用于处理世界上大部分的文字。在 Unicode 出现之前,每种语言文字会使用不同的编码,例如英文主要用 ASCII、中文主要用 GB 2312 和大五码、日文主要用 JIS 等等。这样会造成很多不便,例如一个文本信息很难混合各种语言的文字。
Unicode 定义了统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF。储存这些码点有不同方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。
UTF-8 成为现时互联网上最流行的格式,有几个原因:
- 它采用字节为编码单元,不会有字节序(endianness)的问题。
- 每个 ASCII 字符只需一个字节去储存。
- 如果程序原来是以字节方式储存字符,理论上不需要特别改动就能处理 UTF-8 的数据。
那么,在处理 JSON 时,若使用 UTF-8,我们为何还需要特别处理?这是因为 JSON 的字符串可以包含 \uXXXX
这种转义字符串。例如["\u20AC"]
这个JSON是一个数组,里面有一个字符串,转义之后是欧元符号"€"
。在 JSON 中,这个转义符使用 UTF-16 编码。JSON 也支持 UTF-16 代理对(surrogate pair),例如高音谱号(U+1D11E)可写成"\uD834\uDD1E
"。所以,即使是 UTF-8 的 JSON,我们都需要在解析JSON字符串时做解码/编码工作。
虽然 Unicode 始于上世纪90年代,C++11 才加入较好的支持。RapidJSON 为了支持 C++ 03,需要自行实现一组编码/解码器。
Encoding
RapidJSON 的编码(encoding)的概念是这样的(非C++代码):
concept Encoding {
typename Ch; //! Type of character. A "character" is actually a code unit in unicode's definition.
enum { supportUnicode = 1 }; // or 0 if not supporting unicode
//! \brief Encode a Unicode codepoint to an output stream.
//! \param os Output stream.
//! \param codepoint An unicode codepoint, ranging from 0x0 to 0x10FFFF inclusively.
template<typename OutputStream>
static void Encode(OutputStream& os, unsigned codepoint);
//! \brief Decode a Unicode codepoint from an input stream.
//! \param is Input stream.
//! \param codepoint Output of the unicode codepoint.
//! \return true if a valid codepoint can be decoded from the stream.
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint);
//! \brief Validate one Unicode codepoint from an encoded stream.
//! \param is Input stream to obtain codepoint.
//! \param os Output for copying one codepoint.
//! \return true if it is valid.
//! \note This function just validating and copying the codepoint without actually decode it.
template <typename InputStream, typename OutputStream>
static bool Validate(InputStream& is, OutputStream& os);
// The following functions are deal with byte streams.
//! Take a character from input byte stream, skip BOM if exist.
template <typename InputByteStream>
static CharType TakeBOM(InputByteStream& is);
//! Take a character from input byte stream.
template <typename InputByteStream>
static Ch Take(InputByteStream& is);
//! Put BOM to output byte stream.
template <typename OutputByteStream>
static void PutBOM(OutputByteStream& os);
//! Put a character to output byte stream.
template <typename OutputByteStream>
static void Put(OutputByteStream& os, Ch c);
};
由于 C++ 可使用不同类型作为字符类型,如 char
、wchar_t
、char16_t
(C++11)、char32_t
(C++11)等,实现这个 Encoding
概念的类需要设定一个 Ch
类型。
这当中最种要的函数是 Encode()
和 Decode()
,它们分别把码点编码至输出流,以及从输入流解码成码点。Validate()
则是只验证编码是否正确,并复制至目标流,不做解码工作。例如 UTF-16 的编码/解码实现是:
template<typename CharType = wchar_t>
struct UTF16 {
typedef CharType Ch;
RAPIDJSON_STATIC_ASSERT(sizeof(Ch) >= 2);
enum { supportUnicode = 1 };
template<typename OutputStream>
static void Encode(OutputStream& os, unsigned codepoint) {
RAPIDJSON_STATIC_ASSERT(sizeof(typename OutputStream::Ch) >= 2);
if (codepoint <= 0xFFFF) {
RAPIDJSON_ASSERT(codepoint < 0xD800 || codepoint > 0xDFFF); // Code point itself cannot be surrogate pair
os.Put(static_cast<typename OutputStream::Ch>(codepoint));
}
else {
RAPIDJSON_ASSERT(codepoint <= 0x10FFFF);
unsigned v = codepoint - 0x10000;
os.Put(static_cast<typename OutputStream::Ch>((v >> 10) | 0xD800));
os.Put((v & 0x3FF) | 0xDC00);
}
}
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint) {
RAPIDJSON_STATIC_ASSERT(sizeof(typename InputStream::Ch) >= 2);
Ch c = is.Take();
if (c < 0xD800 || c > 0xDFFF) {
*codepoint = c;
return true;
}
else if (c <= 0xDBFF) {
*codepoint = (c & 0x3FF) << 10;
c = is.Take();
*codepoint |= (c & 0x3FF);
*codepoint += 0x10000;
return c >= 0xDC00 && c <= 0xDFFF;
}
return false;
}
// ...
};
转码
RapidJSON 的解析器可以读入某种编码的JSON,并转码为另一种编码。例如我们可以解析一个 UTF-8 JSON文件至 UTF-16 的 DOM。我们可以实现一个类做这样的转码工作:
template<typename SourceEncoding, typename TargetEncoding>
struct Transcoder {
//! Take one Unicode codepoint from source encoding, convert it to target encoding and put it to the output stream.
template<typename InputStream, typename OutputStream>
RAPIDJSON_FORCEINLINE static bool Transcode(InputStream& is, OutputStream& os) {
unsigned codepoint;
if (!SourceEncoding::Decode(is, &codepoint))
return false;
TargetEncoding::Encode(os, codepoint);
return true;
}
// ...
};
这段代码非常简单,就是从输入流解码出一个码点,解码成功就编码并写入输出流。但如果来源的编码和目标的编码都一样,我们不是做了无用功么?但 C++ 的[模板偏特化(partial template specialization)可以这么做:
//! Specialization of Transcoder with same source and target encoding.
template<typename Encoding>
struct Transcoder<Encoding, Encoding> {
template<typename InputStream, typename OutputStream>
RAPIDJSON_FORCEINLINE static bool Transcode(InputStream& is, OutputStream& os) {
os.Put(is.Take()); // Just copy one code unit. This semantic is different from primary template class.
return true;
}
// ...
};
那么,不用转码的时候,就只需复制编码一个单元。零开销!所以,在解析及生成 JSON 时都使用到 Transcoder
去做编码转换。
UTF-8 解码与 DFA
在 UTF-8 中,一个码点可能会编码为1至4个编码单元(字节)。它的解码比较复杂。RapidJSON 参考了 Hoehrmann 的实现,使用确定有限状态自动机(deterministic finite automation, DFA)的方式去解码。UTF-8的解码过程可以表示为以下的DFA:
当中,每个转移(transition)代表在输入流中遇到的编码单元(字节)范围。这幅图忽略了不合法的范围,它们都会转移至一个错误的状态。
原来我希望在本文中详细解析 RapidJSON 实现中的「优化」。但几年前在 Windows 上的测试结果和近日在 Mac 上的测试结果大相迳庭。还是等待之后再分析后再讲。
AutoUTF
有时候,我们不能在编译期决定 JSON 采用了哪种编码。而上述的实现都是在编译期以模板类型做挷定的。所以,后来 RapidJSON 加入了一个运行时做动态挷定的编码类型,称为 AutoUTF
。它之所以称为自动,是因为它还有检测字节顺序标记(byte-order mark, BOM)的功能。如果输入流有 BOM,就能自动选择适当的解码器。不过,因为在运行时挷定,就需要多一层间接。RapidJSON采用了函数指针的数组来做这间接层。
ASCII
有一个用家提出希望写入 JSON 时,能把所有非 ASCII 的字符都写成 \uXXXX
转义形式。解决方法就是加入了 ASCII
这个模板类:
template<typename CharType = char>
struct ASCII {
typedef CharType Ch;
enum { supportUnicode = 0 };
// ...
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint) {
unsigned char c = static_cast<unsigned char>(is.Take());
*codepoint = c;
return c <= 0X7F;
}
// ...
};
通过检测 supportUnicode
,写入 JSON 时就可以决定是否做转义。另外,Decode()
时也会检查是否超出 ASCII 范围。
总结
RapidJSON 提供内置的 Unicode 支持,包括各种 UTF 格式及转码。这是其他 JSON 库较少做的部分。另外,RapidJSON 是在输入输出流的层面去处理,避免了把整个JSON读入、转码,然后才开始解析。RapidJSON 这么实现节省内存,而且性能应该更优。
最近为了开发 RapidJSON 下一个版本新增的 JSON Schema 功能,实现了一个正则表达式引擎。该引擎也利用了 Encoding
这套框架,轻松地实现了 Unicode 支持,例如可以直接匹配 UTF-8 的输入流。
RapidJSON 代码剖析(三):Unicode 的编码与解码的更多相关文章
- RapidJSON 代码剖析(四):优化 Grisu
我曾经在知乎的一个答案里谈及到 V8 引擎里实现了 Grisu 算法,我先引用该文的内容简单介绍 Grisu.然后,再谈及 RapidJSON 对它做了的几个底层优化. (配图中的<Grisù& ...
- RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描
现在的 CPU 都提供了单指令流多数据流(single instruction multiple data, SIMD)指令集.最常见的是用于大量的浮点数计算,但其实也可以用在文字处理方面. 其中,S ...
- RapidJSON 代码剖析(一):混合任意类型的堆栈
大家好,这个专栏会分析 RapidJSON (中文使用手册)中一些有趣的 C++ 代码,希望对读者有所裨益. C++ 语法解说 我们先来看一行代码(document.h): bool StartArr ...
- unicode的编码与解码
- HDFS集中式的缓存管理原理与代码剖析--转载
原文地址:http://yanbohappy.sinaapp.com/?p=468 Hadoop 2.3.0已经发布了,其中最大的亮点就是集中式的缓存管理(HDFS centralized cache ...
- HDFS集中式的缓存管理原理与代码剖析
转载自:http://www.infoq.com/cn/articles/hdfs-centralized-cache/ HDFS集中式的缓存管理原理与代码剖析 Hadoop 2.3.0已经发布了,其 ...
- Python中GBK, UTF-8和Unicode的编码问题
编码问题,一直是使用python2时的一块心病.几乎所有的控制台输入输出.IO操作和HTTP操作都会涉及如下的编码问题: UnicodeDecodeError:‘ascii’codec can’t d ...
- x264代码剖析(三):主函数main()、解析函数parse()与编码函数encode()
x264代码剖析(三):主函数main().解析函数parse()与编码函数encode() x264的入口函数为main().main()函数首先调用parse()解析输入的參数,然后调用encod ...
- 三种字符编码:ASCII、Unicode和UTF-8
原文:三种字符编码:ASCII.Unicode和UTF-8 什么是字符编码? 计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理.最早的计算机在设计时采用8个比特(bit)作为一个字 ...
随机推荐
- AndroidTV版(乐视超3 X55)root,将自己的软件设置为开机自启、系统软件,卸载系统应用等问题总结
最近开发android软件客户要安装在乐视TV上,而且要求是开机自启.我很天真的以为写一个广播接收类接收开机广播就可以了,可是根本不会,有的设备就是不可以接收到开机广播,于是各种百度搜索.大神们说是只 ...
- Android开发—已root的手机获取data路径
开发android的时候,尽管手机已经root但是DDMS中还是没有data/data路径怎么办? 可以用cmd命令提示符为逐个文件夹设置权限: 打开cmd,输入 adb shell 回车—> ...
- Android 窗体设置
requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG ...
- Github+hexo绑定域名
Github绑定域名 近期在新网购买了一个属于自己的域名,因此想着把自己用hexo+github搭建的博客通过域名访问,但是找了n长时间来搞,都没有成功.心灰意冷之中再次通过google来搜索,终于有 ...
- iOS 用 SDWebImage 清理图片缓存
效果图如下: 1.找到 SDWebImage找到SDImageCache类 2.添加如下方法 - (float)checkTmpSize { ; NSDirectoryEnumerator *file ...
- Mybatis基于注解的方式访问数据库
1. 使用方式:在Service层直接调用 package com.disappearwind.service; import org.springframework.beans.factory.an ...
- vim添加代码折叠功能
用空格或者za命名改变,添加如下到vimrc文件 " Enable folding set foldmethod=indent set foldlevel=99 " Enable ...
- jQuery对表单的操作
表单应用 一个表单有3个基本组成部分: 表单标签:包含处理表单数据所用的服务器端程序URL以及数据提交到服务器的方法 表单域:包含文本框.密码框.隐藏域.多行文本框.复选框.单选框.下拉选择框和文件上 ...
- easyui-datagrid连接数据库实现分页查询数据
一.利用MVC思想建立底层数据库: package com.hanqi.dao; import java.util.ArrayList; import java.util.List; import o ...
- DDD实施经验分享—价值导向、从上往下进行(圈内第一个吃螃蟹DDD实施方案)
阅读目录: 1.背景 2.从业务开始 3.从战略到战术 4.借助外力推动研发(QA.领导.自动化测试) 5.领域模型与SAAS平台的内核(价值最大化) 6.最后 1.背景 DDD本身的技术就不介绍了, ...