JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器·进阶篇
(2/3)· 原生缺位下,手搓支持对象数组与动态改值的 JSON 解析器,告别第三方库黑盒
「布尔与空值之外的数值兜底解析」
在 JSON 解析器的词法切分里,true、false、null 都是定长关键字,分别占 4、5、4 个字符,命中后直接推进游标并返回,不再往下走。 若前三者都没匹配上,代码不立即报错,而是把剩余片段丢给 ParseNumber 做兜底,这意味着非布尔非空的标量大概率是数字,否则解析失败。 ParseNumber 先把 m_type 标成 JSON_NUMBER,记下 startPos,再用 while 循环逐字符吞掉负号、正负号、小数点、科学计数 e/E 以及 0–9,遇到非法字符就 break。 最后用 StringSubstr 截出数字串,长度为 0 直接返回 false,否则 StringToDouble 转成 double 写入 m_numVal;这套逻辑在 MT5 里跑通后,你可以用 "1.5e-3" 这种串验证 pos 会停在字母之后而非中途。
pos+=class="num">4; class="kw">return true; } if(StringSubstr(text,pos,class="num">5)=="false") { m_type = JSON_BOOL; m_boolVal = false; pos+=class="num">5; class="kw">return true; } if(StringSubstr(text,pos,class="num">4)=="null") { m_type = JSON_NULL; pos+=class="num">4; class="kw">return true; } class=class="str">"cmt">// Otherwise, treat it as a number or fail class="kw">return ParseNumber(text,pos); } class="type">bool CJsonNode::ParseNumber(class="type">class="kw">string text, class="type">int &pos) { m_type = JSON_NUMBER; class=class="str">"cmt">// capture starting point class="type">int startPos = pos; class="kw">while(pos < StringLen(text)) { class="type">class="kw">string c = StringSubstr(text,pos,class="num">1); if(c=="-" || c=="+" || c=="." || c=="e" || c=="E" || (c>="class="num">0" && c<="class="num">9")) { pos++; } else break; } class=class="str">"cmt">// substring from startPos to pos class="type">class="kw">string numStr = StringSubstr(text,startPos,pos-startPos); if(StringLen(numStr)==class="num">0) class="kw">return false; class=class="str">"cmt">// convert to class="type">class="kw">double m_numVal = StringToDouble(numStr); class="kw">return true; }
◍ 扩展我们的解析器,具备高级功能
当下,我们已拥有一款功能齐全的 MQL5 版本 JSON 解析器,即能够处理对象、数组、字符串、数字、布尔值、和空值。在本章中,我们将探讨更多功能和改进。我们将讨论如何以一种更便捷途径去提取子元素,如何优雅地处理潜在错误,甚至如何将数据转换回 JSON 文本。通过将这些强化功能叠加在我们所构建解析器之上,您将获得一个更健壮、更灵活的工具 — 单枪匹马就能满足各种现世需求。 按主键或索引提取子项 如果我们的解析器要真正实用,我们打算轻松吸纳对象中某个主键的数值,或数组中特定索引处的数值。比方说我们得到这个 JSON: 我们想象将其解析到一个名为 rootNode 的根 CJsonNodeobject。我们希望能做一些事情,譬如: 如果我们在解析器中定义 GetChild(const string key),我们当前的代码结构或许允许这样做。这是该方法在您的 CJsonNodeclass 中可能的样子: 以这种方式,如果当前节点不是一个对象,我们简单地返回 NULL。否则,我们会扫描所有 m_keysto 来找到一个匹配的。如果有,我们返回对应的子项指针。 同样,我们能针对数组定义一个方法: 如果节点是数组,我们简单地检查边界,并返回相应元素。如果不是数组 — 或者索引超出范围 — 我们返回 NULL。在取消引用前,在您的代码中检查 NULL 非常重要。 优雅地处理错误 在许多现世场景中,JSON 或许会遇到错误格式(例如,缺少引号、尾随逗号、或异常符号)。一个健壮的解析器应当能检测、并报告这些错误。您能够以按此行事: 返回一个布尔值:我们的大多数解析方法都已返回布尔值。如果有什么失败了,我们返回 false。但我们也能存储一个内部错误信息,像是 m_errorMsg,如此调用代码就能看到哪里出了问题。 继续解析还是中止?:一旦检测到致命的解析错误 — 比方说意外字符、或未闭合的括号 — 您或许会决定中止整个解析,保持节点处于“无效”状态。备案,您也能尝试跳过或恢复,但那会更先进。 此处有个概念上的调整:在 ParseArray 或 ParseObject 里,若您看到一些意想不到的内容(像是没有引号的主键、或缺了冒号),您可以写: 然后,在您的调用代码中,您或许这样做: 由您决定这些信息的细节要多深。有时,一次“解析失败”就足以触发交易场景。其它时候,您虎嗅需要更细致的调试 JSON 输入。 将 MQL5 数据转换回 JSON 格式 读取 JSON 仅是故事的一半。如果您打算把数据发送回服务器,或者以 JSON 格式书写自己的日志怎么办?您能够用 “serializer” 方法扩展您的 CJsonNodeclass,它会遍历节点数据,并重建 JSON 文本。我们称之为 ToJsonString(),例如: 然后您就能定义,例如 SerializeObject: 数组亦类似: 您会注意到我们调用了 EscapeString 函数。我们你网购复用处理 JSON 字符串转义的代码 — 像是把特殊字符写成 \“、\\、\n、等等。若输出中包含引号、或换行符时,这样可确保是有效的 JSON。 如果您喜欢“漂亮”的 JSON,只需插入一些断行符(“\n”)和缩进。一种方式是基于深度构建一串空格,这样您的 JSON 结构在视觉上更清晰: 然后在每行或每元素前插入缩进。这是可选的,但如果您日常需要手工读取、或调试 JSON 输出,这就很方便。 如果您的 JSON 数据很庞大,比方说数万行,您可能需要考虑性能: 高效的字符串操作 请留意,重复性子字符串操作(Stri
JSON 节点的序列化与取值落点
CJsonNode 的序列化靠一个分派函数按类型走不同分支:对象交 SerializeObject、数组交 SerializeArray、字符串包双引号并转义、数字用 DoubleToString(m_numVal, 10) 保留 10 位精度、布尔转 true/false、空值写 null,未识别类型回退到空字符串占位。 SerializeObject 与 SerializeArray 都用 for 循环拼 result,i>0 时才插逗号,避免尾部多余分隔;对象额外对 key 做 EscapeString 并包引号,子节点递归时 depth+1 把缩进层级传下去。 实际取配置数值时别直接读裸指针。下面这段先查 parent 的 lots 子节点,类型命中 JSON_NUMBER 才转 double;更稳的写法是 getDoubleOrDefault,key 缺失或类型不对就回退默认值,外汇/贵金属参数读取用这套能少踩空指针坑,但杠杆与仓位误设仍属高风险操作。
{
case JSON_OBJ:
class="kw">return SerializeObject(depth);
case JSON_ARRAY:
class="kw">return SerializeArray(depth);
case JSON_STRING:
class="kw">return "\""+EscapeString(m_value)+"\"";
case JSON_NUMBER:
{
class=class="str">"cmt">// Convert class="type">class="kw">double to class="type">class="kw">string carefully
class="kw">return DoubleToString(m_numVal, class="num">10);
}
case JSON_BOOL:
class="kw">return m_boolVal ? "true":"false";
case JSON_NULL:
class="kw">return "null";
class="kw">default:
class="kw">return "\"\""; class=class="str">"cmt">// or some placeholder
}
}
class="type">class="kw">string CJsonNode::SerializeObject(class="type">int depth) class="kw">const
{
class="type">class="kw">string result = "{";
for(class="type">int i=class="num">0; i<ArraySize(m_children); i++)
{
if(i>class="num">0) result += ",";
class="type">class="kw">string key = EscapeString(m_keys[i]);
class="type">class="kw">string value = m_children[i].SerializeNode(depth+class="num">1);
result += "\""+key+"\":";
result += value;
}
result += "}";
class="kw">return result;
}
class="type">class="kw">string CJsonNode::SerializeArray(class="type">int depth) class="kw">const
{
class="type">class="kw">string result = "[";
for(class="type">int i=class="num">0; i<ArraySize(m_children); i++)
{
if(i>class="num">0) result += ",";
result += m_children[i].SerializeNode(depth+class="num">1);
}
result += "]";
class="kw">return result;
}
class="type">class="kw">string indentation = "";
for(class="type">int d=class="num">0; d<depth; d++)
indentation += " ";
CJsonNode* node = parent.GetChild("lots");
if(node != NULL && node.GetType() == JSON_NUMBER)
class="type">class="kw">double myLots = node.AsNumber();
class="type">class="kw">double getDoubleOrDefault(CJsonNode &obj, class="kw">const class="type">class="kw">string key, class="type">class="kw">double defaultVal)
{
CJsonNode* c = obj.GetChild(key);
if(c == NULL || c.GetType() != JSON_NUMBER)
class="kw">return defaultVal;
class="kw">return c.AsNumber();
}「把解析器收进一个可复用模块」
前面几节拆开的 JSON 解析、错误检查、逆序列化与按键取值逻辑,现在可以并到同一个 CJsonNode 类里。把它存成 .mqh 再在 MT5 项目里 include,比每次重写散落片段更稳,也方便在 EA 或脚本里直接调。 下面这份实现覆盖了对象/数组两种结构的解析,类型用 JsonNodeType 枚举区分,未定义节点值为 0。公开方法里 ParseString 吃整段文本,IsValid 与 GetErrorMsg 负责排查脏数据,GetChild 重载了按 key 和按 index 两种取法。 私有段能看到 m_children 与 m_keys 两个动态数组配对存子树,m_valid 标记解析成败。注意 ParseObject 只是声明,真正递归吃字符的活儿在 ParseValue 里,自己接手时记得把这两个内部函数补全,否则链接期就会报未实现。 外汇与贵金属行情接口常返回嵌套 JSON,用这套模块解析时须留意网络延迟与报文截断带来的高风险,解析失败分支不要直接假设数据合法。
class="macro">#ifndef __CJSONNODE_MQH__ class="macro">#define __CJSONNODE_MQH__ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CJsonNode.mqh - A Minimalistic JSON Parser & Serializer in MQL5 | class=class="str">"cmt">//| Feel free to adapt as needed. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property strict class=class="str">"cmt">//--- Enumeration of possible JSON node types enum JsonNodeType { JSON_UNDEF = class="num">0, JSON_OBJ, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL }; class=class="str">"cmt">//+-----------------------------------------------------------------+ class=class="str">"cmt">//| Class representing a single JSON node | class=class="str">"cmt">//+-----------------------------------------------------------------+ class CJsonNode { class="kw">public: class=class="str">"cmt">//--- Constructor & Destructor CJsonNode(); ~CJsonNode(); class=class="str">"cmt">//--- Parse entire JSON text class="type">bool ParseString(class="type">class="kw">string jsonText); class=class="str">"cmt">//--- Check if node is valid class="type">bool IsValid(); class=class="str">"cmt">//--- Get potential error message if not valid class="type">class="kw">string GetErrorMsg(); class=class="str">"cmt">//--- Access node type JsonNodeType GetType(); class=class="str">"cmt">//--- For arrays class="type">int ChildCount(); class=class="str">"cmt">//--- For objects: get child by key CJsonNode* GetChild(class="type">class="kw">string key); class=class="str">"cmt">//--- For arrays: get child by index CJsonNode* GetChild(class="type">int index); class=class="str">"cmt">//--- Convert to class="type">class="kw">string / number / class="type">bool class="type">class="kw">string AsString(); class="type">class="kw">double AsNumber(); class="type">bool AsBool(); class=class="str">"cmt">//--- Serialize back to JSON class="type">class="kw">string ToJsonString(); class="kw">private: class=class="str">"cmt">//--- Data members JsonNodeType m_type; class=class="str">"cmt">// Type of this node(object, array, etc.) class="type">class="kw">string m_value; class=class="str">"cmt">// For storing class="type">class="kw">string content if node is class="type">class="kw">string class="type">class="kw">double m_numVal; class=class="str">"cmt">// For numeric values class="type">bool m_boolVal; class=class="str">"cmt">// For boolean values CJsonNode m_children[]; class=class="str">"cmt">// Child nodes(for objects and arrays) class="type">class="kw">string m_keys[]; class=class="str">"cmt">// Keys for child nodes(valid if JSON_OBJ) class="type">bool m_valid; class=class="str">"cmt">// True if node is validly parsed class="type">class="kw">string m_errMsg; class=class="str">"cmt">// Optional error message for debugging class=class="str">"cmt">//--- Internal methods class="type">void Reset(); class="type">bool ParseValue(class="type">class="kw">string text,class="type">int &pos); class="type">bool ParseObject(class="type">class="kw">string text,class="type">int &pos);
◍ JSON 解析类的构造与入口函数
在 MT5 里手写一个 JSON 解析器,第一步是把节点对象的状态清零。CJsonNode 的构造函数把 m_type 设为 JSON_UNDEF、m_numVal 置 0.0、m_boolVal 置 false,并用 ArrayResize 把 m_children 和 m_keys 两个动态数组长度压到 0,保证每次新建对象不残留上一次的结构。 析构函数反而什么都不用做——MQL5 的数组由运行时自行回收,没有手动 malloc 的指针需要释放,这一点和 C++ 差别明显,也少了内存泄漏的坑。 真正的解析入口是 ParseString。它先 Reset() 清空旧状态,从 pos=0 开始调 ParseValue,再用 SkipWhitespace 跳过尾部空白;若 pos 没走到 StringLen(jsonText) 的末尾,且剩余不是纯空白,就判为“Extra data after JSON parsing.”并置 m_valid=false。也就是说,一段文本里若混了多个 JSON 值,这套逻辑会直接报错而不是吞掉。 把下面这段粘进 MT5 的 MQH 里就能看到类的骨架与解析边界处理,省得自己从头判字符串越界。
class="type">bool ParseArray(class="type">class="kw">string text,class="type">int &pos); class="type">bool ParseNumber(class="type">class="kw">string text,class="type">int &pos); class="type">bool ParseStringLiteral(class="type">class="kw">string text,class="type">int &pos); class="type">bool ParseKeyLiteral(class="type">class="kw">string text,class="type">int &pos,class="type">class="kw">string &keyOut); class="type">class="kw">string UnescapeString(class="type">class="kw">string input_); class="type">bool SkipWhitespace(class="type">class="kw">string text,class="type">int &pos); class="type">bool AllWhitespace(class="type">class="kw">string text,class="type">int pos); class="type">class="kw">string SerializeNode(); class="type">class="kw">string SerializeObject(); class="type">class="kw">string SerializeArray(); class="type">class="kw">string EscapeString(class="type">class="kw">string s); }; class=class="str">"cmt">//+-----------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+-----------------------------------------------------------------+ CJsonNode::CJsonNode() { m_type = JSON_UNDEF; m_value = ""; m_numVal = class="num">0.0; m_boolVal = false; m_valid = true; ArrayResize(m_children,class="num">0); ArrayResize(m_keys,class="num">0); m_errMsg = ""; } class=class="str">"cmt">//+-----------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+-----------------------------------------------------------------+ CJsonNode::~CJsonNode() { class=class="str">"cmt">// No dynamic pointers to free; arrays are handled by MQL itself } class=class="str">"cmt">//+-----------------------------------------------------------------+ class=class="str">"cmt">//| Parse entire JSON text | class=class="str">"cmt">//+-----------------------------------------------------------------+ class="type">bool CJsonNode::ParseString(class="type">class="kw">string jsonText) { Reset(); class="type">int pos = class="num">0; class="type">bool res = (ParseValue(jsonText,pos) && SkipWhitespace(jsonText,pos)); class=class="str">"cmt">// If there&class="macro">#x27;s leftover text that&class="macro">#x27;s not whitespace, it&class="macro">#x27;s an error if(pos < StringLen(jsonText)) { if(!AllWhitespace(jsonText,pos)) { m_valid = false; m_errMsg = "Extra data after JSON parsing."; res = false; } } class="kw">return (res && m_valid); } class=class="str">"cmt">//+-----------------------------------------------------------------+ class=class="str">"cmt">//| Check if node is valid | class=class="str">"cmt">//+-----------------------------------------------------------------+ class="type">bool CJsonNode::IsValid() {
取数接口的容错与类型分支
CJsonNode 的读取类方法把「合法性」和「类型匹配」放在第一道闸门。GetChild(string key) 先判 m_type != JSON_OBJ 直接返 NULL,说明对象节点才允许按键取值,数组或标量调这个方法必然落空。 按索引取子节点时,除类型检查外还卡了边界:index<0 或 index>=ArraySize(m_children) 都返 NULL。这意味着遍历 JSON 数组前用 ChildCount() 拿长度更稳,避免越界指针。 AsString() 暴露了数值精度设定:JSON_NUMBER 走 DoubleToString(m_numVal,8),固定 8 位小数。若你接的行情接口返回第 9 位有效位,这里会被截断,回测和实盘解析可能差 1 个点外的值。 GetErrorMsg() 与 GetType() 都是单行返回内部字段,调试时若 m_valid 为 false,优先读 GetErrorMsg() 而非猜节点结构。外汇与贵金属 JSON 推送常含嵌套数组,用这些接口解析时须留意高风险——报文畸变会让类型判断失效。
class="type">class="kw">string CJsonNode::GetErrorMsg() { class="kw">return m_errMsg; } JsonNodeType CJsonNode::GetType() { class="kw">return m_type; } class="type">int CJsonNode::ChildCount() { class="kw">return ArraySize(m_children); } CJsonNode* CJsonNode::GetChild(class="type">class="kw">string key) { if(m_type != JSON_OBJ) class="kw">return NULL; for(class="type">int i=class="num">0; i<ArraySize(m_keys); i++) { if(m_keys[i] == key) class="kw">return &m_children[i]; } class="kw">return NULL; } CJsonNode* CJsonNode::GetChild(class="type">int index) { if(m_type != JSON_ARRAY) class="kw">return NULL; if(index<class="num">0 || index>=ArraySize(m_children)) class="kw">return NULL; class="kw">return &m_children[index]; } class="type">class="kw">string CJsonNode::AsString() { if(m_type == JSON_STRING) class="kw">return m_value; if(m_type == JSON_NUMBER) class="kw">return DoubleToString(m_numVal,class="num">8); if(m_type == JSON_BOOL) class="kw">return m_boolVal ? "true" : "false"; if(m_type == JSON_NULL) class="kw">return "null"; class="kw">return ""; }