跳到主要内容

DOM

文档对象模型(DOM)是一种内存中的 JSON 表示,便于查询和操作。我们在 教程 中介绍了 DOM 的基本用法,本节将介绍更多细节和高级用法。

[TOC]

模板

在教程中,我们使用了 ValueDocument 类型。与 std::string 类似,这些类型实际上是两个模板类的 typedef

namespace merak::json {

template <typename Encoding, typename Allocator = MemoryPoolAllocator<> >
class GenericValue {
// ...
};

template <typename Encoding, typename Allocator = MemoryPoolAllocator<> >
class GenericDocument : public GenericValue<Encoding, Allocator> {
// ...
};

typedef GenericValue<UTF8<> > Value;
typedef GenericDocument<UTF8<> > Document;

} // namespace merak::json

用户可以自定义模板参数。

编码

Encoding 参数指定 DOM 在内存中存储 JSON 字符串所使用的编码。有效选项包括 UTF8UTF16UTF32。注意,这三种类型本身也是模板类。UTF8<> 等价于 UTF8<char>,即用 char 存储字符串。更多细节请参考 Encoding

示例:假设 Windows 应用程序需要查询 JSON 中存储的本地化字符串。Windows 的 Unicode 函数使用 UTF-16(宽字符)编码。无论 JSON 文件的编码如何,我们可以在内存中使用 UTF-16 存储这些字符串。

using namespace merak::json;

typedef GenericDocument<UTF16<> > WDocument;
typedef GenericValue<UTF16<> > WValue;

FILE* fp = fopen("localization.json", "rb"); // 非 Windows 平台可用 "r"

char readBuffer[256];
FileReadStream bis(fp, readBuffer, sizeof(readBuffer));

AutoUTFInputStream<unsigned, FileReadStream> eis(bis); // 将 bis 包装成 eis

WDocument d;
d.ParseStream<0, AutoUTF<unsigned> >(eis);

const WValue locale(L"ja"); // 日语

MessageBoxW(hWnd, d[locale].GetString(), L"Test", MB_OK);

分配器

Allocator 定义 Document/Value 分配和释放内存时使用的分配类。Document 拥有或引用一个 Allocator 实例,而 Value 为节省内存不拥有此实例。

GenericDocument 默认使用 MemoryPoolAllocator。该分配器按顺序分配内存,无法释放单独的内存块,非常适合在解析 JSON 生成 DOM 时使用。

Merak 还提供了另一种分配器 CrtAllocator(CRT 表示 C 运行库)。它直接使用标准的 malloc() / realloc() / free(),适用于频繁添加/删除操作,但效率远低于 MemoryPoolAllocator

解析

Document 提供多种解析函数。下面的 (1) 是核心函数,其他函数都是调用它的辅助函数:

using namespace merak::json;

// (1) 核心
template <unsigned parseFlags, typename SourceEncoding, typename InputStream>
GenericDocument& GenericDocument::ParseStream(InputStream& is);

// (2) 使用流的编码
template <unsigned parseFlags, typename InputStream>
GenericDocument& GenericDocument::ParseStream(InputStream& is);

// (3) 使用默认标志
template <typename InputStream>
GenericDocument& GenericDocument::ParseStream(InputStream& is);

// (4) 原地解析
template <unsigned parseFlags>
GenericDocument& GenericDocument::ParseInsitu(Ch* str);

// (5) 原地解析,使用默认标志
GenericDocument& GenericDocument::ParseInsitu(Ch* str);

// (6) 普通字符串解析
template <unsigned parseFlags, typename SourceEncoding>
GenericDocument& GenericDocument::Parse(const Ch* str);

// (7) 使用 Document 编码解析字符串
template <unsigned parseFlags>
GenericDocument& GenericDocument::Parse(const Ch* str);

// (8) 默认标志解析字符串
GenericDocument& GenericDocument::Parse(const Ch* str);

教程中示例使用 (8) 来解析普通字符串,而 Streams 中的示例使用前三个函数。原地解析将在后面介绍。

parseFlags 是以下位标志的组合:

解析标志含义
kParseNoFlags不设置标志。
kParseDefaultFlags默认解析选项,相当于宏 RAPIDJSON_PARSE_DEFAULT_FLAGS(定义为 kParseNoFlags)。
kParseInsituFlag原地(破坏性)解析。
kParseValidateEncodingFlag验证 JSON 字符串编码。
kParseIterativeFlag迭代解析(常量栈空间复杂度)。
kParseStopWhenDoneFlag完成解析根节点后停止处理剩余流,可解析多个 JSON。
kParseFullPrecisionFlag全精度解析数字(较慢),默认使用普通精度(最大误差 3 ULP)。
kParseCommentsFlag允许单行 // ... 和多行 /* ... */ 注释(宽松 JSON)。
kParseNumbersAsStringsFlag将数字类型解析为字符串。
kParseTrailingCommasFlag允许对象和数组末尾的逗号(宽松 JSON)。
kParseNanAndInfFlag允许 NaNInfInfinity-Inf-Infinity(宽松 JSON)。
kParseEscapedApostropheFlag允许字符串中使用转义单引号 \'(宽松 JSON)。

使用非类型模板参数而非函数参数,C++ 编译器可以为每种组合生成专用代码,提高性能并减少代码体积(若只使用单个特化)。缺点是标志必须在编译时确定。

SourceEncoding 参数定义流的编码类型,可与 Document 的编码不同。详细信息请参见 转码与验证

InputStream 是输入流类型。

解析错误

解析成功时,Document 包含解析结果。解析失败时,原 DOM 保持不变。 可使用以下函数获取错误状态:

  • HasParseError()
  • GetParseError()
  • GetErrorOffset()
错误码描述
kParseErrorNone无错误。
kParseErrorDocumentEmpty文档为空。
kParseErrorDocumentRootNotSingular根节点后不允许其他值。
kParseErrorValueInvalid值无效。
kParseErrorObjectMissName对象成员缺少名称。
kParseErrorObjectMissColon对象成员名称后缺少冒号。
kParseErrorObjectMissCommaOrCurlyBracket对象成员后缺少逗号或 }
kParseErrorArrayMissCommaOrSquareBracket数组元素后缺少逗号或 ]
kParseErrorStringUnicodeEscapeInvalidHex字符串中 \u 后的十六进制数字无效。
kParseErrorStringUnicodeSurrogateInvalid字符串中代理对无效。
kParseErrorStringEscapeInvalid字符串中转义字符无效。
kParseErrorStringMissQuotationMark字符串缺少结尾引号。
kParseErrorStringInvalidEncoding字符串编码无效。
kParseErrorNumberTooBig数值过大,无法存储在 double 中。
kParseErrorNumberMissFraction数值缺少小数部分。
kParseErrorNumberMissExponent数值缺少指数部分。

错误偏移量是从流开头到错误位置的字符数。Merak 目前不记录行号。

错误消息默认提供英文(merak/json/error/en.h),用户可修改或使用自定义本地化。

示例:

#include "merak/json/document.h"
#include "merak/json/error/en.h"

Document d;
if (d.Parse(json).HasParseError()) {
fprintf(stderr, "\nError(offset %u): %s\n",
(unsigned)d.GetErrorOffset(),
GetParseError_En(d.GetParseErrorCode()));
}

原地解析

根据 Wikipedia

In situ … 字面意思是“原位”、“在位置上”。在计算机科学中,若算法所需额外内存为 O(1),则称为原地算法。例如,堆排序就是原地排序算法。

在普通解析中,需要将 JSON 字符串解码并拷贝到其他缓冲区,开销较大。原地解析直接在 JSON 原存储位置解码字符串,可行因为解码后的长度 ≤ 原字符串长度。解码包括处理转义字符(如 "\n""\u1234")并在末尾添加空字符 '\0'

普通解析和原地解析对比:

  • 普通解析:将解码后的字符串拷贝到新缓冲区,"\\n" (2 字符) → "\n" (1 字符),"\\u0073" (6 字符) → "s" (1 字符)。
  • 原地解析:直接修改原 JSON 内容,若字符串无转义(如 "msg"),仅将结尾引号替换为 '\0'

原地解析 API 使用 char* 而非 const char*

FILE* fp = fopen("test.json", "r");
fseek(fp, 0, SEEK_END);
size_t filesize = (size_t)ftell(fp);
fseek(fp, 0, SEEK_SET);
char* buffer = (char*)malloc(filesize + 1);
size_t readLength = fread(buffer, 1, filesize, fp);
buffer[readLength] = '\0';
fclose(fp);

Document d;
d.ParseInsitu(buffer);

// 查询和修改 DOM

free(buffer);
// 注意:此时 d 可能包含指向已释放缓冲区的悬空指针

特点与限制:

  1. JSON 必须完整存储在内存。
  2. 源编码必须与 DOM 编码一致。
  3. 缓冲区必须保留到 DOM 不再使用。
  4. 若 DOM 长期使用,且 JSON 字符串少,保留缓冲区可能浪费内存。

原地解析适合短期、一次性 JSON,例如反序列化为 C++ 对象、处理 Web 请求等。

转码与验证

Merak 支持不同 Unicode 格式间的转换(UCS Transformation Formats)。DOM 解析时,源流编码可与 DOM 编码不同,例如源流为 UTF-8,DOM 使用 UTF-16。示例见 EncodedInputStream

输出 JSON 时也可使用转码,示例见 EncodedOutputStream

转码过程:先将源字符串解码为 Unicode 码点,再编码为目标格式,同时验证源字节序列合法性。若非法,返回 kParseErrorStringInvalidEncoding

默认情况下,若源编码与 DOM 编码一致,不进行验证,可通过 kParseValidateEncodingFlag 强制验证。

技巧

使用 DOM 生成 SAX 事件

使用 DOM 生成 JSON:

Writer<StringBuffer> writer(buffer);
d.Accept(writer);

实际上,Value::Accept() 会将 SAX 事件发送给处理器。设计上解耦了 ValueWriter,可自定义处理器,例如将 DOM 转换为 XML。

详情见 SAX

用户提供缓冲区

许多应用希望减少内存分配。MemoryPoolAllocator 支持用户提供缓冲区,可放在栈上或静态数组,用于临时存储。

示例:使用栈内存作为值存储和解析临时缓冲区:

typedef GenericDocument<UTF8<>, MemoryPoolAllocator<>, MemoryPoolAllocator<>> DocumentType;
char valueBuffer[4096];
char parseBuffer[1024];
MemoryPoolAllocator<> valueAllocator(valueBuffer, sizeof(valueBuffer));
MemoryPoolAllocator<> parseAllocator(parseBuffer, sizeof(parseBuffer));
DocumentType d(&valueAllocator, sizeof(parseBuffer), &parseAllocator);
d.Parse(json);

若总分配 ≤ 4096 + 1024 字节,不会触发堆内存分配。

可通过 MemoryPoolAllocator::Size() 查询当前已分配内存,便于确定缓冲区大小。