JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器·进阶篇
🧩

JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器·进阶篇

(2/3)· 原生缺位下,手搓支持对象数组与动态改值的 JSON 解析器,告别第三方库黑盒

案例拆解新手友好 第 2/3 篇
很多交易者直接把网上的 MQL5 JSON 库拖进工程,却看不懂内部怎么遍历节点,一旦经纪商 API 改了字段就抓瞎。本篇带你从类结构画起,每一步都清楚数据存在哪、怎么改。

「布尔与空值之外的数值兜底解析」

在 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 会停在字母之后而非中途。

MQL5 / C++
    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 缺失或类型不对就回退默认值,外汇/贵金属参数读取用这套能少踩空指针坑,但杠杆与仓位误设仍属高风险操作。

MQL5 / C++
  {
      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,用这套模块解析时须留意网络延迟与报文截断带来的高风险,解析失败分支不要直接假设数据合法。

MQL5 / C++
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 里就能看到类的骨架与解析边界处理,省得自己从头判字符串越界。

MQL5 / C++
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 推送常含嵌套数组,用这些接口解析时须留意高风险——报文畸变会让类型判断失效。

MQL5 / C++
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 "";
  }
交给小布盯盘看盘口外的数据管道
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到外部 JSON 行情的结构化解析结果,你把重复劳动交给小布,专注决策。

常见问题

硬编码截取容易在嵌套对象或转义引号处错位,且无法动态修改后回写。本篇类结构用递归解析规避这类断裂,概率上更稳。
可以,小布盯盘的品种页已内置结构化数据视图,能读入本地 JSON 参数并高亮差异,不用你手动比对版本。
MQL5 严格类型下混用会触发隐式转换报错;分开分支可在词法分析阶段锁定类型标签,后续访问更安全。
极端点差下外部服务可能返回异常字段,解析器需对缺失键做默认兜底。外汇贵金属高风险,配置加载失败应倾向暂停而非裸奔交易。