跳到主要内容

SAX

“SAX” 一词源自 Simple API for XML。我们借用此术语应用于 JSON 的解析与生成。

在 Merak 中,ReaderGenericReader<...> 的 typedef)是 JSON 的 SAX 风格解析器,而 WriterGenericWriter<...> 的 typedef)是 JSON 的 SAX 风格生成器。

Reader

Reader 用于从输入流解析 JSON。在读取流中的字符时,它根据 JSON 语法分析这些字符,并将事件发送给 handler。

例如,考虑如下 JSON:

{
"hello": "world",
"t": true ,
"f": false,
"n": null,
"i": 123,
"pi": 3.1416,
"a": [1, 2, 3, 4]
}

Reader 解析此 JSON 时,会依次发送以下事件到 handler:

StartObject()
Key("hello", 5, true)
String("world", 5, true)
Key("t", 1, true)
Bool(true)
Key("f", 1, true)
Bool(false)
Key("n", 1, true)
Null()
Key("i")
Uint(123)
Key("pi")
Double(3.1416)
Key("a")
StartArray()
Uint(1)
Uint(2)
Uint(3)
Uint(4)
EndArray(4)
EndObject(7)

这些事件可以很容易映射到 JSON 结构,除了一些事件参数需要额外说明。可以参考 simplereader 示例,它的输出与上述完全一致:

#include "merak/json/reader.h"
#include <iostream>

using namespace merak::json;
using namespace std;

struct MyHandler : public BaseReaderHandler<UTF8<>, MyHandler> {
bool Null() { cout << "Null()" << endl; return true; }
bool Bool(bool b) { cout << "Bool(" << boolalpha << b << ")" << endl; return true; }
bool Int(int i) { cout << "Int(" << i << ")" << endl; return true; }
bool Uint(unsigned u) { cout << "Uint(" << u << ")" << endl; return true; }
bool Int64(int64_t i) { cout << "Int64(" << i << ")" << endl; return true; }
bool Uint64(uint64_t u) { cout << "Uint64(" << u << ")" << endl; return true; }
bool Double(double d) { cout << "Double(" << d << ")" << endl; return true; }
bool String(const char* str, SizeType length, bool copy) {
cout << "String(" << str << ", " << length << ", " << boolalpha << copy << ")" << endl;
return true;
}
bool StartObject() { cout << "StartObject()" << endl; return true; }
bool Key(const char* str, SizeType length, bool copy) {
cout << "Key(" << str << ", " << length << ", " << boolalpha << copy << ")" << endl;
return true;
}
bool EndObject(SizeType memberCount) { cout << "EndObject(" << memberCount << ")" << endl; return true; }
bool StartArray() { cout << "StartArray()" << endl; return true; }
bool EndArray(SizeType elementCount) { cout << "EndArray(" << elementCount << ")" << endl; return true; }
};

void main() {
const char json[] = " { \"hello\" : \"world\", \"t\" : true , \"f\" : false, \"n\": null, \"i\":123, \"pi\": 3.1416, \"a\":[1, 2, 3, 4] } ";

MyHandler handler;
Reader reader;
StringStream ss(json);
reader.Parse(ss, handler);
}

注意,Merak 使用模板静态绑定 Reader 类型和 handler 类型,而不是使用虚函数类。此方式可以通过内联函数提高性能。

Handler

如上例所示,用户需要实现 handler 来处理 Reader 的事件(函数调用)。handler 必须包含以下成员函数:

class Handler {
bool Null();
bool Bool(bool b);
bool Int(int i);
bool Uint(unsigned i);
bool Int64(int64_t i);
bool Uint64(uint64_t i);
bool Double(double d);
bool RawNumber(const Ch* str, SizeType length, bool copy);
bool String(const Ch* str, SizeType length, bool copy);
bool StartObject();
bool Key(const Ch* str, SizeType length, bool copy);
bool EndObject(SizeType memberCount);
bool StartArray();
bool EndArray(SizeType elementCount);
};
  • Null():当 Reader 遇到 JSON null 值时调用。

  • Bool(bool):当 Reader 遇到 JSON true 或 false 值时调用。

  • 遇到 JSON 数字时,Reader 会选择合适的 C++ 类型并调用 唯一一个 Int(int)Uint(unsigned)Int64(int64_t)Uint64(uint64_t)Double(double)。如果启用 kParseNumbersAsStrings 选项,则调用 RawNumber()

  • 遇到 JSON 字符串时,调用 String(const char* str, SizeType length, bool copy)

  • 第一个参数指向字符串。

  • 第二个参数为字符串长度(不含结尾 \0)。Merak 支持字符串中包含 null 字符 \0,此时 strlen(str) < length

  • copy 表示 handler 是否需要复制字符串。正常解析时 copy = true;仅在原地解析(in situ)时 copy = false。字符类型与目标编码相关,稍后说明。

  • 遇到 JSON 对象开始时调用 StartObject()。对象是键值对集合(成员)。如果对象包含成员,先调用 Key() 获取成员名,再根据值类型调用对应函数,直到调用 EndObject(SizeType memberCount)memberCount 仅供参考,用户可忽略。

  • JSON 数组类似对象,但更简单。开始时调用 StartArray(),每个元素调用对应函数,结束时调用 EndArray(SizeType elementCount)elementCount 仅供参考。

每个 handler 函数返回 bool,通常返回 true。如果 handler 遇到错误,可返回 false 通知事件发送者停止处理。

例如,当使用 Reader 解析 JSON 时,若 handler 检测到 JSON 不符合要求,可以返回 false,使 Reader 停止解析,并进入错误状态,错误码为 kParseErrorTermination

GenericReader

如前所述,ReaderGenericReader 模板类的 typedef:

namespace merak::json {

template <typename SourceEncoding, typename TargetEncoding, typename Allocator = MemoryPoolAllocator<> >
class GenericReader {
// ...
};

typedef GenericReader<UTF8<>, UTF8<> > Reader;

} // namespace merak::json

Reader 默认使用 UTF-8 作为源和目标编码。源编码指 JSON 流的编码;目标编码用于 String() 参数输出。 例如,要解析 UTF-8 流并输出 UTF-16 字符串事件:

GenericReader<UTF8<>, UTF16<> > reader;

注意 UTF16 默认类型为 wchar_t,此 reader 会调用 handler 的 String(const wchar_t*, SizeType, bool)

第三个模板参数 Allocator 是内部数据结构的分配器类型(实际上是栈)。

Parsing

Reader 唯一功能是解析 JSON:

template <unsigned parseFlags, typename InputStream, typename Handler>
bool Parse(InputStream& is, Handler& handler);

template <typename InputStream, typename Handler>
bool Parse(InputStream& is, Handler& handler); // 使用 parseFlags = kDefaultParseFlags

解析出错时返回 false。用户可调用 HasParseError()GetParseErrorCode()GetErrorOffset() 获取错误状态。实际上,Document 使用这些 Reader 函数获取解析错误。详见 DOM

Writer

Reader 是将 JSON 解析成事件,而 Writer 则正好相反,它将事件生成 JSON。

Writer 使用非常简单。如果应用程序只需要把一些数据转换成 JSON,直接使用 Writer 往往比先构建 Document 再用 Writer 转换更方便。

simplewriter 示例中,我们做了 simplereader 的逆操作:

#include "merak/json/writer.h"
#include "merak/json/stringbuffer.h"
#include <iostream>

using namespace merak::json;
using namespace std;

void main() {
StringBuffer s;
Writer<StringBuffer> writer(s);

writer.StartObject();
writer.Key("hello");
writer.String("world");
writer.Key("t");
writer.Bool(true);
writer.Key("f");
writer.Bool(false);
writer.Key("n");
writer.Null();
writer.Key("i");
writer.Uint(123);
writer.Key("pi");
writer.Double(3.1416);
writer.Key("a");
writer.StartArray();
for (unsigned i = 0; i < 4; i++)
writer.Uint(i);
writer.EndArray();
writer.EndObject();

cout << s.GetString() << endl;
}

输出:

{"hello":"world","t":true,"f":false,"n":null,"i":123,"pi":3.1416,"a":[0,1,2,3]}
  • String()Key() 各有两个重载版本。一个有 3 个参数,遵循 handler 概念,可处理包含 null 字符的字符串;另一个是更简单的版本,如上例所示。
  • 示例中 EndArray()EndObject() 没有参数。虽然可以传 SizeType 参数,但 Writer 会忽略它。

为什么不用 sprintf()std::stringstream

原因有几条:

  1. Writer 保证输出合法 JSON,如果事件顺序错误(如 Int() 紧跟 StartObject()),调试模式下会断言失败。
  2. Writer::String() 可处理字符串转义(如将 U+000A 转换为 \n)并支持 Unicode 转码。
  3. Writer 一致地处理数字输出。
  4. Writer 实现了事件 handler 概念,可处理 ReaderDocument 或其他事件生成器的事件。
  5. Writer 针对不同平台进行了优化。

总之,使用 Writer API 生成 JSON 比 ad-hoc 方法更可靠、更简单。

Template

WriterReader 有一些设计差异。它是模板类,而非 typedef,没有 GenericWriter。声明如下:

namespace merak::json {

template<typename OutputStream, typename SourceEncoding = UTF8<>, typename TargetEncoding = UTF8<>, typename Allocator = CrtAllocator<> >
class Writer {
public:
Writer(OutputStream& os, Allocator* allocator = 0, size_t levelDepth = kDefaultLevelDepth)
// ...
};

} // namespace merak::json
  • OutputStream 模板参数:输出流类型,必须显式指定。
  • SourceEncodingString(const Ch*, …) 输入字符串编码。
  • TargetEncoding:输出流编码。
  • Allocator:内部数据结构(栈)分配器类型。

writeFlags 是以下标志的组合:

写入标志含义
kWriteNoFlags无标志
kWriteDefaultFlags默认选项,等价于宏 RAPIDJSON_WRITE_DEFAULT_FLAGS(定义为 kWriteNoFlags
kWriteValidateEncodingFlag验证 JSON 字符串编码
kWriteNanAndInfFlag允许写入 Infinity-InfinityNaN

构造函数的 levelDepth 参数影响初始内存分配,用于存储层级信息。

PrettyWriter

Writer 输出的 JSON 是最紧凑的格式,不带空格或换行,适合网络传输或存储,但不利于人工阅读。

因此,Merak 提供了 PrettyWriter,可在输出中添加缩进和换行。

使用方法与 Writer 几乎相同,PrettyWriter 提供了 SetIndent(Ch indentChar, unsigned indentCharCount) 函数,默认缩进为 4 个空格。

Completeness and Reset

  • Writer 每次只能输出一个 JSON,根节点可为任意类型。当处理完单个根事件(如 String())或匹配的最终 EndObject() / EndArray() 时,输出 JSON 为完整格式。可通过 Writer::IsComplete() 检测。
  • JSON 完成后,Writer 不能再接受新事件,否则输出将无效(如出现多个根节点)。若要复用 Writer 对象,可调用 Writer::Reset(OutputStream& os) 重置内部状态并设置新输出流。

Techniques

Parsing JSON to Custom Data Structures

Document 的解析完全依赖 Reader。实际上,Document 是一个 handler,接收事件并在解析 JSON 时构建 DOM。

用户可直接使用 Reader 构建其他数据结构,省去构建 DOM 的步骤,减少内存开销并提高性能。

messagereader 示例中,ParseMessages() 解析 JSON,应为对象形式的键值对:

#include "merak/json/reader.h"
#include "merak/json/error/en.h"
#include <iostream>
#include <string>
#include <map>

using namespace std;
using namespace merak::json;

typedef map<string, string> MessageMap;

struct MessageHandler : public BaseReaderHandler<UTF8<>, MessageHandler> {
MessageHandler() : state_(kExpectObjectStart) {}

bool StartObject() {
switch (state_) {
case kExpectObjectStart:
state_ = kExpectNameOrObjectEnd;
return true;
default:
return false;
}
}

bool String(const char* str, SizeType length, bool) {
switch (state_) {
case kExpectNameOrObjectEnd:
name_ = string(str, length);
state_ = kExpectValue;
return true;
case kExpectValue:
messages_.insert(MessageMap::value_type(name_, string(str, length)));
state_ = kExpectNameOrObjectEnd;
return true;
default:
return false;
}
}

bool EndObject(SizeType) { return state_ == kExpectNameOrObjectEnd; }
bool Default() { return false; } // 其他事件无效

MessageMap messages_;
enum State { kExpectObjectStart, kExpectNameOrObjectEnd, kExpectValue } state_;
std::string name_;
};

void ParseMessages(const char* json, MessageMap& messages) {
Reader reader;
MessageHandler handler;
StringStream ss(json);
if (reader.Parse(ss, handler))
messages.swap(handler.messages_);
else {
ParseErrorCode e = reader.GetParseErrorCode();
size_t o = reader.GetErrorOffset();
cout << "Error: " << GetParseError_En(e) << endl;
cout << " at offset " << o << " near '" << string(json).substr(o, 10) << "...'" << endl;
}
}

示例输出:

{ "greeting" : "Hello!", "farewell" : "bye-bye!" }
farewell: bye-bye!
greeting: Hello!

Parse a JSON with invalid schema.
{ "greeting" : "Hello!", "farewell" : "bye-bye!", "foo" : {} }
Error: Terminate parsing due to Handler error.
at offset 59 near '} }...'
  • json1 成功解析为 MessageMap,由于 std::map 的特性,打印顺序按 key 排序,与 JSON 顺序不同。
  • json2foo 的值是空对象,StartObject() 会在 state_ = kExpectValue 下返回 false,导致解析中断,错误码为 kParseErrorTermination

Filtering JSON

Writer 可处理 Reader 发出的事件。例如,example/condense/condense.cppWriter 设置为 Reader 的 handler,移除 JSON 中的所有空白字符。example/pretty/pretty.cpp 将其替换为 PrettyWriter,可为 JSON 添加缩进与换行。

SAX 风格 API 可加入中间层以过滤 JSON 内容。例如 capitalize 示例可将 JSON 中所有字符串转换为大写:

#include "merak/json/reader.h"
#include "merak/json/writer.h"
#include "merak/json/filereadstream.h"
#include "merak/json/filewritestream.h"
#include "merak/json/error/en.h"
#include <vector>
#include <cctype>

using namespace merak::json;

template<typename OutputHandler>
struct CapitalizeFilter {
CapitalizeFilter(OutputHandler& out) : out_(out) {}

bool Null() { return out_.Null(); }
bool Bool(bool b) { return out_.Bool(b); }
bool Int(int i) { return out_.Int(i); }
bool Uint(unsigned u) { return out_.Uint(u); }
bool Int64(int64_t i) { return out_.Int64(i); }
bool Uint64(uint64_t u) { return out_.Uint64(u); }
bool Double(double d) { return out_.Double(d); }
bool RawNumber(const char* str, SizeType length, bool copy) { return out_.RawNumber(str, length, copy); }
bool String(const char* str, SizeType length, bool) {
buffer_.clear();
for (SizeType i = 0; i < length; i++)
buffer_.push_back(std::toupper(str[i]));
return out_.String(&buffer_.front(), length, true);
}
bool StartObject() { return out_.StartObject(); }
bool Key(const char* str, SizeType length, bool copy) { return String(str, length, copy); }
bool EndObject(SizeType memberCount) { return out_.EndObject(memberCount); }
bool StartArray() { return out_.StartArray(); }
bool EndArray(SizeType elementCount) { return out_.EndArray(elementCount); }

OutputHandler& out_;
std::vector<char> buffer_;
};

int main(int, char*[]) {
Reader reader;
char readBuffer[65536];
FileReadStream is(stdin, readBuffer, sizeof(readBuffer));

char writeBuffer[65536];
FileWriteStream os(stdout, writeBuffer, sizeof(writeBuffer));
Writer<FileWriteStream> writer(os);

CapitalizeFilter<Writer<FileWriteStream> > filter(writer);
if (!reader.Parse(is, filter)) {
fprintf(stderr, "\nError(%u): %s\n", (unsigned)reader.GetErrorOffset(), GetParseError_En(reader.GetParseErrorCode()));
return 1;
}

return 0;
}

注意,不能简单地将 JSON 当作字符串全部转换为大写。比如:

["Hello\nWorld"]

直接转换为大写会破坏转义字符:

["HELLO\NWORLD"]

capitalize 生成的结果正确:

["HELLO\nWORLD"]

更复杂的过滤器也可实现,但 SAX API 每次仅提供单个事件信息,用户需要自行记录上下文信息(如根节点路径、相关值)。对于某些场景,使用 DOM 实现比 SAX 更简单。