JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器
概述
您好,欢迎!如果您曾经尝试以 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 在算法交易世界最常见的实际应用场景包括:
-
吸纳市场数据:许多现代经纪商 API 或金融数据服务提供 JSON 格式的实时或历史数据。拥有 JSON 解读器可令您快速解析这些数据,并将其集成进您的交易策略当中。
-
策略配置:假设您有一款支持多个参数的智能系统:最大点差、期望账户风险水平、或允许交易的时间。JSON 文件能够整洁地存储这些设置,MQL5 中的 JSON 解读器则能动态加载、或更新这些参数,而无需重新编译代码。
-
发送日志或数据:在某些设置中,您或许想把交易日志或调试消息传输到一台外部服务器进行分析。以 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 章(您来了!)— 概述
我们刚刚讨论了什么是 JSON,为什么它重要,以及如何编写 MQL5 版本的自定义解析器。这为后续一切奠定了基础。 -
第 2 章 — 基础知识:JSON 和 MQL5 基础
我们将复习 JSON 的关键结构元素,然后将其映射到 MQL5 数据类型,并展示哪些方面需要我们特别留意。 -
第 3 章 – 为我们的解析器扩展高级功能
于此,我们将谈及潜在的扩展或改进:如何处理数组,如何添加错误检查,以及若您需要发送数据,如何将 MQL5 数据转换回 JSON。 -
第 4 章 – 完整代码
最后,我们将将整个函数库汇编至一处,给您一份单一的参考文件。 -
第 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 数据类型:
- 字符串:JSON 字符串出现在双引号之内,譬如 “Hello World”。在 MQL5 中,我们也有字符串类型,但这些字符串可以包含特殊字符、转义序列、和 Unicode。故此,我们首先要面对的细节是确保解析器正确处理引号、转义符号(如 \“),以及潜在的 Unicode 码(例如,\u00A9)。
- 数字:在 JSON 中,数字可以是整数(如 42)、或小数(3.14159)。MQL5 主要以整数(int)、或双精度(浮点数值)存储数字。然而,并非所有 JSON 中的数值都能干净利落地映射到 int。例如,1234567890 是有效的,但在某些情境下,如果该数值真的很大,您可能需要 MQL5 的长整数型(long)型。当 JSON 数值超出典型 32-位整数范围时,我们需要特别注意。另外,如果一个大整数超出标准整数极限,您或许需要将其转换成双精度,但这会带来四舍五入问题。
- 布尔值:JSON 使用小写的 true 和 false 。与此同时,MQL5 使用 bool 。这是一个直截映射,但在解析时我们必须仔细检测这些标记(true 和 false)。有一小处要注意:任何语法错误 — 比如大写的 True 或 FALSE — 都不是有效的 JSON,尽管它们在其它语的解析器当中允许。如果您的数据有时使用大写布尔值,您需要优雅地处理,或者确保数据严格符合 JSON。
- NULL:JSON 中的空值往往表示字段为空或缺失。MQL5 没有专门的“空类型(null)”。代之,我们可以选择将 JSON 的 null 表示为特殊的内部枚举(如 jtNULL,如果我们为 JSON 元素类型定义枚举),或者将其视为空字符串或、默认值。我们很快会看到解析器如何管理空。
- 对象:当您看到花括弧时,{ ... } ,那是一个 JSON 对象。它本质上是一个键-值对集合。在 MQL5 中,没有内置字典类型,但我们可以通过一个存储键-值对的动态数组、或构建一个持有键和值的自定义类来模拟。典型情况下我们会定义一些东西,像是 CMyJsonObject 类(或带有内部“对象”状态的通用类),驻留子项列表。每个子项有一个键(字符串),和一个任意 JSON 数据类型的值。
- 数组:JSON 中的数组是顺序列表,由方括号包围,[ ... ] 。数组中的每一项可以是字符串、数字、对象,甚至另一个数组。在 MQL5 中,我们用 ArrayResize 函数和直接索引来处理数组。我们很可能会把 JSON 数组存储为动态元素数组。我们的代码需要持续跟踪的事实,即特定节点是数组,以及它内部的子项。
我们来看一些潜在的挑战:
- 处理转义序列:在 JSON 中,反斜杠 \ 能够出现在字符前面,如引号或换行。举例,您或许会看到 "description": "Line one\\nLine two"。我们需要在最后的一串字符串中,将 \\n 解释为一个实际换行。特殊序列包括:
- \" 双引号
- \\ 反斜线
- \/ 正斜杠
- \n 新行
- \t 制表符
- \u Unicode 码
我们必须按部就班地将原生 JSON 字符串中的这些序列转换为 MQL5 中它们代表的实际字符。否则,解析器或许会错误地存储它们,或在使用这些标准转义范式的输入数据时失败。
- 裁剪空白和控制字符:一个有效的 JSON 字符串可以包含空格、制表符、和换行(尤其在元素之间)。虽然在大多数地方都允许这些,且无语义差异,但如果我们不小心,它们会令解析逻辑变得复杂。健壮的解析器通常会忽略由引号包夹字符串之外的任何空格。这意味着我们会在从一个词元到下一个词元时跳过它们。
- 处置大数据:如果您的 JSON 字符串极端庞大,您或许会担心 MQL5 中的内存约束。这门语言能够相当不错地处理数组,但如果您的元素接近数千万,会有上限。大多数交易者罕有需要那么大的 JSON,但值得注意的是,若有需求,您或许需要以“流”、或迭代的方式来做到。对于大多数正常用法 — 比如读取设置、或中等规模的数据集 — 我们直接了当的方式应该够好。
-
并非所有 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 的相关性有了坚实的领悟。我们知道需要一个灵活的类,能够如下行事:
- 存储节点类型 — 无论是数字、字符串、对象、数组、布尔、或者空。
- 解析 — 逐字符阅读原生文本,解释花括弧、方括弧、引号、和特殊词元。
- 访问 — 提供便捷的方法,按主键(对象)或索引(数组)获取或设置子节点。
- 转换 — 将数值或布尔节点转换为 MQL5 的原语,如 double、int 或 bool。
- 转义/逆转义 — 将 JSON 编码序列字符串转换为普通的 MQL5 字符串(反之,如果我们未来添加 “至 JSON” 方法)。
- 错误检查 — 可能检测到错误的输入或未知词元,然后优雅地处理。
我们将在下一章分步探讨这些特性,真正的编程之旅也将由此开始。如果您担心性能或内存占用,放心吧,直接了当的方式通常足够快速、且内存高效,适合正常使用。如果您遇到性能瓶颈、或内存约束,您始终能够剖析代码、或采用部分解析技术。
在第 3 章,我们将开始详细地构建解析器。我们将定义凌驾一切的类 — 类似 CJsonNode — 并从最简单的任务开始:存储节点的类型和数值,加上编写一个“分词器”方法来识别 JSON 词元(如花括弧、或引号)。一旦基础打好,我们会向上打造支持对象、数组、嵌套元素、和数据提取。
无论您计划解析小型 JSON 配置文件,亦或从网络上提取宽泛的数据,这些基础都适用。即便您是读取 MQL5 外部数据的新手,也不必担忧:一旦您逐步明白逻辑,一切都变得相当可控。
现在深呼吸;我们即将沉浸到我们的代码之中。下一章,我们将亲手分步构建自定义 JSON 解析器,并分享确保数据可靠处理的实用技巧。我们令 MQL5 急不可待地“说” JSON!
解析器的核心类:我们的解析器类目标是表示任何 JSON 数据(有时称为树中的“节点”)。此处是我们或许需要的草图:
- 节点类型的枚举:我们希望能够轻松区分 JSON 对象、数组、字符串、等等。我们像这样定义:
enum JsonNodeType { JSON_UNDEF = 0, JSON_OBJ, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL };
- 成员变量:
每个 CJsonNode 存储 - JsonNodeType m_typeto 标识节点类型。
- 对于对象:一个结构(如数组),持有键-值对。
- 对于数组:一个持有已索引子节点的结构。
- 对于字符串:字符串 m_value。
- 对于数字:一个双精度 m_numVal,可能需要额外的长整型 m_intValif。
- 对于布尔值:一个布尔值 m_boolVal。
- 解析与实用方法:
- 解析原生 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)。我们的真实实现能够更具防御性,如果字符出现异常,我们会处理错误。
我们来考察如何解析不同的案例:
- 解析对象: 当我们看到一个花括弧({)时,我们创建一个对象节点。然后我们反复寻找键-值对,直至遇到闭合花括弧( })。以下是 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 对象的核心。该结构在数组中重复,但数组没有主键 — 只有已索引的元素。
-
解析数组:数组以 [ 开始。其内,我们会发现零或多个由逗号分隔的元素。大致是:
[ "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; }
我们跳过 [ 和任何空格。如果我们看到 ],其为空。否则,我们会在循环中解析元素直至我们遇到 ]。与对象的关键区别在于我们不会解析键-值对 — 只按顺序解析数值。
-
解析一个数值, 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); }
我们于此:
- 跳过空格。
- 查看当前字符(或子字符串),看看它是不是 {、[、“、等等。
- 调用相关的解析函数。
- 如果我们发现 “true”、“false”、或 “null”,就直接处理它们。
- 如果没有任何匹配,我们就假设它是一个数字。
根据您的需求,您或许会添加更好的错误处理。举例,如果子字符串与识别出的范式不匹配,您能够设置一个错误。
-
解析一个数字,我们需要解析看起来像数字的数字,比如 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 文本。通过将这些强化功能叠加在我们所构建解析器之上,您将获得一个更健壮、更灵活的工具 — 单枪匹马就能满足各种现世需求。 -
按主键或索引提取子项
如果我们的解析器要真正实用,我们打算轻松吸纳对象中某个主键的数值,或数组中特定索引处的数值。比方说我们得到这个 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 非常重要。
-
优雅地处理错误
在许多现世场景中,JSON 或许会遇到错误格式(例如,缺少引号、尾随逗号、或异常符号)。一个健壮的解析器应当能检测、并报告这些错误。您能够以按此行事:
-
返回一个布尔值:我们的大多数解析方法都已返回布尔值。如果有什么失败了,我们返回 false。但我们也能存储一个内部错误信息,像是 m_errorMsg,如此调用代码就能看到哪里出了问题。
-
继续解析还是中止?:一旦检测到致命的解析错误 — 比方说意外字符、或未闭合的括号 — 您或许会决定中止整个解析,保持节点处于“无效”状态。备案,您也能尝试跳过或恢复,但那会更先进。
此处有个概念上的调整:在 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 输入。
-
-
将 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 数据很庞大,比方说数万行,您可能需要考虑性能:
-
高效的字符串操作
请留意,重复性子字符串操作(StringSubstr)成本昂贵。MQL5 效率相当高,但若您的数据真的庞大,您或许考虑基于板块的解析、或迭代方式。 -
流式分析与 DOM 解析
我们的策略是“如 DOM” 的方式,意即我们把整个输入解析至一个树状结构。如果数据大到无法舒适地存储在内存当中,您需要一个流式解析器,一次处理一小片。这会更复杂,但对于极端庞大的数据集合来说是必要的。 -
高速缓存
如果您频繁查询同一对象以获取相同的主键,您或许会将它们存储在小映射之中,或者保留直接指针,以便加快重复性查找。针对典型的交易任务,这样的需求罕有,但如果性能至关重要,这也是一个选项。
-
-
最佳实践
以下是一些最佳实践,保障您的代码安全和可维护性:
-
务必检查 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在MQL5中创建交易管理面板(第九部分):代码组织(5):分析面板(AnalyticsPanel)类
将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(四) —— 测试交易策略
MQL5自动化交易策略(第十四部分):基于MACD-RSI统计方法的交易分层策略
,等我拿到手,我会做一个比较基准。
我比较了 4 个库的速度,包括 MQL5Book 的 ToyJson3。作为 json 样本,我使用了 Binance 对 "exchangeInfo "的响应,大小为 768 Kb。当库从字符串中读取时,会对其进行整体解析,然后我选择一个符号并读取其所有数据。如此循环 100 次。
结果(查询处理时间):
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:我没有发布用于测量的脚本。代码的形式完全不雅观。
我比较了 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 字符串或文件。
https://fapi.binance.com/fapi/v1/exchangeInfo
https://eapi.binance.com/eapi/v1/exchangeInfo
778 KB(796 729 字节)。
能否请您提供 json 字符串或文件?