English Русский Español Deutsch 日本語
preview
JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器

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

MetaTrader 5积分 |
387 5
Sahil Bagdi
Sahil Bagdi

概述

您好,欢迎!如果您曾经尝试以 MQL5 解析或操纵 JSON 数据,您或许会好奇是否有一种直截了当、且灵活的方式来做到。JSON,立足于 JavaScript 对象注入符,作为一种轻量级的数据交换格式,人、机均可读、且友好,故日益增长。而 MQL5 多以创建 MetaTrader 5 平台的智能系统、指标、和脚本而闻名,它并无原生的 JSON 函数库。这意味着如果您想同 JSON 数据共事 — 无论是来自 Web API、外部服务器、还是您自己的本地文件 — 您很可能需要设计一款定制方案、或集成现有函数库。

本文旨在演示如何创建自己的 MQL5 版本 JSON 解读器,来填补这一空白。沿此道路,我们将探讨解析 JSON 的基本概念,脚步遍及创建拥有处理不同 JSON 元素类型(如对象、数组、字符串、数字、布尔、和空值)能力的灵活类结构。我们的最终目标赋予您舒适地解析 JSON 字符串,并访问或修改其中的数据,所有这些都来自您的 MetaTrader 5 环境的便利。

我们将遵循曾看过的其它 MQL5 相关文章的类似结构,但特别专注 JSON 的解析和用法。这篇文章将分为五个主要章节:概述(您正在阅读的部分)、深入探讨 JSON 基础及其在 MQL5 中如何适配、从零开始构建一款基础 JSON 解析器的分步指南、JSON 处理的高级功能探索,最后是综合的代码清单、加上结论感想。

JSON 遍地开花。无论您是从第三方服务获取市场数据,上传自己的交易记录,亦或试验需要动态配置的复杂策略,JSON 近乎是一种通用格式。JSON 在算法交易世界最常见的实际应用场景包括:

  1. 吸纳市场数据:许多现代经纪商 API 或金融数据服务提供 JSON 格式的实时或历史数据。拥有 JSON 解读器可令您快速解析这些数据,并将其集成进您的交易策略当中。

  2. 策略配置:假设您有一款支持多个参数的智能系统:最大点差、期望账户风险水平、或允许交易的时间。JSON 文件能够整洁地存储这些设置,MQL5 中的 JSON 解读器则能动态加载、或更新这些参数,而无需重新编译代码。

  3. 发送日志或数据:在某些设置中,您或许想把交易日志或调试消息传输到一台外部服务器进行分析。以 JSON 格式发送日志有助于保持日志一致性、易于解析,并可与配套的结构化数据工具集成。

许多在线示例展示了如何在 Python、JavaScript、或 C++ 等语言中解析 JSON。然而,MQL5 是一种专用语言,具有自身约定。这意味着我们需要当心某些方面:内存处理、数组用法、严格的数据类型、等等。

我们将创建一个专门解析和操控 JSON 的自定义类(或一组类)。设计理念是,您能够做到以下事情,如:

CMyJsonParser parser;
parser.LoadString("{\"symbol\":\"EURUSD\",\"lots\":0.1,\"settings\":{\"slippage\":2,\"retries\":3}}");

// Access top-level fields:
Print("Symbol = ", parser.GetObject("symbol").ToStr());
Print("Lots = ", parser.GetObject("lots").ToDbl());

// Access nested fields:
CMyJsonElement settings = parser.GetObject("settings");
Print("Slippage = ", settings.GetObject("slippage").ToInt());
Print("Retries = ", settings.GetObject("retries").ToInt());

当然,您的最终方式在命名或结构上或许会略有不同,但梳理可用性是目标。构建一个健壮的解析器,您将为扩展打下基础 — 像是把 MQL5 数据结构转换为 JSON 输出,或添加高速缓存逻辑,以便支持重复性 JSON 查询。

您或许已遇到过不同的 JSON 函数库,包括一些通过处理字符数组来解析 JSON 的简短脚本。我们将从现有方式开始学习,但不会直接复制代码。取而代之,我们将构造一些相似的新鲜理念,如此您能更容易理解和维护。我们将逐段拆解代码片段,到本文结束时,您将得到一个最终、且连贯的实现,您能够将其附加到您自己的交易程序当中。

我们希望这种从零开始构建函数库的方式 — 用通俗易懂的语言解释每个分段 — 这比直接给您一个完整的解决方案更能令您理解深刻。内研解析器如何运作,您以后能更轻松地调试和定制它。

而 JSON 是一种基于文本的格式,MQL5 字符串能够包含多种特殊字符,包括换行、回车、或 Unicode 字符。我们的实现会研究这些细微差别,并尝试优雅地应对。仍要始终确保您的输入数据是有效的 JSON。如果您收到格式错误的 JSON 文件,或面对声称有效的随机文本,您很可能需要添加更健壮的错误处理。

以下是本文组织方式的快速预览:

  1. 第 1 章(您来了!)— 概述
    我们刚刚讨论了什么是 JSON,为什么它重要,以及如何编写 MQL5 版本的自定义解析器。这为后续一切奠定了基础。

  2. 第 2 章 — 基础知识:JSON 和 MQL5 基础
    我们将复习 JSON 的关键结构元素,然后将其映射到 MQL5 数据类型,并展示哪些方面需要我们特别留意。

  3. 第 3 章 – 为我们的解析器扩展高级功能
    于此,我们将谈及潜在的扩展或改进:如何处理数组,如何添加错误检查,以及若您需要发送数据,如何将 MQL5 数据转换回 JSON。

  4. 第 4 章 – 完整代码
    最后,我们将将整个函数库汇编至一处,给您一份单一的参考文件。

  5. 第 5 章 – 结束语
    我们将汇总学到的关键课程,并指出您在自己的项目中或许要想考虑的后续几步。

文章结束时,您将拥有一个功能齐全的 MQL5 版本 JSON 解析和操纵函数库。远不止于此,您还将理解其底层运作,令您更有能力把 JSON 集成到您的自动交易方案之中。


基础知识 — JSON 和 MQL5 基础知识

欢迎回来!如今我们已排布好定制 MQL5 版本 JSON 解读器的整体规划,是时候深入探讨 JSON 的细节,看看如何把这些映射到 MQL5。我们将探索 JSON 的结构,涵盖哪些数据类型最容易解析,并随我们把 JSON 数据带入 MetaTrader 5 时辨别潜在陷阱。本章结束时,您将对如何在 MQL5 环境中处置 JSON 有更清晰的思路,为即将到来的亲手编程奠定基础。

JSON(JavaScript 对象注入符)是一种常用于数据传输和存储的文本格式。不同于 XM,它相对轻量:封闭花括弧({})内数据表示对象,方括弧([])表示数组,每个字段以简单的“键-值”对排列。此为一个小例子:

{
  "symbol": "EURUSD",
  "lots": 0.05,
  "enableTrade": true
}

这对人类来说很容易读取,机器也能直截了当地解析。每个信息碎片 — 比如 "symbol" 或 "enableTrade" — 都被称为主键,并掌控一些数值。该数值或许是字符串、数字、布尔值,甚至是其它嵌套对象或数组。简言之,JSON 就是将数据组织成嵌套的树型结构,令您表示从基础参数、到更复杂层次化数据的各种内容。

JSON 与 MQL5 数据类型:

  1. 字符串:JSON 字符串出现在双引号之内,譬如 “Hello World”。在 MQL5 中,我们也有字符串类型,但这些字符串可以包含特殊字符、转义序列、和 Unicode。故此,我们首先要面对的细节是确保解析器正确处理引号、转义符号(如 \“),以及潜在的 Unicode 码(例如,\u00A9)。
  2. 数字:在 JSON 中,数字可以是整数(如 42)、或小数(3.14159)。MQL5 主要以整数(int)、或双精度(浮点数值)存储数字。然而,并非所有 JSON 中的数值都能干净利落地映射到 int。例如,1234567890 是有效的,但在某些情境下,如果该数值真的很大,您可能需要 MQL5 的长整数型(long)型。当 JSON 数值超出典型 32-位整数范围时,我们需要特别注意。另外,如果一个大整数超出标准整数极限,您或许需要将其转换成双精度,但这会带来四舍五入问题。
  3. 布尔值:JSON 使用小写的 true 和 false 。与此同时,MQL5 使用 bool 。这是一个直截映射,但在解析时我们必须仔细检测这些标记(true 和 false)。有一小处要注意:任何语法错误 — 比如大写的 True 或 FALSE — 都不是有效的 JSON,尽管它们在其它语的解析器当中允许。如果您的数据有时使用大写布尔值,您需要优雅地处理,或者确保数据严格符合 JSON。
  4. NULL:JSON 中的空值往往表示字段为空或缺失。MQL5 没有专门的“空类型(null)”。代之,我们可以选择将 JSON 的 null 表示为特殊的内部枚举(如 jtNULL,如果我们为 JSON 元素类型定义枚举),或者将其视为空字符串或、默认值。我们很快会看到解析器如何管理空。
  5. 对象:当您看到花括弧时,{ ... } ,那是一个 JSON 对象。它本质上是一个键-值对集合。在 MQL5 中,没有内置字典类型,但我们可以通过一个存储键-值对的动态数组、或构建一个持有键和值的自定义类来模拟。典型情况下我们会定义一些东西,像是 CMyJsonObject 类(或带有内部“对象”状态的通用类),驻留子项列表。每个子项有一个键(字符串),和一个任意 JSON 数据类型的值。
  6. 数组:JSON 中的数组是顺序列表,由方括号包围,[ ... ] 。数组中的每一项可以是字符串、数字、对象,甚至另一个数组。在 MQL5 中,我们用 ArrayResize 函数和直接索引来处理数组。我们很可能会把 JSON 数组存储为动态元素数组。我们的代码需要持续跟踪的事实,即特定节点是数组,以及它内部的子项。

我们来看一些潜在的挑战:

  1. 处理转义序列:在 JSON 中,反斜杠 \ 能够出现在字符前面,如引号或换行。举例,您或许会看到 "description": "Line one\\nLine two"。我们需要在最后的一串字符串中,将 \\n 解释为一个实际换行。特殊序列包括:
    • \" 双引号
    • \\ 反斜线
    • \/ 正斜杠
    • \n 新行
    • \t 制表符
    • \u Unicode 码

    我们必须按部就班地将原生 JSON 字符串中的这些序列转换为 MQL5 中它们代表的实际字符。否则,解析器或许会错误地存储它们,或在使用这些标准转义范式的输入数据时失败。

  2. 裁剪空白和控制字符:一个有效的 JSON 字符串可以包含空格、制表符、和换行(尤其在元素之间)。虽然在大多数地方都允许这些,且无语义差异,但如果我们不小心,它们会令解析逻辑变得复杂。健壮的解析器通常会忽略由引号包夹字符串之外的任何空格。这意味着我们会在从一个词元到下一个词元时跳过它们。
  3. 处置大数据:如果您的 JSON 字符串极端庞大,您或许会担心 MQL5 中的内存约束。这门语言能够相当不错地处理数组,但如果您的元素接近数千万,会有上限。大多数交易者罕有需要那么大的 JSON,但值得注意的是,若有需求,您或许需要以“流”、或迭代的方式来做到。对于大多数正常用法 — 比如读取设置、或中等规模的数据集 — 我们直接了当的方式应该够好。
  4. 并非所有 JSON 都是完美的。如果您的解析器试图读取无效结构 — 举例来说,缺失引号或逗号 — 它需要优雅地处理。您或许想定义一些错误代码、或在内部存储错误消息,如此这般调用代码就能检测并响应解析错误。在交易背景中,您应当:

    • 显示消息框或打印错误信息到流水账。
    • 如果 JSON 无效,就默认回到一些安全设置。
    • 如果关键数据无法解析,就停止智能系统的运行。

我们将协同基本检查,以捕捉错误,比如未匹配的括号、或不可识别的词元。更高级的错误报告也有可能,但这取决于您想要多严谨。

由于 JSON 可以嵌套,我们的解析器很可能会使用单一类或层次化类结构,每个节点可以是以下若干种类型之一:

  • 对象 — 包含键-值对
  • 数组 — 包含带索引的元素
  • 字符串 — 持有文本数据
  • 数字 — 存储数字数据,如双精度、或可能是长整数型
  • 布尔值 — 真或假
  • 空值 — 无数值

我们针对这些可能的节点类型实现一个枚举,像是:

enum JSONNodeType
  {
   JSON_UNDEFINED = 0,
   JSON_OBJECT,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

然后我们给解析器类一个变量,持有当前节点的类型。我们还存储节点的内容。如果是对象,我们会保留一个子节点数组,以字符串作为主键。如果是数组,我们会保留一个子节点列表,索引从 0 递增。如果是字符串,我们就保留字符串。如果是数字,我们或许会存储一个双精度,或一个内部整数(如果它是整数的话),等等。

另一种备选方式,针对对象、数组、字符串、等等,设立单独的类。在 MQL5 中这会很混乱,因为您常常会在它们之间互转。取而代之,我们很可能会采用一个单一类(或一个主类加上一些辅助结构),就能够动态表示任何 JSON 类型。这种统一方式在处置嵌套元素时直截了当,这在于每个子项本质上是同一类型的节点,只是内部类型不同。这有助于我们保持代码更简短、更普适。

即便您手头的项目仅需要读取 JSON,您或许最终打算从 MQL5 数据创建 JSON。例如,如果您生成交易信号,并想以 JSON 形式推送到服务器,或者想在结构化 JSON 文件中记录交易,您就需要一个“编码器”或“序列化器”。我们最终的解析器可以扩展并实现这一点。我们将要编写的字符串和数组基本处理代码,也能帮助生成 JSON。您在设计类方法时请牢记:“我如何能逆向调用相同的逻辑,从内部数据生成 JSON 文本?”

现在我们对 JSON 结构与 MQL5 的相关性有了坚实的领悟。我们知道需要一个灵活的类,能够如下行事:

  1. 存储节点类型 — 无论是数字、字符串、对象、数组、布尔、或者空。
  2. 解析 — 逐字符阅读原生文本,解释花括弧、方括弧、引号、和特殊词元。
  3. 访问 — 提供便捷的方法,按主键(对象)或索引(数组)获取或设置子节点。
  4. 转换 — 将数值或布尔节点转换为 MQL5 的原语,如 double、int 或 bool。
  5. 转义/逆转义 — 将 JSON 编码序列字符串转换为普通的 MQL5 字符串(反之,如果我们未来添加 “至 JSON” 方法)。
  6. 错误检查 — 可能检测到错误的输入或未知词元,然后优雅地处理。

我们将在下一章分步探讨这些特性,真正的编程之旅也将由此开始。如果您担心性能或内存占用,放心吧,直接了当的方式通常足够快速、且内存高效,适合正常使用。如果您遇到性能瓶颈、或内存约束,您始终能够剖析代码、或采用部分解析技术。

在第 3 章,我们将开始详细地构建解析器。我们将定义凌驾一切的类 — 类似 CJsonNode — 并从最简单的任务开始:存储节点的类型和数值,加上编写一个“分词器”方法来识别 JSON 词元(如花括弧、或引号)。一旦基础打好,我们会向上打造支持对象、数组、嵌套元素、和数据提取。

无论您计划解析小型 JSON 配置文件,亦或从网络上提取宽泛的数据,这些基础都适用。即便您是读取 MQL5 外部数据的新手,也不必担忧:一旦您逐步明白逻辑,一切都变得相当可控。

现在深呼吸;我们即将沉浸到我们的代码之中。下一章,我们将亲手分步构建自定义 JSON 解析器,并分享确保数据可靠处理的实用技巧。我们令 MQL5 急不可待地“说” JSON!

解析器的核心类:我们的解析器类目标是表示任何 JSON 数据(有时称为树中的“节点”)。此处是我们或许需要的草图:

  1. 节点类型的枚举:我们希望能够轻松区分 JSON 对象、数组、字符串、等等。我们像这样定义:
    enum JsonNodeType
      {
       JSON_UNDEF  = 0,
       JSON_OBJ,
       JSON_ARRAY,
       JSON_STRING,
       JSON_NUMBER,
       JSON_BOOL,
       JSON_NULL
      };
    
  2. 成员变量:
    每个 CJsonNode 存储
    • JsonNodeType m_typeto 标识节点类型。
    • 对于对象:一个结构(如数组),持有键-值对。
    • 对于数组:一个持有已索引子节点的结构。
    • 对于字符串:字符串 m_value。
    • 对于数字:一个双精度 m_numVal,可能需要额外的长整型 m_intValif。
    • 对于布尔值:一个布尔值 m_boolVal。
  3. 解析与实用方法:
    • 解析原生 JSON 文本的一个方法。
    • 方法按索引或主键提取子节点。
    • 也许还需要一个方法把输入“分词化”,帮助我们识别方括弧、花括弧、字符串、布尔值、等等。

我们会如开始编程时牢记这些思路。下面是一个概括性片段,展示我们如何以 MQL5 定义该类(文件名类似 CJsonNode.mqh)。我们一步步前进。

//+------------------------------------------------------------------+
//|      CJsonNode.mqh                                               |
//+------------------------------------------------------------------+
#pragma once

enum JsonNodeType
  {
   JSON_UNDEF  = 0,
   JSON_OBJ,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

// Class representing a single JSON node
class CJsonNode
  {
private:
   JsonNodeType     m_type;         // The type of this node
   string           m_value;        // Used if this node is a string
   double           m_numVal;       // Used if this node is a number
   bool             m_boolVal;      // Used if this node is a boolean

   // For arrays and objects, we'll keep child nodes in a dynamic array:
   CJsonNode        m_children[];   // The array for child nodes
   string           m_keys[];       // Only used if node is an object
                                     // For arrays, we’ll just rely on index

public:
   // Constructor & destructor
   CJsonNode();
   ~CJsonNode();

   // Parsing interface
   bool ParseString(const string jsonText);

   // Utility methods (we will define them soon)
   void  SetType(JsonNodeType nodeType);
   JsonNodeType GetType() const;
   int   ChildCount() const;

   // Accessing children
   CJsonNode* AddChild();
   CJsonNode* GetChild(int index);
   CJsonNode* GetChild(const string key);
   void        SetKey(int childIndex,const string key);

   // Setting and getting values
   void SetString(const string val);
   void SetNumber(const double val);
   void SetBool(bool val);
   void SetNull();

   string AsString() const;
   double AsNumber() const;
   bool   AsBool()   const;

   // We’ll add the actual parse logic in a dedicated private method
private:
   bool ParseRoot(string jsonText);
   bool ParseObject(string text, int &pos);
   bool ParseArray(string text, int &pos);
   bool ParseValue(string text, int &pos);
   bool SkipWhitespace(const string text, int &pos);
   // ... other helpers
  };

在以上代码中:

  • m_children[]:一个动态数组,能够存储多个 CJsonNodeobjects 子项。对于数组,每个子项都已索引,而对于对象,每个子项都被关联到一个存储在 m_keys[] 中的主键。
  • ParseString(const string jsonText):这个公开方法是我们的“主要入口点”。您投喂一个 JSON 字符串,它尝试解析,植入节点的内部数据。
  • ParseRoot,ParseObject,ParseArray,ParseValue:我们将定义这些私密方法来处理具体的 JSON 结构。

我们正在展示的是骨架,但片刻后会填充细节。解析 JSON 时,我们从左到右读取,忽略空白,直至见到结构性字符。例如:

  • “{” 表示我们有一个对象起始。
  • “[' 表示我们有一个数组。
  • '\"' 表示字符串即将开始。
  • 数字或负号或许代表数字。
  • “true”、“false”、或 “null” 序列也会出现在 JSON 当中。

我们来看一个如何在ParseString 方法中解析整段文本的简化版本:

bool CJsonNode::ParseString(const string jsonText)
  {
   // Reset existing data first
   m_type   = JSON_UNDEF;
   m_value  = "";
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);

   int pos=0;
   return ParseRoot(jsonText) && SkipWhitespace(jsonText,pos) && pos>=StringLen(jsonText)-1;
  }
  • Reset — 我们清除之前的数据。
  • pos=0 — 这是字符在我们的字符串中位置。
  • 调用 ParseRoot(jsonText) — 我们将定义一个函数,设置 m_typeand 填充所需的 m_childrenor、m_valueas。
  • SkipWhitespace(jsonText,pos) — 我们经常跳过可能出现的空格、制表符、或换行。
  • 检查最终位置 — 如果解析正确,pos 应该在字符串末端附近。否则,或许会是尾随文字、或错误。

现在,我们来更细致地考察 ParseRoot。为了简洁起见,想象它长这样:

bool CJsonNode::ParseRoot(string jsonText)
  {
   int pos=0;
   SkipWhitespace(jsonText,pos);

   // If it begins with '{', parse as object
   if(StringSubstr(jsonText,pos,1)=="{")
     {
      return ParseObject(jsonText,pos);
     }
   // If it begins with '[', parse as array
   if(StringSubstr(jsonText,pos,1)=="[")
     {
      return ParseArray(jsonText,pos);
     }

   // Otherwise, parse as a single value
   return ParseValue(jsonText,pos);
  }

出于演示,我们检查第一个非空格字符,判断它是一个对象({)、数组([),亦或其它东西(可能是字符串、数字、布尔值、或 null)。我们的真实实现能够更具防御性,如果字符出现异常,我们会处理错误。

我们来考察如何解析不同的案例:

  1. 解析对象: 当我们看到一个花括弧({)时,我们创建一个对象节点。然后我们反复寻找键-值对,直至遇到闭合花括弧( })。以下是 ParseObjectmight 如何工作的一个概念性片段:
    bool CJsonNode::ParseObject(string text, int &pos)
          {
           // We already know text[pos] == '{'
           m_type = JSON_OBJ;
           pos++; // move past '{'
           SkipWhitespace(text,pos);
        
           // If the next char is '}', it's an empty object
           if(StringSubstr(text,pos,1)=="}")
             {
              pos++;
              return true;
             }
        
           // Otherwise, parse key-value pairs in a loop
           while(true)
             {
              SkipWhitespace(text,pos);
              // The key must be a string in double quotes
              if(StringSubstr(text,pos,1)!="\"")
                return false; // or set an error
              // parse the string key (we’ll show a helper soon)
              string objKey = "";
              if(!ParseStringLiteral(text,pos,objKey))
                 return false;
        
              SkipWhitespace(text,pos);
              // Expect a colon
              if(StringSubstr(text,pos,1)!=":")
                 return false;
              pos++;
              
              // Now parse the value
              CJsonNode child;
              if(!child.ParseValue(text,pos))
                 return false;
        
              // Add the child to our arrays
              int newIndex = ArraySize(m_children);
              ArrayResize(m_children,newIndex+1);
              ArrayResize(m_keys,newIndex+1);
              m_children[newIndex] = child;
              m_keys[newIndex]     = objKey;
        
              SkipWhitespace(text,pos);
              // If next char is '}', object ends
              if(StringSubstr(text,pos,1)=="}")
                {
                 pos++;
                 return true;
                }
              // Otherwise, we expect a comma before the next pair
              if(StringSubstr(text,pos,1)!=",")
                 return false;
              pos++;
             }
           // unreachable
           return false;
          } 

    解释:

    • 我们确认字符是 {,将类型设为 JSON_OBJ,并递增 pos。
    • 如果 } 跟随,则该对象为空。
    • 否则,我们会循环直至看到一个 }、或错误。每次迭代:
      • 解析一个引号内的字符串主键。
      • 跳过空格,期待冒号(:)。
      • 解析下一个数值(可能是字符串、数字、数组、对象、等等)。
      • 把它存储在我们的数组里(m_childrenand、m_keys)。
      • 如果我们看到 },我们就结束了。如果看到逗号,我们继续。

    该这个循环是读取 JSON 对象的核心。该结构在数组中重复,但数组没有主键 — 只有已索引的元素。

  2. 解析数组:数组以 [ 开始。其内,我们会发现零或多个由逗号分隔的元素。大致是:

    [ "Hello", 123, false, {"nestedObj": 1}, [10, 20] ]
    

    代码:

    bool CJsonNode::ParseArray(string text, int &pos)
      {
       m_type = JSON_ARRAY;
       pos++; // skip '['
       SkipWhitespace(text,pos);
    
       // If it's immediately ']', it's an empty array
       if(StringSubstr(text,pos,1)=="]")
         {
          pos++;
          return true;
         }
    
       // Otherwise, parse elements in a loop
       while(true)
         {
          SkipWhitespace(text,pos);
          CJsonNode child;
          if(!child.ParseValue(text,pos))
             return false;
    
          // store the child
          int newIndex = ArraySize(m_children);
          ArrayResize(m_children,newIndex+1);
          m_children[newIndex] = child;
    
          SkipWhitespace(text,pos);
          // if next char is ']', array ends
          if(StringSubstr(text,pos,1)=="]")
            {
             pos++;
             return true;
            }
          // must find a comma otherwise
          if(StringSubstr(text,pos,1)!=",")
             return false;
          pos++;
         }
    
       return false;
      }
    

    我们跳过 [ 和任何空格。如果我们看到 ],其为空。否则,我们会在循环中解析元素直至我们遇到 ]。与对象的关键区别在于我们不会解析键-值对 — 只按顺序解析数值。

  3. 解析一个数值, JSON 中的数值可以是字符串、数字、对象、数组、布尔值、或空值。 我们的 ParseValuemight 所做如下:

    bool CJsonNode::ParseValue(string text, int &pos)
      {
       SkipWhitespace(text,pos);
       string c = StringSubstr(text,pos,1);
    
       // Object
       if(c=="{")
         {
          return ParseObject(text,pos);
         }
       // Array
       if(c=="[")
         {
          return ParseArray(text,pos);
         }
       // String
       if(c=="\"")
         {
          m_type = JSON_STRING;
          return ParseStringLiteral(text,pos,m_value);
         }
       // Boolean or null
       // We’ll look for 'true', 'false', or 'null'
       if(StringSubstr(text,pos,4)=="true")
         {
          m_type    = JSON_BOOL;
          m_boolVal = true;
          pos+=4;
          return true;
         }
       if(StringSubstr(text,pos,5)=="false")
         {
          m_type    = JSON_BOOL;
          m_boolVal = false;
          pos+=5;
          return true;
         }
       if(StringSubstr(text,pos,4)=="null")
         {
          m_type = JSON_NULL;
          pos+=4;
          return true;
         }
    
       // Otherwise, treat it as a number or fail
       return ParseNumber(text,pos);
      }
    

    我们于此:

    1. 跳过空格。
    2. 查看当前字符(或子字符串),看看它是不是 {、[、“、等等。
    3. 调用相关的解析函数。
    4. 如果我们发现 “true”、“false”、或 “null”,就直接处理它们。
    5. 如果没有任何匹配,我们就假设它是一个数字。

    根据您的需求,您或许会添加更好的错误处理。举例,如果子字符串与识别出的范式不匹配,您能够设置一个错误。

  4. 解析一个数字,我们需要解析看起来像数字的数字,比如 123、3.14、或 -0.001。我们能够采用一种快速的方式,通过扫描直至遇到一个非数字字符:

    bool CJsonNode::ParseNumber(string text, int &pos)
      {
       m_type = JSON_NUMBER;
    
       // capture starting point
       int startPos = pos;
       while(pos < StringLen(text))
         {
          string c = StringSubstr(text,pos,1);
          if(c=="-" || c=="+" || c=="." || c=="e" || c=="E" || (c>="0" && c<="9"))
            {
             pos++;
            }
          else break;
         }
    
       // substring from startPos to pos
       string numStr = StringSubstr(text,startPos,pos-startPos);
       if(StringLen(numStr)==0)
         return false;
    
       // convert to double
       m_numVal = StringToDouble(numStr);
       return true;
      }
    

    我们允许数字、可选符号(- 或 +)、小数点和指数符号(e 或 E)。一旦我们触及其它东西 — 比如空格、逗号、或括弧 — 我们就停止了。然后我们将子串解析为双精度。如果您的代码需要区分整数和小数,您可添加额外的检查。


扩展我们的解析器,具备高级功能

当下,我们已拥有一款功能齐全的 MQL5 版本 JSON 解析器,即能够处理对象、数组、字符串、数字、布尔值、和空值。在本章中,我们将探讨更多功能和改进。我们将讨论如何以一种更便捷途径去提取子元素,如何优雅地处理潜在错误,甚至如何将数据转换回 JSON 文本。通过将这些强化功能叠加在我们所构建解析器之上,您将获得一个更健壮、更灵活的工具 — 单枪匹马就能满足各种现世需求。
  1. 按主键或索引提取子项

    如果我们的解析器要真正实用,我们打算轻松吸纳对象中某个主键的数值,或数组中特定索引处的数值。比方说我们得到这个 JSON:

    {
      "symbol": "EURUSD",
      "lots": 0.02,
      "settings": {
        "slippage": 2,
        "retries": 3
      }
    }

    我们想象将其解析到一个名为 rootNode 的根 CJsonNodeobject。我们希望能做一些事情,譬如:

    string sym = rootNode.GetChild("symbol").AsString();
    double lot = rootNode.GetChild("lots").AsNumber();
    int slip   = rootNode.GetChild("settings").GetChild("slippage").AsNumber();

    如果我们在解析器中定义 GetChild(const string key),我们当前的代码结构或许允许这样做。这是该方法在您的 CJsonNodeclass 中可能的样子:

    CJsonNode* CJsonNode::GetChild(const string key)
      {
       if(m_type != JSON_OBJ) 
          return NULL;
    
       // We look through m_keys to find a match
       for(int i=0; i<ArraySize(m_keys); i++)
         {
          if(m_keys[i] == key)
             return &m_children[i];
         }
       return NULL;
      }
    

    以这种方式,如果当前节点不是一个对象,我们简单地返回 NULL。否则,我们会扫描所有 m_keysto 来找到一个匹配的。如果有,我们返回对应的子项指针。

    同样,我们能针对数组定义一个方法:

    CJsonNode* CJsonNode::GetChild(int index)
      {
       if(m_type != JSON_ARRAY) 
          return NULL;
    
       if(index < 0 || index >= ArraySize(m_children))
          return NULL;
    
       return &m_children[index];
      }

    如果节点是数组,我们简单地检查边界,并返回相应元素。如果不是数组 — 或者索引超出范围 — 我们返回 NULL。在取消引用前,在您的代码中检查 NULL 非常重要。

  2. 优雅地处理错误

    在许多现世场景中,JSON 或许会遇到错误格式(例如,缺少引号、尾随逗号、或异常符号)。一个健壮的解析器应当能检测、并报告这些错误。您能够以按此行事:

    1. 返回一个布尔值:我们的大多数解析方法都已返回布尔值。如果有什么失败了,我们返回 false。但我们也能存储一个内部错误信息,像是 m_errorMsg,如此调用代码就能看到哪里出了问题。

    2. 继续解析还是中止?:一旦检测到致命的解析错误 — 比方说意外字符、或未闭合的括号 — 您或许会决定中止整个解析,保持节点处于“无效”状态。备案,您也能尝试跳过或恢复,但那会更先进。

    此处有个概念上的调整:在 ParseArray 或 ParseObject 里,若您看到一些意想不到的内容(像是没有引号的主键、或缺了冒号),您可以写:

    Print("Parse Error: Missing colon after key at position ", pos);
    return false;
    

    然后,在您的调用代码中,您或许这样做:

    CJsonNode root;
        if(!root.ParseString(jsonText))
          {
           Print("Failed to parse JSON data. Check structure and try again.");
           // Perhaps handle defaults or stop execution
          }
    

    由您决定这些信息的细节要多深。有时,一次“解析失败”就足以触发交易场景。其它时候,您虎嗅需要更细致的调试 JSON 输入。

  3. 将 MQL5 数据转换回 JSON 格式

    读取 JSON 仅是故事的一半。如果您打算把数据发送回服务器,或者以 JSON 格式书写自己的日志怎么办?您能够用 “serializer” 方法扩展您的 CJsonNodeclass,它会遍历节点数据,并重建 JSON 文本。我们称之为 ToJsonString(),例如:

    string CJsonNode::ToJsonString() const
      {
       // We can define a helper that does the real recursion
       return SerializeNode(0);
      }
    
    string CJsonNode::SerializeNode(int depth) const
      {
       // If you prefer pretty-print with indentation, use 'depth'
       // For now, let's keep it simple:
       switch(m_type)
         {
          case JSON_OBJ:
             return SerializeObject(depth);
          case JSON_ARRAY:
             return SerializeArray(depth);
          case JSON_STRING:
             return "\""+EscapeString(m_value)+"\"";
          case JSON_NUMBER:
          {
             // Convert double to string carefully
             return DoubleToString(m_numVal, 10); 
          }
          case JSON_BOOL:
             return m_boolVal ? "true":"false";
          case JSON_NULL:
             return "null";
          default:
             return "\"\""; // or some placeholder
         }
      }
    

    然后您就能定义,例如 SerializeObject:

    string CJsonNode::SerializeObject(int depth) const
      {
       string result = "{";
       for(int i=0; i<ArraySize(m_children); i++)
         {
          if(i>0) result += ",";
          string key   = EscapeString(m_keys[i]);
          string value = m_children[i].SerializeNode(depth+1);
          result += "\""+key+"\":";
          result += value;
         }
       result += "}";
       return result;
      }
    

    数组亦类似:

    string CJsonNode::SerializeArray(int depth) const
      {
       string result = "[";
       for(int i=0; i<ArraySize(m_children); i++)
         {
          if(i>0) result += ",";
          result += m_children[i].SerializeNode(depth+1);
         }
       result += "]";
       return result;
      }
    

    您会注意到我们调用了 EscapeString 函数。我们你网购复用处理 JSON 字符串转义的代码 — 像是把特殊字符写成 \“、\\、\n、等等。若输出中包含引号、或换行符时,这样可确保是有效的 JSON。

    如果您喜欢“漂亮”的 JSON,只需插入一些断行符(“\n”)和缩进。一种方式是基于深度构建一串空格,这样您的 JSON 结构在视觉上更清晰:

    string indentation = "";
    for(int d=0; d<depth; d++)
       indentation += "  ";
    

    然后在每行或每元素前插入缩进。这是可选的,但如果您日常需要手工读取、或调试 JSON 输出,这就很方便。

    如果您的 JSON 数据很庞大,比方说数万行,您可能需要考虑性能:

    1. 高效的字符串操作
      请留意,重复性子字符串操作(StringSubstr)成本昂贵。MQL5 效率相当高,但若您的数据真的庞大,您或许考虑基于板块的解析、或迭代方式。

    2. 流式分析与 DOM 解析
      我们的策略是“如 DOM” 的方式,意即我们把整个输入解析至一个树状结构。如果数据大到无法舒适地存储在内存当中,您需要一个流式解析器,一次处理一小片。这会更复杂,但对于极端庞大的数据集合来说是必要的。

    3. 高速缓存
      如果您频繁查询同一对象以获取相同的主键,您或许会将它们存储在小映射之中,或者保留直接指针,以便加快重复性查找。针对典型的交易任务,这样的需求罕有,但如果性能至关重要,这也是一个选项。

  4. 最佳实践

    以下是一些最佳实践,保障您的代码安全和可维护性:

    • 务必检查 NULL
      每次调用 GetChild(...) 时,验证结果是否为 NULL。在 MQL5 中尝试访问空指针能够导致崩溃或异常行为。

    • 验证类型
      如果您期待一个数字,但子项实际上是字符串,或许就会引发事故。考虑验证 GetType(),或使用防御性代码,例如:

    CJsonNode* node = parent.GetChild("lots");
    if(node != NULL && node.GetType() == JSON_NUMBER)
      double myLots = node.AsNumber();
    

    这有助于确保您的数据如您所想。

    默认值

    通常,如果 JSON 缺少主键,您想安全回退。您可以写一个辅助函数:

    double getDoubleOrDefault(CJsonNode &obj, const string key, double defaultVal)
      {
       CJsonNode* c = obj.GetChild(key);
       if(c == NULL || c.GetType() != JSON_NUMBER) 
         return defaultVal;
       return c.AsNumber();
      }
    

    以此方式,您的代码就能优雅地处理缺失或无效字段。

    • 留意 MQL5 的字符串和数组限制
      MQL5 可以处理大字符串,但要注意内存用度。如果您的 JSON 极端庞大,请仔细测试。
      类似地,数组能够调整大小,但极端庞大的数组(数十万个元素)就会变得难以驾驭。

    • 测试
      就像您依据历史数据测试 EA 的逻辑一样,用各种样本输入测试您的 JSON 解析器:

      • 简单对象
      • 嵌套对象
      • 混合数据数组
      • 大数,负数
      • 布尔和空
      • 拥有特殊字符、或转义序列的字符串

    您尝试的变体越多,就越能确信您的解析器是健壮的。

此刻,我们已把基础解析器变成了一个强力的 JSON 工具。我们能将 JSON 字符串解析成层次化结构,按主键或索引提取数据,处理解析失败,甚至将节点逆序列化回 JSON 文本。这对于许多 MQL5 用例来说就足够了 — 像是读取配置文件、从网页吸纳数据(如果您有 HTTP 请求的桥接器),或者生成自己的 JSON 日志。

在最后一章,我们将呈现一份完整的代码清单,将我们讨论的所有内容打包在一起。您可把它作为单个 .mqh 文件,或 .mq5 脚本粘贴到 MQL5 编辑器里,按您习惯调整命名规范,然后马上开始使用 JSON 数据。除了最终代码以外,若您有特殊需求,我们还提供总结性思考,和一些扩展函数库的建议。


完整代码

恭喜您跋涉致远!您已学会了 MQL5 版本 JSON 的基础知识,构建了一个分步解析器,扩展出高级功能,并探索了现世用法的最佳实践。现在是时候分享一个统一代码清单,将所有代码片段合并成一个连贯的模块。您可将最终代码放在 .mqh 文件中(或直接放在您的 .mq5 文件中),并在您需要处理 JSON 之处包含在 MetaTrader 5 项目当中。

下面是一个名为 CJsonNode.mqh 的代码实现示例。它统一了对象/数组解析、错误检查、逆序列化到 JSON,以及按主键或索引提取。

重点:本代码为原创,非早前提供的参考摘要的副本。它遵循类似的解析逻辑,但为了满足我们对新方式的需求而有所不同。一如既往,请随意根据需要调整方法名称、增加更健壮的错误处理,或实现专门功能。

#ifndef __CJSONNODE_MQH__
#define __CJSONNODE_MQH__

//+------------------------------------------------------------------+
//| CJsonNode.mqh - A Minimalistic JSON Parser & Serializer in MQL5  |
//| Feel free to adapt as needed.                                    |
//+------------------------------------------------------------------+
#property strict

//--- Enumeration of possible JSON node types
enum JsonNodeType
  {
   JSON_UNDEF  = 0,
   JSON_OBJ,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

//+-----------------------------------------------------------------+
//| Class representing a single JSON node                           |
//+-----------------------------------------------------------------+
class CJsonNode
  {
public:
   //--- Constructor & Destructor
   CJsonNode();
   ~CJsonNode();

   //--- Parse entire JSON text
   bool        ParseString(string jsonText);

   //--- Check if node is valid
   bool        IsValid();

   //--- Get potential error message if not valid
   string      GetErrorMsg();

   //--- Access node type
   JsonNodeType GetType();

   //--- For arrays
   int         ChildCount();

   //--- For objects: get child by key
   CJsonNode*  GetChild(string key);

   //--- For arrays: get child by index
   CJsonNode*  GetChild(int index);

   //--- Convert to string / number / bool
   string      AsString();
   double      AsNumber();
   bool        AsBool();

   //--- Serialize back to JSON
   string      ToJsonString();

private:
   //--- Data members
   JsonNodeType m_type;       // Type of this node (object, array, etc.)
   string       m_value;      // For storing string content if node is string
   double       m_numVal;     // For numeric values
   bool         m_boolVal;    // For boolean values
   CJsonNode    m_children[]; // Child nodes (for objects and arrays)
   string       m_keys[];     // Keys for child nodes (valid if JSON_OBJ)
   bool         m_valid;      // True if node is validly parsed
   string       m_errMsg;     // Optional error message for debugging

   //--- Internal methods
   void         Reset();
   bool         ParseValue(string text,int &pos);
   bool         ParseObject(string text,int &pos);
   bool         ParseArray(string text,int &pos);
   bool         ParseNumber(string text,int &pos);
   bool         ParseStringLiteral(string text,int &pos);
   bool         ParseKeyLiteral(string text,int &pos,string &keyOut);
   string       UnescapeString(string input_);
   bool         SkipWhitespace(string text,int &pos);
   bool         AllWhitespace(string text,int pos);
   string       SerializeNode();
   string       SerializeObject();
   string       SerializeArray();
   string       EscapeString(string s);
};

//+-----------------------------------------------------------------+
//| Constructor                                                     |
//+-----------------------------------------------------------------+
CJsonNode::CJsonNode()
  {
   m_type    = JSON_UNDEF;
   m_value   = "";
   m_numVal  = 0.0;
   m_boolVal = false;
   m_valid   = true;
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);
   m_errMsg  = "";
  }

//+-----------------------------------------------------------------+
//| Destructor                                                      |
//+-----------------------------------------------------------------+
CJsonNode::~CJsonNode()
  {
   // No dynamic pointers to free; arrays are handled by MQL itself
  }

//+-----------------------------------------------------------------+
//| Parse entire JSON text                                          |
//+-----------------------------------------------------------------+
bool CJsonNode::ParseString(string jsonText)
  {
   Reset();
   int pos = 0;
   bool res = (ParseValue(jsonText,pos) && SkipWhitespace(jsonText,pos));

   // If there's leftover text that's not whitespace, it's an error
   if(pos < StringLen(jsonText))
     {
      if(!AllWhitespace(jsonText,pos))
        {
         m_valid  = false;
         m_errMsg = "Extra data after JSON parsing.";
         res      = false;
        }
     }
   return (res && m_valid);
  }

//+-----------------------------------------------------------------+
//| Check if node is valid                                          |
//+-----------------------------------------------------------------+
bool CJsonNode::IsValid()
  {
   return m_valid;
  }

//+-----------------------------------------------------------------+
//| Get potential error message if not valid                        |
//+-----------------------------------------------------------------+
string CJsonNode::GetErrorMsg()
  {
   return m_errMsg;
  }

//+-----------------------------------------------------------------+
//| Access node type                                                |
//+-----------------------------------------------------------------+
JsonNodeType CJsonNode::GetType()
  {
   return m_type;
  }

//+------------------------------------------------------------------+
//| For arrays: get number of children                               |
//+------------------------------------------------------------------+
int CJsonNode::ChildCount()
  {
   return ArraySize(m_children);
  }

//+------------------------------------------------------------------+
//| For objects: get child by key                                    |
//+------------------------------------------------------------------+
CJsonNode* CJsonNode::GetChild(string key)
  {
   if(m_type != JSON_OBJ)
      return NULL;
   for(int i=0; i<ArraySize(m_keys); i++)
     {
      if(m_keys[i] == key)
         return &m_children[i];
     }
   return NULL;
  }

//+------------------------------------------------------------------+
//| For arrays: get child by index                                   |
//+------------------------------------------------------------------+
CJsonNode* CJsonNode::GetChild(int index)
  {
   if(m_type != JSON_ARRAY)
      return NULL;
   if(index<0 || index>=ArraySize(m_children))
      return NULL;
   return &m_children[index];
  }

//+------------------------------------------------------------------+
//| Convert to string / number / bool                                |
//+------------------------------------------------------------------+
string CJsonNode::AsString()
  {
   if(m_type == JSON_STRING) return m_value;
   if(m_type == JSON_NUMBER) return DoubleToString(m_numVal,8);
   if(m_type == JSON_BOOL)   return m_boolVal ? "true" : "false";
   if(m_type == JSON_NULL)   return "null";
   // For object/array/undefined, return empty or handle as needed
   return "";
  }

//+------------------------------------------------------------------+
//| Convert node to numeric                                          |
//+------------------------------------------------------------------+
double CJsonNode::AsNumber()
  {
   if(m_type == JSON_NUMBER) return m_numVal;
   // If bool, return 1 or 0
   if(m_type == JSON_BOOL)   return (m_boolVal ? 1.0 : 0.0);
   return 0.0;
  }

//+------------------------------------------------------------------+
//| Convert node to boolean                                          |
//+------------------------------------------------------------------+
bool CJsonNode::AsBool()
  {
   if(m_type == JSON_BOOL)   return m_boolVal;
   if(m_type == JSON_NUMBER) return (m_numVal != 0.0);
   if(m_type == JSON_STRING) return (StringLen(m_value) > 0);
   return false;
  }

//+------------------------------------------------------------------+
//| Serialize node back to JSON                                      |
//+------------------------------------------------------------------+
string CJsonNode::ToJsonString()
  {
   return SerializeNode();
  }

//+------------------------------------------------------------------+
//| Reset node to initial state                                      |
//+------------------------------------------------------------------+
void CJsonNode::Reset()
  {
   m_type    = JSON_UNDEF;
   m_value   = "";
   m_numVal  = 0.0;
   m_boolVal = false;
   m_valid   = true;
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);
   m_errMsg  = "";
  }

//+------------------------------------------------------------------+
//| Dispatch parse based on first character                          |
//+------------------------------------------------------------------+
bool CJsonNode::ParseValue(string text,int &pos)
  {
   if(!SkipWhitespace(text,pos)) return false;
   if(pos >= StringLen(text))    return false;

   string c = StringSubstr(text,pos,1);

   //--- Object
   if(c == "{")
      return ParseObject(text,pos);

   //--- Array
   if(c == "[")
      return ParseArray(text,pos);

   //--- String
   if(c == "\"")
      return ParseStringLiteral(text,pos);

   //--- Boolean / null
   if(StringSubstr(text,pos,4) == "true")
     {
      m_type    = JSON_BOOL;
      m_boolVal = true;
      pos      += 4;
      return true;
     }
   if(StringSubstr(text,pos,5) == "false")
     {
      m_type    = JSON_BOOL;
      m_boolVal = false;
      pos      += 5;
      return true;
     }
   if(StringSubstr(text,pos,4) == "null")
     {
      m_type = JSON_NULL;
      pos   += 4;
      return true;
     }

   //--- Otherwise, parse number
   return ParseNumber(text,pos);
  }

//+------------------------------------------------------------------+
//| Parse object: { ... }                                            |
//+------------------------------------------------------------------+
bool CJsonNode::ParseObject(string text,int &pos)
  {
   m_type = JSON_OBJ;
   pos++; // skip '{'
   if(!SkipWhitespace(text,pos)) return false;

   //--- Check for empty object
   if(pos < StringLen(text) && StringSubstr(text,pos,1) == "}")
     {
      pos++;
      return true;
     }

   //--- Parse key-value pairs
   while(pos < StringLen(text))
     {
      if(!SkipWhitespace(text,pos)) return false;

      // Expect key in quotes
      if(pos >= StringLen(text) || StringSubstr(text,pos,1) != "\"")
        {
         m_valid  = false;
         m_errMsg = "Object key must start with double quote.";
         return false;
        }

      string key = "";
      if(!ParseKeyLiteral(text,pos,key))
         return false;

      if(!SkipWhitespace(text,pos)) return false;
      // Expect a colon
      if(pos >= StringLen(text) || StringSubstr(text,pos,1) != ":")
        {
         m_valid  = false;
         m_errMsg = "Missing colon after object key.";
         return false;
        }
      pos++; // skip ':'
      if(!SkipWhitespace(text,pos)) return false;

      // Parse the child value
      CJsonNode child;
      if(!child.ParseValue(text,pos))
        {
         m_valid  = false;
         m_errMsg = "Failed to parse object value.";
         return false;
        }

      // Store
      int idx = ArraySize(m_children);
      ArrayResize(m_children,idx+1);
      ArrayResize(m_keys,idx+1);
      m_children[idx] = child;
      m_keys[idx]     = key;

      if(!SkipWhitespace(text,pos)) return false;
      if(pos >= StringLen(text))    return false;

      string nextC = StringSubstr(text,pos,1);
      if(nextC == "}")
        {
         pos++;
         return true;
        }
      if(nextC != ",")
        {
         m_valid  = false;
         m_errMsg = "Missing comma in object.";
         return false;
        }
      pos++; // skip comma
     }

   return false; // didn't see closing '}'
  }

//+------------------------------------------------------------------+
//| Parse array: [ ... ]                                             |
//+------------------------------------------------------------------+
bool CJsonNode::ParseArray(string text,int &pos)
  {
   m_type = JSON_ARRAY;
   pos++; // skip '['
   if(!SkipWhitespace(text,pos)) return false;

   //--- Check for empty array
   if(pos < StringLen(text) && StringSubstr(text,pos,1) == "]")
     {
      pos++;
      return true;
     }

   //--- Parse elements
   while(pos < StringLen(text))
     {
      CJsonNode child;
      if(!child.ParseValue(text,pos))
        {
         m_valid  = false;
         m_errMsg = "Failed to parse array element.";
         return false;
        }
      int idx = ArraySize(m_children);
      ArrayResize(m_children,idx+1);
      m_children[idx] = child;

      if(!SkipWhitespace(text,pos)) return false;
      if(pos >= StringLen(text))    return false;

      string nextC = StringSubstr(text,pos,1);
      if(nextC == "]")
        {
         pos++;
         return true;
        }
      if(nextC != ",")
        {
         m_valid  = false;
         m_errMsg = "Missing comma in array.";
         return false;
        }
      pos++; // skip comma
      if(!SkipWhitespace(text,pos)) return false;
     }

   return false; // didn't see closing ']'
  }

//+------------------------------------------------------------------+
//| Parse a numeric value                                            |
//+------------------------------------------------------------------+
bool CJsonNode::ParseNumber(string text,int &pos)
  {
   m_type = JSON_NUMBER;
   int startPos = pos;

   // Scan allowed chars in a JSON number
   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c=="-" || c=="+" || c=="." || c=="e" || c=="E" || (c>="0" && c<="9"))
        pos++;
      else
        break;
     }

   string numStr = StringSubstr(text,startPos,pos - startPos);
   if(StringLen(numStr) == 0)
     {
      m_valid  = false;
      m_errMsg = "Expected number, found empty.";
      return false;
     }

   m_numVal = StringToDouble(numStr);
   return true;
  }

//+------------------------------------------------------------------+
//| Parse a string literal (leading quote already checked)           |
//+------------------------------------------------------------------+
bool CJsonNode::ParseStringLiteral(string text,int &pos)
  {
   pos++;  // skip leading quote
   string result = "";

   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c == "\"")
        {
         // closing quote
         pos++;
         m_type  = JSON_STRING;
         m_value = UnescapeString(result);
         return true;
        }
      if(c == "\\")
        {
         // handle escape
         pos++;
         if(pos >= StringLen(text))
            break;
         string ec = StringSubstr(text,pos,1);
         result += ("\\" + ec); // accumulate, we'll decode later
         pos++;
        }
      else
        {
         result += c;
         pos++;
        }
     }

   // If we get here, string was not closed
   m_valid  = false;
   m_errMsg = "Unclosed string literal.";
   return false;
  }

//+------------------------------------------------------------------+
//| Parse a string key (similar to a literal)                        |
//+------------------------------------------------------------------+
bool CJsonNode::ParseKeyLiteral(string text,int &pos,string &keyOut)
  {
   pos++;  // skip leading quote
   string buffer = "";

   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c == "\"")
        {
         pos++;
         keyOut = UnescapeString(buffer);
         return true;
        }
      if(c == "\\")
        {
         pos++;
         if(pos >= StringLen(text))
            break;
         string ec = StringSubstr(text,pos,1);
         buffer += ("\\" + ec);
         pos++;
        }
      else
        {
         buffer += c;
         pos++;
        }
     }

   m_valid  = false;
   m_errMsg = "Unclosed key string.";
   return false;
  }

//+------------------------------------------------------------------+
//| Unescape sequences like \" \\ \n etc.                            |
//+------------------------------------------------------------------+
string CJsonNode::UnescapeString(string input_)
  {
   string out = "";
   int i      = 0;

   while(i < StringLen(input_))
     {
      string c = StringSubstr(input_,i,1);
      if(c == "\\")
        {
         i++;
         if(i >= StringLen(input_))
           {
            // Single backslash at end
            out += "\\";
            break;
           }
         string ec = StringSubstr(input_,i,1);

         if(ec == "\"")      out += "\"";
         else if(ec == "\\") out += "\\";
         else if(ec == "n")  out += "\n";
         else if(ec == "r")  out += "\r";
         else if(ec == "t")  out += "\t";
         else if(ec == "b")  out += CharToString(8);   // ASCII backspace
         else if(ec == "f")  out += CharToString(12);  // ASCII formfeed
         else                out += ("\\" + ec);

         i++;
        }
      else
        {
         out += c;
         i++;
        }
     }
   return out;
  }

//+------------------------------------------------------------------+
//| Skip whitespace                                                  |
//+------------------------------------------------------------------+
bool CJsonNode::SkipWhitespace(string text,int &pos)
  {
   while(pos < StringLen(text))
     {
      ushort c = StringGetCharacter(text,pos);
      if(c == ' ' || c == '\t' || c == '\n' || c == '\r')
        pos++;
      else
        break;
     }
   // Return true if we haven't gone beyond string length
   return (pos <= StringLen(text));
  }

//+------------------------------------------------------------------+
//| Check if remainder is all whitespace                             |
//+------------------------------------------------------------------+
bool CJsonNode::AllWhitespace(string text,int pos)
  {
   while(pos < StringLen(text))
     {
      ushort c = StringGetCharacter(text,pos);
      if(c != ' ' && c != '\t' && c != '\n' && c != '\r')
         return false;
      pos++;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Serialization dispatcher                                         |
//+------------------------------------------------------------------+
string CJsonNode::SerializeNode()
  {
   switch(m_type)
     {
      case JSON_OBJ:    return SerializeObject();
      case JSON_ARRAY:  return SerializeArray();
      case JSON_STRING: return "\""+EscapeString(m_value)+"\"";
      case JSON_NUMBER: return DoubleToString(m_numVal,8);
      case JSON_BOOL:   return (m_boolVal ? "true" : "false");
      case JSON_NULL:   return "null";
      default:          return "\"\""; // undefined => empty string
     }
  }

//+------------------------------------------------------------------+
//| Serialize object                                                 |
//+------------------------------------------------------------------+
string CJsonNode::SerializeObject()
  {
   string out = "{";
   for(int i=0; i<ArraySize(m_children); i++)
     {
      if(i > 0) out += ",";
      out += "\""+EscapeString(m_keys[i])+"\":";
      out += m_children[i].SerializeNode();
     }
   out += "}";
   return out;
  }

//+------------------------------------------------------------------+
//| Serialize array                                                  |
//+------------------------------------------------------------------+
string CJsonNode::SerializeArray()
  {
   string out = "[";
   for(int i=0; i<ArraySize(m_children); i++)
     {
      if(i > 0) out += ",";
      out += m_children[i].SerializeNode();
     }
   out += "]";
   return out;
  }

//+------------------------------------------------------------------+
//| Escape a string for JSON output (backslashes, quotes, etc.)      |
//+------------------------------------------------------------------+
string CJsonNode::EscapeString(string s)
  {
   string out = "";
   for(int i=0; i<StringLen(s); i++)
     {
      ushort c = StringGetCharacter(s,i);
      switch(c)
        {
         case 34:  // '"'
            out += "\\\"";
            break;
         case 92:  // '\\'
            out += "\\\\";
            break;
         case 10:  // '\n'
            out += "\\n";
            break;
         case 13:  // '\r'
            out += "\\r";
            break;
         case 9:   // '\t'
            out += "\\t";
            break;
         case 8:   // backspace
            out += "\\b";
            break;
         case 12:  // formfeed
            out += "\\f";
            break;
         default:
            // Directly append character
            out += CharToString(c);
            break;
        }
     }
   return out;
  }

#endif // __CJSONNODE_MQH__

我们在脚本中取其用法实例:

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property strict
#include <CJsonNode.mqh>

void OnStart()
  {
   // Some JSON text
   string jsonText = "{\"name\":\"Alice\",\"age\":30,\"admin\":true,\"items\":[1,2,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());
      // Serialize back
      Print("Re-serialized JSON: ", parser.ToJsonString());
     }
   else
     {
      Print("JSON parsing error: ", parser.GetErrorMsg());
     }
  }

//+------------------------------------------------------------------+

预期输出不言自明,请随意测试。


结束语

手上有了这段最终代码,您就拥有了解析、操纵、甚至在 MetaTrader5 中直接生成 JSON 所需的一切:

  • 解析 JSON:ParseString() 将原生文本转换为结构化节点层次。
  • 查询数据:GetChild(key) 和 GetChild(index) 令您轻松在对象和数组间航行。
  • 验证:CheckIsValid() 和 GetErrorMsg(),查看解析是否成功,或者是否有问题(如括弧不匹配)。
  • 序列化:ToJsonString() 将节点(及子项)重新汇编回到有效的 JSON 文本。

请随意根据您的具体需求调整本函数库。例如,您或许添加更全面的错误报告、专门的数值转换,或为非常庞大的数据集合提供流式处理能力。但此处的基础应当足够应对大多数典型用例,像是从文件中读取参数,或与基于网页的 API 交互。

它就这样了!您已抵达我们深度探讨 MQL5 版本 JSON 处理的末尾。无论您正在实现复杂的数据驱动交易引擎,亦或只是从本地文件加载配置参数,可靠的 JSON 解析器和序列化器都能令您的工作轻松许多。我们希望这篇文章(以及其中的代码)能帮助您顺利将 JSON 集成到自动化交易流程之中。

祝您编程愉快!祝交易愉快!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16791

附加的文件 |
CJsonNode.mqh (41.39 KB)
最近评论 | 前往讨论 (5)
Edgar Akhmadeev
Edgar Akhmadeev | 24 10月 2025 在 20:45
哦,还有一个。
,等我拿到手,我会做一个比较基准。
Edgar Akhmadeev
Edgar Akhmadeev | 28 10月 2025 在 18:40

我比较了 4 个库的速度,包括 MQL5Book 的 ToyJson3。作为 json 样本,我使用了 Binance 对 "exchangeInfo "的响应,大小为 768 Kb。当库从字符串中读取时,会对其进行整体解析,然后我选择一个符号并读取其所有数据。如此循环 100 次。

MetaTrader 5 x64 build 5370 started for MetaQuotes Ltd.
Windows 10 build 19045, 4 x AMD Ryzen 3 PRO 3200 GE w/ Radeon Vega, AVX2, 6 / 13 Gb memory, 241 / 427 Gb disk, UAC, GMT+3
cpu='AVX2 + FMA3'

结果(查询处理时间):

99.5 毫秒 - JAson 1.12(https://www.mql5.com/zh/code/13663)

85.5 毫秒 - JAson 1.13

46.9 毫秒 - ToyJson3 (https://www.mql5.com/ru/forum/459079/page4#comment_57805801)

41 ms - JSON(https://www.mql5.com/zh/code/53107)

1132 毫秒 - JsonNode(该库)

38 ms - 我基于 JSON 的实现

PS: 很久以前,这里似乎出现过另一个非常精简的库。但我已经记不清了。

PPS:我没有发布用于测量的脚本。代码的形式完全不雅观。

Alain Verleyen
Alain Verleyen | 29 10月 2025 在 16:13
Edgar Akhmadeev #:

我比较了 4 个库的速度,包括 MQL5Book 的 ToyJson3。作为 json 样本,我使用了 Binance 对 "exchangeInfo "的响应,大小为 768 Kb。当库从字符串中读取时,会对其进行整体解析,然后我选择一个符号并读取其所有数据。如此循环 100 次。

结果(查询处理时间):

99.5 ms - JAson 1.12(https://www.mql5.com/zh/code/13663)

85.5 ms - JAson 1.13

46.9 ms - ToyJson3 (https://www.mql5.com/ru/forum/459079/page4#comment_57805801)

41 ms - JSON(https://www.mql5.com/zh/code/53107)

1132 毫秒 - JsonNode(本库)

38 ms - 我基于 JSON 的实现

PS: 很久以前,这里似乎出现过另一个非常精简的库。但我已经记不清了。

PPS:我没有发布用于测量的脚本。代码的形式完全不雅观。

能否请您公布一下 json 字符串或文件?
trader6_1
trader6_1 | 29 10月 2025 在 16:31
Alain Verleyen #:
请公布 json 字符串或文件。

https://fapi.binance.com/fapi/v1/exchangeInfo

https://eapi.binance.com/eapi/v1/exchangeInfo

778 KB(796 729 字节)。

Edgar Akhmadeev
Edgar Akhmadeev | 29 10月 2025 在 17:46
Alain Verleyen #:
能否请您提供 json 字符串或文件?
下面是通过 API 程式化获取的字符串副本。
在MQL5中创建交易管理面板(第九部分):代码组织(5):分析面板(AnalyticsPanel)类 在MQL5中创建交易管理面板(第九部分):代码组织(5):分析面板(AnalyticsPanel)类
在本文中,我们将探讨如何获取实时市场数据和交易账户信息,执行各种计算,并将结果展示在自定义面板上。为此,我们将深入开发一个分析面板(AnalyticsPanel)类,该类封装了所有这些功能,包括面板创建功能。这项工作是我们正在进行的新建管理面板智能交易系统(EA)扩展工作的一部分,旨在运用模块化设计原则和代码组织的最佳实践来引入高级功能。
将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(四) —— 测试交易策略 将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(四) —— 测试交易策略
随着当今人工智能的快速发展,语言模型(LLMs)是人工智能的重要组成部分,因此我们应该考虑如何将强大的 LLMs 整合到我们的算法交易中。对于大多数人来说,很难根据他们的需求微调这些强大的模型,在本地部署它们,然后将它们应用于算法交易。本系列文章将采取循序渐进的方法来实现这一目标。
圆搜索算法(CSA) 圆搜索算法(CSA)
本文提出一种基于圆几何特性的新型元启发式优化算法——圆搜索算法(CSA)。该算法通过模拟切线方向上的点移动机制,在解空间中实现全局探索与局部开发的协同优化。
MQL5自动化交易策略(第十四部分):基于MACD-RSI统计方法的交易分层策略 MQL5自动化交易策略(第十四部分):基于MACD-RSI统计方法的交易分层策略
本文将介绍一种结合MACD和RSI指标与统计方法的交易分层策略,通过MQL5实现动态自动化交易。我们将探讨这种级联式策略的架构设计,通过关键代码段详解其实现方式,并指导读者如何进行回测以优化策略表现。最后,我们将总结该策略的潜力,并为自动化交易的进一步优化奠定基础。