拓冰建站拓冰建站
首页 / 资讯中心 / 正文

RapidJSON 编码体系详解:Unicode/UTF 支持、编码校验与 Transcoder 转码实战

RapidJSON 编码体系详解Unicode/UTF 支持、编码校验与 Transcoder 转码实战【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson本文基于 RapidJSON 官方编码文档doc/encoding.zh-cn.md与仓库源码系统讲解 RapidJSON 如何在不依赖外部库如 ICU的前提下支持 UTF-8/UTF-16/UTF-32/ASCII 多种编码包括编码 struct 的选型与CharType的含义、DOM 模板编码参数、AutoUTF运行时动态编码、kParseValidateEncodingFlag编码校验以及用Transcoder为非 JSON 字符串做转码的完整用法并深入 include/rapidjson/encodings.h 的实现细节。读完后你将能在 C 项目中正确地为Document、Reader、Writer选择编码处理带 BOM 的文件并对 JSON 输入做严格的编码合法性校验。一、背景JSON 标准对编码的要求RapidJSON 的编码设计源自 JSON 标准本身对字符集的规定根据 ECMA-404 标准IntroductionJSON 文本是 Unicode 码点code point的序列较早的 RFC 4627§3声明JSON 文本应以 Unicode 编码缺省编码为 UTF-8RFC 4627§6进一步说明JSON 可使用UTF-8、UTF-16 或 UTF-32表示。以 UTF-8 写入时JSON 是 8 位兼容的以 UTF-16 或 UTF-32 写入时就必须使用二进制的 content-transfer-encoding。RapidJSON 对这一要求的落地方式是内置多种编码的实现既能校验 JSON 是否为标明编码的合法序列也能在不同编码之间自动转码全部在库内部完成无需引入 ICU 等外部程序库。核心实现集中在两个头文件include/rapidjson/encodings.h各编码 struct、AutoUTF、Transcoderinclude/rapidjson/encodedstream.h编码感知的流包装器EncodedInputStream/EncodedOutputStream/AutoUTFInputStream/AutoUTFOutputStream。二、Unicode 与 UTF 转换格式2.1 码点code pointUnicode 为每个字符提供一个唯一的数字不论平台、程序或语言。这些唯一数字称为码点范围介乎0x0至0x10FFFF之间。存储码点有多种编码方式统称为 Unicode 转换格式UTF。RapidJSON 支持其中最常用的三种格式编码方式一个码点占用UTF-88 位可变长度14 个字节UTF-1616 位可变长度12 个 16 位编码单元24 字节UTF-3232 位固定长度1 个 32 位编码单元4 字节2.2 字节序endianness对于 UTF-16 及 UTF-32字节序是有影响的在内存中码点通常以该计算机CPU的字节序存储使用内存编码UTF16/UTF32时无需关心字节序——源码注释明确说明For in-memory access, no need to concern endianness. The code units and code points are represented by CPUs endianness.见 encodings.h在文件中存储或网上传输时必须指明字节序列是小端little endian, LE还是大端big-endian, BE此时应选用UTF16LE/UTF16BE/UTF32LE/UTF32BE。2.3 RapidJSON 的编码 struct 一览RapidJSON 通过rapidjson/encodings.h中的 struct 提供各种编码见 encodings.hnamespace rapidjson { templatetypename CharType char struct UTF8; // 内存字节编码默认 char templatetypename CharType wchar_t struct UTF16; // 内存 16 位编码CPU 字节序 templatetypename CharType wchar_t struct UTF16LE; // 16 位小端用于 I/O templatetypename CharType wchar_t struct UTF16BE; // 16 位大端用于 I/O templatetypename CharType unsigned struct UTF32; // 内存 32 位编码CPU 字节序 templatetypename CharType unsigned struct UTF32LE; // 32 位小端用于 I/O templatetypename CharType unsigned struct UTF32BE; // 32 位大端用于 I/O } // namespace rapidjson使用原则与原文档一致内存中的文本使用UTF8、UTF16或UTF32经过 I/O 的文本文件、网络可使用UTF8、UTF16LE、UTF16BE、UTF32LE或UTF32BE。从源码结构看UTF16LE/UTF16BE继承自UTF16UTF32LE/UTF32BE继承自UTF32如 UTF16LE 定义它们额外实现了Take/Put/TakeBOM/PutBOM四个面向字节流的函数负责在 1 字节的字节流与多字节编码单元之间做显式的字节序拆装父类只负责Encode/Decode/Validate。三、DOM 中如何指定编码使用 DOM 风格 API 时GenericValueEncoding及GenericDocumentEncoding里的Encoding模板参数指明内存中存储的 JSON 字符串使用哪种编码因此通常使用UTF8、UTF16或UTF32。选择取决于应用所在的操作系统与其他程序库Windows API 使用 UTF-16 表示 Unicode 字符而多数 Linux 发行版及应用软件更偏好 UTF-8。使用 UTF-16 的 DOM 声明例子typedef GenericDocumentUTF16 WDocument; typedef GenericValueUTF16 WValue;更完整的 DOM 编码用法可参考 doc/stream.zh-cn.md 中 DOMs Encoding 一节以及 doc/dom.zh-cn.md 里基于WDocument/WValue的示例如用 UTF-16 的WValue构造非 ASCII 键名。四、CharType存储的是编码单元不是字符上面的声明中每个编码都有一个CharType模板参数。这一点容易混淆CharType存储的是一个编码单元code unit而不是一个字符码点。例如在 UTF-8 中一个码点可能编码成 14 个编码单元。源码对CharType的大小做了静态断言约束UTF16要求sizeof(Ch) 2encodings.hUTF32要求sizeof(Ch) 4encodings.h因此UTF16(LE|BE)的CharType必须是至少 2 字节的整数类型UTF32(LE|BE)必须是至少 4 字节的整数类型。注意 C11 新增了char16_t及char32_t类型也可以分别用于UTF16及UTF32。4.1 Encoding 概念每个编码 struct 提供的接口从 encodings.h 的文档注释可以看到每个编码都实现同一组静态函数concept函数作用Encode(os, codepoint)把一个码点0x00x10FFFF编码写入输出流Decode(is, codepoint)从输入流解码一个码点失败返回 falseValidate(is, os)只校验并复制一个码点不做完整解码TakeBOM(is)/Take(is)从字节流取一个编码单元跳过 BOMPutBOM(os)/Put(os, c)向字节流写 BOM / 写一个编码单元4.2 实现细节速览源码级UTF-8 编码UTF8::Encode按码点范围分四个分支把高位码点拆入每字节 6 个有效位encodings.h例如 4 字节分支0xF0 | (cp 18)加三个0x80 | ...尾字节。解码/校验则基于 Bjoern Hoehrmann 的 DFA 状态机GetRange用一张 256 字节的查表encodings.h把每个字节映射为类型码Decode/Validate用宏展开的状态转移完成 14 字节的连续取码与合法性判断对过长的编码、孤立的代理值等都会返回失败。UTF-16 编码UTF16::Encode对0xFFFF以下的码点直接输出 1 个编码单元更大的码点则转换为0xD800/0xDC00开头的代理对surrogate pairencodings.hDecode反向合并代理对且会校验高代理后面必须跟低代理返回c 0xDC00 c 0xDFFF作为合法性判断encodings.h。ASCIIASCIIstruct 的supportUnicode为 0encodings.hDecode中c 0x7F之外一律判定非法PutBOM为空操作——这与下文 ASCII 不能用于内存 的限制相对应。五、AutoUTF运行时动态选择编码UTF8、UTF16等编码都是编译期静态绑定的——使用者必须事先知道内存或流里是什么编码。但当需要读写编码只能在运行时才能确定的文件比如按 BOM 判断时就需要AutoUTF。AutoUTF是为此设计的编码它根据输入/输出流在运行时选择使用哪种编码应与EncodedInputStream及EncodedOutputStream家族在 encodedstream.h 中结合使用。5.1 工作机制函数指针分派从源码结构看AutoUTF内部维护一个运行时UTFType// include/rapidjson/encodings.h (L603-L609) enum UTFType { kUTF8 0, // UTF-8 kUTF16LE 1, // UTF-16 little endian kUTF16BE 2, // UTF-16 big endian kUTF32LE 3, // UTF-32 little endian kUTF32BE 4 // UTF-32 big endian };AutoUTF::Encode/Decode/Validateencodings.h通过静态函数指针数组f[]按流的GetType()动态分派到对应的UTF8/UTF16LE/… 实现。这也意味着流必须提供GetType()——只有AutoUTFInputStream/AutoUTFOutputStream提供该接口普通EncodedInputStream没有所以AutoUTF必须与这两个动态流搭配使用。5.2 编码探测BOM RFC 4627 空字节模式AutoUTFInputStream的构造函数会先执行编码探测encodedstream.h探测规则分两级BOM 探测BOM 字节编码00 00 FE FFUTF-32BEFF FE 00 00UTF-32LEFE FFUTF-16BEFF FEUTF-16LEEF BB BFUTF-8无 BOM 时按 RFC 4627 §3 的空字节模式探测JSON 文本的前两个字符必为 ASCII因此可通过前 4 个字节的空字节模式判断编码——00 00 00 xx为 UTF-32BE、00 xx 00 xx为 UTF-16BE、xx 00 00 00为 UTF-32LE、xx 00 xx 00为 UTF-16LE全非零则为 UTF-8均不匹配时使用构造参数给定的缺省类型默认kUTF8。探测完成后流的GetType()返回识别出的UTFTypeHasBOM()返回是否带 BOM。对应的AutoUTFOutputStream则按指定类型写入 BOMencodedstream.h。5.3 完整实战任意编码的 JSON 美化输出仓库示例 example/prettyauto/prettyauto.cpp 展示了完整链路输入支持 UTF-8/UTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE先转成 UTF-8 内存 DOMSAX 流式解析再按输入编码原样写出。关键代码#include rapidjson/reader.h #include rapidjson/prettywriter.h #include rapidjson/filereadstream.h #include rapidjson/filewritestream.h #include rapidjson/encodedstream.h // NEW #ifdef _WIN32 // 防止 Windows 把 CRLF 与 LF 互转 _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif using namespace rapidjson; int main(int, char*[]) { // Reader源编码为 AutoUTFunsignedDOM 侧按 UTF-8 处理 GenericReaderAutoUTFunsigned, UTF8 reader; char readBuffer[65536]; FileReadStream is(stdin, readBuffer, sizeof(readBuffer)); AutoUTFInputStreamunsigned, FileReadStream eis(is); // 自动探测编码 char writeBuffer[65536]; FileWriteStream os(stdout, writeBuffer, sizeof(writeBuffer)); // 输出与输入保持同一编码并沿用输入的 BOM 设置 typedef AutoUTFOutputStreamunsigned, FileWriteStream OutputStream; OutputStream eos(os, eis.GetType(), eis.HasBOM()); PrettyWriterOutputStream, UTF8, AutoUTFunsigned writer(eos); // 解析时开启编码校验 if (!reader.ParsekParseValidateEncodingFlag(eis, writer)) { fprintf(stderr, \nError(%u): %s\n, static_castunsigned(reader.GetErrorOffset()), GetParseError_En(reader.GetParseErrorCode())); return 1; } return 0; }几点解析GenericReaderAutoUTFunsigned, UTF8 的第一个模板参数是源流编码第二个是目标编码二者不同Reader会解码并转成 UTF-8 语义因此解析即完成了任意编码 → UTF-8的转码Writer的三个模板参数为OutputStream, SourceEncoding, TargetEncoding此处目标是AutoUTFunsigned由AutoUTFOutputStream的GetType()决定实际写出的编码如果不想沿用输入编码也可以静态绑定输出编码例如固定输出 UTF-16LE 带 BOMtypedef EncodedOutputStreamUTF16LE, FileWriteStream OutputStream; OutputStream eos(os, true); // 第二参数 putBOM PrettyWriterOutputStream, UTF8, UTF16LE writer(eos);EncodedInputStream/EncodedOutputStream本身则是静态绑定版本构造时编码即确定输入构造时自动经Encoding::TakeBOM跳过 BOMencodedstream.h输出可经putBOM参数决定是否写 BOMencodedstream.h。六、ASCII面向不支持 UTF-8 的场景JSON 标准并未提及 ASCII但有时我们需要写出 7 位 ASCII 的 JSON供无法处理 UTF-8 的应用程序使用。由于任一 JSON 都可以把 Unicode 字符表示为\uXXXX转义序列JSON 总可以用 ASCII 编码。把 UTF-8 的 DOM 写成 ASCII JSON 的例子using namespace rapidjson; Document d; // UTF8 // ... StringBuffer buffer; WriterStringBuffer, Document::EncodingType, ASCII writer(buffer); d.Accept(writer); std::cout buffer.GetString();ASCII 的两条重要限制可用于输入流当输入流包含大于 127 的字节时会触发kParseErrorStringInvalidEncoding解析错误对应ASCII::Decode中c 0x7F的判定见 encodings.h不能用于内存不能作Document的编码也不能作Reader的目标编码因为它不能表示完整 Unicode 码点supportUnicode 0。七、编码校验kParseValidateEncodingFlag当 RapidJSON 解析一个 JSON 时它能校验输入 JSON判断它是否所标明编码的合法序列。开启方式把kParseValidateEncodingFlag加入Parse的parseFlags模板参数。该标志定义在 include/rapidjson/reader.hkParseValidateEncodingFlag 2, //! Validate encoding of JSON strings.典型用法参考 example/pretty/pretty.cppreader.ParsekParseValidateEncodingFlag(is, handler); // 或 Document::ParsekParseValidateEncodingFlag(...)两个关键规则与原文档一致输入编码与 DOM 编码相同时缺省情况下解析器不会逐序列校验字符串编码需显式加此标志强制校验输入编码与 DOM 编码不同时Reader及Writer会自动转码文本此时不需要该标志——因为解析器本来就必须解码输入序列若序列无法解码则必然是不合法的。校验失败时产生kParseErrorStringInvalidEncoding错误。官方 FAQdoc/faq.zh-cn.md中也给出了相同结论如何校验 JSON 字符串编码是否合法只需把kParseValidateEncodingFlag传给Parse()发现非法编码即产生kParseErrorStringInvalidEncoding。仓库单元测试 test/unittest/documenttest.cpp 中即有用例对 UTF-16 字符串调用json.ParsekParseValidateEncodingFlag(...)验证编码校验路径。八、Transcoder把编码机制借用给非 JSON 字符串RapidJSON 的编码功能虽然是为 JSON 解析/生成设计的但使用者也可以借用它们对任意非 JSON 字符串做转码。核心是 Transcoder 模板// include/rapidjson/encodings.h (L657-L667) templatetypename SourceEncoding, typename TargetEncoding struct Transcoder { templatetypename InputStream, typename OutputStream static RAPIDJSON_FORCEINLINE bool Transcode(InputStream is, OutputStream os) { unsigned codepoint; if (!SourceEncoding::Decode(is, codepoint)) return false; // 源序列非法 TargetEncoding::Encode(os, codepoint); return true; } // ... };即每次从源流解码一个码点 → 用目标编码写回源端解码失败返回 false即表示输入不是合法序列。注意还有一个特化当源、目标编码相同时Transcode退化为直接复制一个编码单元不做解码encodings.h。完整例子——把 UTF-8 字符串转码成 UTF-16stream.h 中StringStream即GenericStringStreamUTF8的别名#include rapidjson/encodings.h using namespace rapidjson; const char* s ...; // UTF-8 字符串 StringStream source(s); GenericStringBufferUTF16 target; bool hasError false; while (source.Peek() ! \0) if (!TranscoderUTF8, UTF16 ::Transcode(source, target)) { hasError true; break; } if (!hasError) { const wchar_t* t target.GetString(); // ... 使用 t }同样地可以把TranscoderAutoUTFunsigned, ...与AutoUTFInputStream/AutoUTFOutputStream搭配在运行时指定源/目的编码。九、质量保障全码点范围的单元测试编码实现的可信度可以从测试看出test/unittest/encodingstest.cpp 内嵌了 Unicode 全部基本多语言平面及补充平面的码点区间表kCodepointRanges覆盖0x00000x10FFFF的各个 block对UTF8/UTF16/UTF32/ASCII逐一执行三类验证Encode后的字节序列用独立的 Hoehrmann UTF-8 DFA 解码器交叉验证TEST(EncodingsTest, UTF8)Decode还原出的码点必须与原码点一致Validate必须通过且输出字节与输入完全一致。这解释了为什么UTF8::GetRange的 DFA 表encodings.h与 test/unittest/encodingstest.cpp 中的参照表高度同源——实现与验证共用同一状态机设计从源码结构看这是一种以公认正确性基准自证的做法。十、速查小结需求选择依据内存 DOMLinux/通用 CUTF8encodings.h内存 DOMWindows APIUTF16encodings.h读/写明确编码的文件EncodedInputStream/EncodedOutputStreamUTF16LE等encodedstream.h编码只能运行时确定AutoUTFAutoUTFInputStream/AutoUTFOutputStreamBOM/RFC 4627 自动探测encodedstream.h、encodings.h输出 7 位 ASCII JSONWriter..., ASCII 不可用于内存 DOMencodings.h校验字符串编码合法性ParsekParseValidateEncodingFlag跨编码解析无需该标志reader.h非 JSON 字符串转码TranscoderSource, Target::Transcodeencodings.h以上所有能力——多编码、BOM 处理、编码校验、自动转码——均由 RapidJSON 在内部实现不依赖 ICU 等外部库配合 example/prettyauto/prettyauto.cpp 与 test/unittest/encodingstest.cpp可直接复用其模式处理实际项目中的多编码 JSON 输入输出。【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门