JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器·综合运用
(3/3)· 当标准库缺席,如何用九步在 MT5 里造出能读能改的 JSON 引擎
「JSON 节点的取值与解析入口」
在 MT5 里做 AIGC 行情接口对接时,CJsonNode 这类封装决定了你从返回报文里能掏出什么。AsNumber 方法只认 JSON_NUMBER 类型,若节点是布尔则返 1.0 或 0.0,其余一律 0.0——这意味着乱用类型转换会静默吃掉你的数据。 AsBool 的兜底逻辑更野:数字非零即真,字符串长度大于 0 也为真。如果你拿它去判一个空数组节点,这里不会返回真,因为类型既不是 BOOL、NUMBER 也不是 STRING,直接落到了 false。 Reset 把节点回到 JSON_UNDEF 初始态,顺手用 ArrayResize(m_children,0) 和 ArrayResize(m_keys,0) 清空子节点与键表。每次重新解析前不清 Reset,旧 children 可能残留导致结构错乱。 ParseValue 是分发器:先 SkipWhitespace 跳过空白,越界直接返 false;再看首字符——'{' 走对象、'[' 走数组、'"' 走字符串,遇到子串 'true' 就原地置布尔真并 pos+=4。注意它只处理了 true,false 和 null 在同节未列,实际解析需确认后续分支。
class="type">class="kw">double CJsonNode::AsNumber() { if(m_type == JSON_NUMBER) class="kw">return m_numVal; class=class="str">"cmt">// If class="type">bool, class="kw">return class="num">1 or class="num">0 if(m_type == JSON_BOOL) class="kw">return (m_boolVal ? class="num">1.0 : class="num">0.0); class="kw">return class="num">0.0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Convert node to boolean | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CJsonNode::AsBool() { if(m_type == JSON_BOOL) class="kw">return m_boolVal; if(m_type == JSON_NUMBER) class="kw">return (m_numVal != class="num">0.0); if(m_type == JSON_STRING) class="kw">return (StringLen(m_value) > class="num">0); class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Serialize node back to JSON | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CJsonNode::ToJsonString() { class="kw">return SerializeNode(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Reset node to initial state | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CJsonNode::Reset() { 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">//| Dispatch parse based on first character | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CJsonNode::ParseValue(class="type">class="kw">string text,class="type">int &pos) { if(!SkipWhitespace(text,pos)) class="kw">return false; if(pos >= StringLen(text)) class="kw">return false; class="type">class="kw">string c = StringSubstr(text,pos,class="num">1); class=class="str">"cmt">//--- Object if(c == "{") class="kw">return ParseObject(text,pos); class=class="str">"cmt">//--- Array if(c == "[") class="kw">return ParseArray(text,pos); class=class="str">"cmt">//--- String if(c == "\"") class="kw">return ParseStringLiteral(text,pos); class=class="str">"cmt">//--- Boolean / null if(StringSubstr(text,pos,class="num">4) == "true") { m_type = JSON_BOOL; m_boolVal = true; pos += class="num">4; class="kw">return true; }
◍ JSON 对象节点怎么在 MT5 里逐字符啃下来
在自写的 CJsonNode 类里,ParseObject 负责把 {...} 这一段从字符串里抠成树节点。它先吃掉开头的 { 并后移 pos,再调 SkipWhitespace 跳过空格;若下一个字符就是 },直接 pos++ 返回,说明是个空对象,不建任何子节点。
非空对象走 while 循环:每轮先跳过空白,然后强制要求键以双引号开头,否则把 m_valid 置 false 并报 Object key must start with double quote.。键名由 ParseKeyLiteral 读出,随后必须出现冒号,缺失就抛 Missing colon after object key.——这两个报错信息在调试 EA 接收的 WebRequest 回包时很实用。
冒号跳过后再跳过空白,用局部 CJsonNode child 调 ParseValue 递归解析值;只要子值解析失败,本层就标记 Failed to parse object value. 并退出。整段没有用正则,纯靠 StringSubstr(text,pos,1) 做单字符比对,在 30 KB 以内的 JSON 回包上开销可忽略,但你拿去解析超长 tick 流时要留意 StringLen 的反复调用次数。
别把正态当圣经
这套手写解析若遇到键里带转义双引号,得确认 ParseKeyLiteral 自己处理了反斜杠,否则 pos 会错位到值解析阶段才爆错,排错成本翻倍。
class="type">bool CJsonNode::ParseObject(class="type">class="kw">string text,class="type">int &pos) { m_type = JSON_OBJ; pos++; class=class="str">"cmt">// skip &class="macro">#x27;{&class="macro">#x27; if(!SkipWhitespace(text,pos)) class="kw">return false; class=class="str">"cmt">//--- Check for empty object if(pos < StringLen(text) && StringSubstr(text,pos,class="num">1) == "}") { pos++; class="kw">return true; } class=class="str">"cmt">//--- Parse key-value pairs class="kw">while(pos < StringLen(text)) { if(!SkipWhitespace(text,pos)) class="kw">return false; class=class="str">"cmt">// Expect key in quotes if(pos >= StringLen(text) || StringSubstr(text,pos,class="num">1) != "\"") { m_valid = false; m_errMsg = "Object key must start with class="type">class="kw">double quote."; class="kw">return false; } class="type">class="kw">string key = ""; if(!ParseKeyLiteral(text,pos,key)) class="kw">return false; if(!SkipWhitespace(text,pos)) class="kw">return false; class=class="str">"cmt">// Expect a colon if(pos >= StringLen(text) || StringSubstr(text,pos,class="num">1) != ":") { m_valid = false; m_errMsg = "Missing colon after object key."; class="kw">return false; } pos++; class=class="str">"cmt">// skip &class="macro">#x27;:&class="macro">#x27; if(!SkipWhitespace(text,pos)) class="kw">return false; class=class="str">"cmt">// Parse the child value CJsonNode child; if(!child.ParseValue(text,pos)) { m_valid = false; m_errMsg = "Failed to parse object value."; class="kw">return false; }
对象与数组的节点挂载差异
在 JSON 解析器里,对象节点和数组节点都靠同一个 m_children 动态数组挂子项,但键的处理完全不同。对象每挂一个子节点,会同步在 m_keys 里存一个 key,数组则只压 child、m_keys 保持空位。 下面这段是对象写入子节点的核心片段:先取当前数组长度 idx,把 m_children 和 m_keys 都扩容 1,再按 idx 写入 child 与 key。随后跳空白、判结尾——遇到 '}' 直接 pos++ 返回 true,遇到非 ',' 且非 '}' 就置 m_valid=false 并报 Missing comma in object。
class=class="str">"cmt">// Store class="type">int idx = ArraySize(m_children); ArrayResize(m_children,idx+class="num">1); ArrayResize(m_keys,idx+class="num">1); m_children[idx] = child; m_keys[idx] = key; if(!SkipWhitespace(text,pos)) class="kw">return false; if(pos >= StringLen(text)) class="kw">return false; class="type">class="kw">string nextC = StringSubstr(text,pos,class="num">1); if(nextC == "}") { pos++; class="kw">return true; } if(nextC != ",") { m_valid = false; m_errMsg = "Missing comma in object."; class="kw">return false; } pos++; class=class="str">"cmt">// skip comma } class="kw">return false; class=class="str">"cmt">// didn&class="macro">#x27;t see closing &class="macro">#x27;}&class="macro">#x27;
class=class="str">"cmt">// Store class="type">int idx = ArraySize(m_children); ArrayResize(m_children,idx+class="num">1); ArrayResize(m_keys,idx+class="num">1); m_children[idx] = child; m_keys[idx] = key; if(!SkipWhitespace(text,pos)) class="kw">return false; if(pos >= StringLen(text)) class="kw">return false; class="type">class="kw">string nextC = StringSubstr(text,pos,class="num">1); if(nextC == "}") { pos++; class="kw">return true; } if(nextC != ",") { m_valid = false; m_errMsg = "Missing comma in object."; class="kw">return false; } pos++; class=class="str">"cmt">// skip comma } class="kw">return false; class=class="str">"cmt">// didn&class="macro">#x27;t see closing &class="macro">#x27;}&class="macro">#x27;
「数值与字符串的逐字符解析逻辑」
在自写 JSON 解析类里,ParseNumber 靠一个白名单循环吃字符:只要碰到 -、+、.、e、E 或 0–9 就继续推进 pos,否则立即 break。实测若传入 "12.5E-3" 这类字符串,循环会完整吞掉 7 个字符再交給 StringToDouble,转换后得到 double 值 0.0125,类型标记为 JSON_NUMBER。 ParseStringLiteral 假设调用前已确认首字符是双引号,进门先 pos++ 跳过它。随后逐字符扫描:遇到 " 就闭合、把累积结果丢给 UnescapeString 解码并返回 true;遇到 \ 则把反斜杠和下一个字符原样拼进 result,留待后续统一反转义。若文本在反斜杠后直接截断(pos 越界),循环 break 且不会返回 true,调用方需自行判 m_valid。 两个函数都直接修改入参 int &pos,这意味着上层解析器可以靠同一个下标变量在 text 上滑动,不必每层重新计算偏移。开 MT5 把这段塞进 EA 的 include 里,用 Print(ParseNumber("12.5E-3",pos)) 验证返回值,能直观看到浮点解析路径是否符合预期。
class="type">bool CJsonNode::ParseNumber(class="type">class="kw">string text,class="type">int &pos) { m_type = JSON_NUMBER; class="type">int startPos = pos; class=class="str">"cmt">// Scan allowed chars in a JSON number 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="type">class="kw">string numStr = StringSubstr(text,startPos,pos - startPos); if(StringLen(numStr) == class="num">0) { m_valid = false; m_errMsg = "Expected number, found empty."; class="kw">return false; } m_numVal = StringToDouble(numStr); class="kw">return true; } class="type">bool CJsonNode::ParseStringLiteral(class="type">class="kw">string text,class="type">int &pos) { pos++; class=class="str">"cmt">// skip leading quote class="type">class="kw">string result = ""; class="kw">while(pos < StringLen(text)) { class="type">class="kw">string c = StringSubstr(text,pos,class="num">1); if(c == "\"") { class=class="str">"cmt">// closing quote pos++; m_type = JSON_STRING; m_value = UnescapeString(result); class="kw">return true; } if(c == "\\") { class=class="str">"cmt">// handle escape pos++; if(pos >= StringLen(text)) break; class="type">class="kw">string ec = StringSubstr(text,pos,class="num">1); result += ("\\" + ec); class=class="str">"cmt">// accumulate, we&class="macro">#x27;ll decode later pos++; } else { result += c;
◍ JSON 键名与转义字符的解析落点
在 MT5 里手写 JSON 解析器时,键名(key)的提取逻辑和字符串字面量几乎同构,差别只在用途:ParseKeyLiteral 跳过前导双引号后,用 buffer 逐字符拼接,遇到 " 即闭合并返回 UnescapeString 处理后的 keyOut。若遍历到文本末尾仍未碰到闭合引号,则置 m_valid=false 并抛出 "Unclosed key string.",此时上层调用方应当终止解析。 转义处理集中在 UnescapeString:遇到 \ 时下标 i 自增,若已越界则补一个裸反斜杠并 break;否则取紧随其后的字符 ec,目前仅对 " 与 \ 做还原映射(即 out += "\" 或 out += "\\")。这意味着 \n、\t 这类序列在该实现里不会被展开成换行或制表符,若你的 EA 配置含此类字符,读取后可能仍是字面量。 直接验证方式:把上面两段代码塞进 CJsonNode 类,构造一个含未闭合键名的 string 如 {"key: 1},调用 ParseKeyLiteral 后应拿到 m_valid=false;再喂入 "a\\b" 做 UnescapeString,返回串长度应为 3 而非 4。外汇与贵金属 EA 加载外部 JSON 配置属高风险操作,解析失败静默继续可能让仓位参数失真。
pos++;
}
}
class=class="str">"cmt">// If we get here, class="type">class="kw">string was not closed
m_valid = false;
m_errMsg = "Unclosed class="type">class="kw">string literal.";
class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parse a class="type">class="kw">string key(similar to a literal) |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CJsonNode::ParseKeyLiteral(class="type">class="kw">string text,class="type">int &pos,class="type">class="kw">string &keyOut)
{
pos++; class=class="str">"cmt">// skip leading quote
class="type">class="kw">string buffer = "";
class="kw">while(pos < StringLen(text))
{
class="type">class="kw">string c = StringSubstr(text,pos,class="num">1);
if(c == "\"")
{
pos++;
keyOut = UnescapeString(buffer);
class="kw">return true;
}
if(c == "\\")
{
pos++;
if(pos >= StringLen(text))
break;
class="type">class="kw">string ec = StringSubstr(text,pos,class="num">1);
buffer += ("\\" + ec);
pos++;
}
else
{
buffer += c;
pos++;
}
}
m_valid = false;
m_errMsg = "Unclosed key class="type">class="kw">string.";
class="kw">return false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Unescape sequences like \" \\ \n etc. |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CJsonNode::UnescapeString(class="type">class="kw">string input_)
{
class="type">class="kw">string out = "";
class="type">int i = class="num">0;
class="kw">while(i < StringLen(input_))
{
class="type">class="kw">string c = StringSubstr(input_,i,class="num">1);
if(c == "\\")
{
i++;
if(i >= StringLen(input_))
{
class=class="str">"cmt">// Single backslash at end
out += "\\";
break;
}
class="type">class="kw">string ec = StringSubstr(input_,i,class="num">1);
if(ec == "\"") out += "\"";
else if(ec == "\\") out += "\\";转义字符与空白跳过的底层处理
在 MQL5 里手写 JSON 解析器时,反斜杠后的字符必须逐一映射为控制符:n 转换行、r 转回车、t 转制表,b 和 f 则分别用 CharToString(8) 与 CharToString(12) 补上 ASCII 退格和换页。若反斜杠后不是预定字母,就原样拼回 "\"+ec,避免误吞非法转义。 SkipWhitespace 从 pos 起扫描,遇到空格(32)、制表(9)、换行(10)、回车(13) 便自增下标,直到撞上非空白或越界;返回 pos<=StringLen(text) 表示尚未彻底超出。AllWhitespace 则反向判定:只要剩余段出现任一非四种空白字符立即返回 false,全段干净才返回 true。 这两段是序列化前置的依赖:SerializeNode 按 m_type 分发到 SerializeObject / SerializeArray,而在递归拼串过程中,跳过空白与校验尾随空白直接决定了解析容错度。实盘跑 EA 前,可在 MetaEditor 里把 SkipWhitespace 单独挂个脚本,喂入带缩进的 JSON 字符串,观察 pos 推进是否符合预期。外汇与贵金属行情高波动,任何配置解析偏差都可能让风控参数静默失效,建议仅用历史回测数据验证此类工具函数。
else if(ec == "n") out += "\n"; else if(ec == "r") out += "\r"; else if(ec == "t") out += "\t"; else if(ec == "b") out += CharToString(class="num">8); class=class="str">"cmt">// ASCII backspace else if(ec == "f") out += CharToString(class="num">12); class=class="str">"cmt">// ASCII formfeed else out += ("\\" + ec); i++; } else { out += c; i++; } } class="kw">return out; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Skip whitespace | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CJsonNode::SkipWhitespace(class="type">class="kw">string text,class="type">int &pos) { class="kw">while(pos < StringLen(text)) { class="type">class="kw">ushort c = StringGetCharacter(text,pos); if(c == &class="macro">#x27; &class="macro">#x27; || c == &class="macro">#x27;\t&class="macro">#x27; || c == &class="macro">#x27;\n&class="macro">#x27; || c == &class="macro">#x27;\r&class="macro">#x27;) pos++; else break; } class=class="str">"cmt">// Return true if we haven&class="macro">#x27;t gone beyond class="type">class="kw">string length class="kw">return (pos <= StringLen(text)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if remainder is all whitespace | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CJsonNode::AllWhitespace(class="type">class="kw">string text,class="type">int pos) { class="kw">while(pos < StringLen(text)) { class="type">class="kw">ushort c = StringGetCharacter(text,pos); if(c != &class="macro">#x27; &class="macro">#x27; && c != &class="macro">#x27;\t&class="macro">#x27; && c != &class="macro">#x27;\n&class="macro">#x27; && c != &class="macro">#x27;\r&class="macro">#x27;) class="kw">return false; pos++; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Serialization dispatcher | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CJsonNode::SerializeNode() { class="kw">switch(m_type) { case JSON_OBJ: class="kw">return SerializeObject(); case JSON_ARRAY: class="kw">return SerializeArray();
「正文」
<span class="keyword">case</span> JSON_STRING: <span class="keyword">return</span> <span class="string">"\""</span>+EscapeString(m_value)+<span class="string">"\""</span>; <span class="keyword">case</span> JSON_NUMBER: <span class="keyword">return</span> <span class="functions">DoubleToString</span>(m_numVal,<span class="number">8</span>); <span class="keyword">case</span> JSON_BOOL: <span class="keyword">return</span> (m_boolVal ? <span class="string">"true"</span> : <span class="string">"false"</span>); <span class="keyword">case</span> JSON_NULL: <span class="keyword">return</span> <span class="string">"null"</span>; <span class="keyword">default</span>: <span class="keyword">return</span> <span class="string">"\"\""</span>; <span class="comment">// undefined => empty string</span> } } <span class="comment">//+------------------------------------------------------------------+</span> <span class="comment">//| Serialize object &nb
◍ 控制字符转义与一段可用的 JSON 解析样例
上面这段 switch 的收尾分支,是在把不可见控制字符翻成 JSON 标准转义串:13 对应回车 \r,9 对应制表 \t,8 是退格 \b,12 是换页 \f;default 分支则直接把普通字符用 CharToString(c) 拼回字符串,不再额外处理。 这段逻辑嵌在 CJsonNode 的序列化出口里,意味着你从 MT5 日志里拷出来的字符串,控制字符已经被显式转义,不会悄悄截断。 下面附的 OnStart 是一段能直接跑的验证代码:用 {"name":"Alice","age":30,"admin":true,"items":[1,2,3],"misc":null} 做输入,ParseString 成功后能分别用 GetChild(...).AsString() / AsNumber() / AsBool() 取字段,再用 ToJsonString() 反向序列化。 在 MT5 里建个脚本把这段代码贴进去,编译跑一次,若日志打出 'JSON parsed successfully!' 和 Name: Alice / Age: 30.0 / Admin?true,就说明你的 CJsonNode.mqh 版本转义与解析链路是通的。外汇与贵金属行情数据走 JSON 接口时,这类解析失败往往源于控制字符未转义,属高风险接入点,建议每次对接新源都先跑一遍该样例。
case class="num">13: class=class="str">"cmt">// &class="macro">#x27;\r&class="macro">#x27; out += "\\r"; break; case class="num">9: class=class="str">"cmt">// &class="macro">#x27;\t&class="macro">#x27; out += "\\t"; break; case class="num">8: class=class="str">"cmt">// backspace out += "\\b"; break; case class="num">12: class=class="str">"cmt">// formfeed out += "\\f"; break; class="kw">default: class=class="str">"cmt">// Directly append character out += CharToString(c); break; } } class="kw">return out; } class="macro">#endif class=class="str">"cmt">// __CJSONNODE_MQH__ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ProjectName | class=class="str">"cmt">//| Copyright class="num">2020, CompanyName | class=class="str">"cmt">//| http://www.companyname.net | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property strict class="macro">#include <CJsonNode.mqh> class="type">void OnStart() { class=class="str">"cmt">// Some JSON text class="type">class="kw">string jsonText = "{\"name\":\"Alice\",\"age\":class="num">30,\"admin\":true,\"items\":[class="num">1,class="num">2,class="num">3],\"misc\":null}"; CJsonNode parser; if(parser.ParseString(jsonText)) { Print("JSON parsed successfully!"); Print("Name: ", parser.GetChild("name").AsString()); Print("Age: ", parser.GetChild("age").AsNumber()); Print("Admin?", parser.GetChild("admin").AsBool()); class=class="str">"cmt">// Serialize back Print("Re-serialized JSON: ", parser.ToJsonString()); } else { Print("JSON parsing error: ", parser.GetErrorMsg()); } } class=class="str">"cmt">//+------------------------------------------------------------------+
画得少,看得清
把前面几节攒下的 CJsonNode 函数库跑通后,你在 MT5 里已经能直接干四件事:用 ParseString() 把原生文本变成节点树,用 GetChild(key) 或 GetChild(index) 在对象与数组之间来回切,用 CheckIsValid() 和 GetErrorMsg() 抓括号不匹配之类的解析错误,再用 ToJsonString() 把节点重新拼回合法 JSON。 社区里有人拿这个思路做过压力测试:在 EURUSD H1 上解析 107,652,093 字节的 JSON 耗时约 1,101,111 微秒,峰值 MQL 内存吃到 2493 MB——这说明海量行情或交易所 API 回传一旦进 MT5,就得掂量终端内存余量。 真要接 Binance 这类 700KB+ 的 exchangeInfo,建议先在小样本上验证解析路径,再决定要不要上流式处理。外汇和贵金属杠杆高、滑点突变频繁,任何自动读取参数的环节都可能因格式漂移直接断线,上线前务必在策略测试器里跑一遍异常 JSON。 函数库本身留了口子:更细的错误码、数值强转、大文件流式读写都能自己补。基础骨架够覆盖从本地配置文件加载到网页 API 交互的典型场景,剩下的是你按交易逻辑往上叠。