Осваиваем JSON: Разработка пользовательского JSON-ридера с нуля на MQL5
Введение
Если вы когда-либо пытались анализировать или обрабатывать данные JSON в MQL5, вы, возможно, задавались вопросом, существует ли простой и гибкий подход для такой работы. JSON (JavaScript Object Notation) приобрел популярность как облегченный формат обмена данными, который удобен как для человеческого восприятия, так и для машинной обработки. MQL5 в основном известен возможностями создания советников, индикаторов и скриптов для платформы MetaTrader 5, у него нет собственной библиотеки JSON. Это означает, что если вы хотите работать с данными JSON — будь то из веб-API, внешнего сервера или из ваших собственных локальных файлов — вам, скорее всего, потребуется разработать индивидуальное решение или интегрировать существующую библиотеку.
В этой статье мы постараемся восполнить этот пробел, показав, как создать собственный JSON-ридер на языке MQL5. По ходу дела мы изучим основные концепции анализа JSON и рассмотрим создание гибкой структуры классов, способной обрабатывать различные типы элементов JSON (такие как объекты, массивы, строки, числа, логические значения и значения NULL). Наша конечная цель — предоставить возможность удобно анализировать строки JSON и получать доступ к данным в них или изменять их, и все это в удобной среде MetaTrader 5.
Мы будем следовать структуре, похожей на ту, что мы видели в других статьях, связанных с MQL5, но с упором именно на анализ и использование JSON. Статья будет разделена на пять основных разделов: введение (то, что вы читаете сейчас), более глубокое погружение в основы JSON и его применение в MQL5, пошаговое руководство по созданию базового парсера JSON с нуля, исследование расширенных функций обработки JSON и, наконец, полный листинг кода и заключительные мысли.
JSON повсюду. Независимо от того, извлекаете ли вы рыночные данные из стороннего сервиса, загружаете собственные торговые записи или экспериментируете со сложными стратегиями, требующими динамической конфигурации, JSON остается практически универсальным форматом. Некоторые из наиболее распространенных практических вариантов использования JSON в мире алгоритмической торговли включают в себя:
-
Извлечение рыночных данных: многие современные API-интерфейсы брокеров или службы финансовых данных предлагают данные в режиме реального времени или исторические данные в формате JSON. Наличие в вашем распоряжении JSON-ридера позволяет вам быстро анализировать эти данные и интегрировать их в вашу торговую стратегию.
-
Настройка стратегии: предположим, у вас есть советник, который поддерживает несколько параметров: максимальный спред, желаемый уровень риска счета или разрешенное время торговли. Файл JSON может аккуратно хранить эти настройки, а средство чтения JSON в MQL5 может динамически загружать или обновлять эти параметры без перекомпиляции кода.
-
Отправка журналов или данных: в некоторых настройках вам может потребоваться передавать журналы торговли или отладочные сообщения на внешний сервер для аналитики. Отправка логов в формате JSON поможет сохранить их единообразие, простоту анализа и интеграцию с инструментами, которым требуются структурированные данные.
Множество онлайн-примеров показывают, как анализировать JSON на таких языках, как Python, JavaScript или C++. Однако 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 – Заключение
Мы обобщим основные извлеченные уроки и укажем несколько дальнейших шагов, которые вы, возможно, захотите учесть в своих собственных проектах.
К концу статьи у вас будет полнофункциональная библиотека для анализа и обработки JSON на языке MQL5. Кроме того, вы поймете, как все это работает изнутри, что позволит вам лучше интегрировать JSON в ваши автоматизированные торговые решения.
Основы: JSON и MQL5
Теперь, когда мы составили общий план для нашего пользовательского JSON-ридера, пришло время углубиться в более тонкие моменты JSON и посмотреть, как они соотносятся с MQL5. Мы рассмотрим структуру JSON, выясним, какие типы данных проще всего анализировать, и выявим потенциальные подводные камни при переносе данных JSON в MetaTrader 5. К концу этого раздела у вас будет гораздо более четкое представление о том, как работать с JSON в среде MQL5, что подготовит почву для дальнейшей разработки.
JSON (JavaScript Object Notation) — текстовый формат, обычно используемый для передачи и хранения данных. В отличие от XML, он относительно легкий: данные заключены в фигурные скобки ( {} ) для объектов или в квадратные скобки ( [] ) для массивов, а каждое поле представлено в виде простых пар "ключ-значение". Вот небольшой пример:
{
"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 (для целых чисел) или double (для значений с плавающей точкой). Однако не все числовые значения в JSON будут однозначно соответствовать типу int. Например, число 1234567890 является допустимым, но в некоторых случаях вам может понадобиться long в MQL5, если число действительно большое. Нам необходимо уделять особое внимание случаям, когда число JSON выходит за пределы типичного 32-битного целого числа. Кроме того, нам может потребоваться преобразовать большое целое число в число double, если оно превышает предел стандартного целого числа, но это может повлечь за собой проблемы округления.
- Булевы значения: JSON использует true и false (строчные буквы). Между тем MQL5 использует bool. Это простое сопоставление, но нам придется тщательно обнаруживать эти токены (true и false) во время анализа. Есть небольшая загвоздка: любые синтаксические ошибки, такие как True или FALSE в верхнем регистре, недопустимы в JSON, хотя некоторые парсеры в других языках их допускают. Если в ваших данных иногда используются заглавные логические значения, необходимо добиться корректной обработки или убедиться, что ваши данные строго соответствуют формату JSON.
- NULL: Нулевое значение в JSON часто указывает на пустое или отсутствующее поле. В MQL5 нет специального "типа null". Вместо этого мы можем представить значение null в JSON как специальное внутреннее перечисление (например, jtNULL, если мы определяем перечисление для типов элементов JSON) или рассматривать его как пустую строку или значение по умолчанию. Скоро мы увидим, как управлять нулями в парсере.
- Объекты: Когда вы видите фигурные скобки, { ... } , это объект JSON. По сути это набор пар "ключ-значение". В MQL5 нет встроенного типа словаря, но мы можем смоделировать его, сохранив динамический массив пар или создав пользовательский класс для хранения ключей и значений. Обычно мы определяем что-то вроде класса CMyJsonObject (или класса общего назначения с внутренним "объектным" состоянием), который содержит список дочерних элементов. Каждый дочерний элемент имеет ключ ( string ) и значение, которое может быть любым типом данных JSON.
- Массивы: Массивы в JSON представляют собой упорядоченные списки, заключенные в квадратные скобки, [ ... ] . Каждый элемент массива может быть строкой, числом, объектом или даже другим массивом. В MQL5 мы обрабатываем массивы с помощью функции ArrayResize и прямой индексации. Скорее всего, мы будем хранить массив JSON как динамический массив элементов. Наш код должен будет отслеживать тот факт, что конкретный узел является массивом, вместе со всеми его дочерними элементами.
Давайте рассмотрим некоторые потенциальные проблемы:
- Обработка экранированных последовательностей: в JSON обратная косая черта \ может предшествовать символам, таким как кавычки или переносы строк. Например, вы можете встретить такое "описание": "Line one\\nLine two". Нам необходимо интерпретировать \\n как фактический символ новой строки в конечной строке. Специальные последовательности включают в себя:
- \" для двойных кавычек
- \\ для обратной косой черты
- \/ иногда вместо косой черты
- \n для новой строки
- \t для табуляции
- \u для кодовых точек Unicode
Нам придется методично преобразовывать эти последовательности в необработанной строке JSON в фактические символы, которые они представляют в MQL5. В противном случае парсер может сохранить их неправильно или дать сбой при обработке входных данных, использующих эти стандартные шаблоны экранирования.
- Удаление пробелов и управляющих символов: допустимая строка JSON может включать пробелы, табуляции и символы переноса строк (особенно между элементами). Хотя они разрешены и не имеют семантического значения в большинстве случаев, они могут усложнить логику синтаксического анализа, если мы не будем осторожны. Надежный парсер обычно игнорирует любые пробелы за пределами строк в кавычках. Это значит, что нам нужно будет пропускать их при переходе от одного токена к другому.
- Работа с большими объемами данных: если строка JSON очень большая, возможны проблемы с ограничением памяти в MQL5. Язык может довольно хорошо обрабатывать массивы, но существуют верхние пределы, если количество элементов приближается к десяткам миллионов. Большинству трейдеров редко нужен JSON такого размера, но стоит отметить, что в этом случае может потребоваться "потоковый" или итеративный подход. Для большинства обычных случаев использования — например, для чтения настроек или наборов данных среднего размера — наш простой подход должен быть вполне приемлемым.
-
JSON не идеален. Если ваш анализатор сталкивается с недопустимой структурой (например, пропущенной кавычкой или завершающей запятой), он должен обработать ее корректно. Возможно, вам понадобится определить коды ошибок или сохранить сообщение об ошибке внутри, чтобы вызывающий код мог обнаружить ошибки анализа и отреагировать на них. В контексте трейдинга можно:
- Вывести окно сообщения или распечатать сообщение об ошибке в журнале.
- Если JSON недействителен, возвратиться к некоторым безопасным настройкам по умолчанию.
- Остановить работу советника, если критически важные данные не могут быть проанализированы.
Мы включим базовые проверки для выявления ошибок, таких как непарные скобки или нераспознанные токены. Также возможны более расширенные отчеты об ошибках, но их включение зависит от того, насколько жесткими являются ваши требования.
Поскольку JSON может быть вложенным, наш анализатор, скорее всего, будет использовать один класс или иерархию классов, где каждый узел может быть одним из нескольких типов:
- Объект – содержит пары "ключ-значение"
- Массив – содержит индексированные элементы
- Строка – содержит текстовые данные
- Число – хранит числовые данные в формате double или long
- Булевое значение – истина или ложь
- Ноль – нет значения
Мы можем реализовать перечисление для этих возможных типов узлов, например:
enum JSONNodeType { JSON_UNDEFINED = 0, JSON_OBJECT, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL };
Затем мы назначаем нашему классу парсера переменную, которая содержит тип текущего узла. Мы также сохраняем содержимое узла. Если это объект, мы сохраняем массив дочерних узлов, ключом которых является строка. Если это массив, мы сохраняем список дочерних узлов, индексированных от 0 и выше. Если это строка, мы сохраняем строку. Если это число, мы можем хранить его как double, а также внутреннее целое число, если это целое число, и т. д.
Альтернативный подход — иметь отдельный класс для объектов, массивов, строк и т. д. В MQL5 это может привести к путанице, поскольку вам придется часто переключаться между ними. Вместо этого мы, скорее всего, примем один класс (или один основной класс и несколько вспомогательных структур), который сможет динамически представлять любой тип JSON. Этот унифицированный подход прост при работе с вложенными элементами, поскольку каждый дочерний элемент по сути представляет собой один и тот же тип узла с разным внутренним типом. Это помогает нам сделать код более общим и коротким.
Даже если ваш непосредственный проект требует только чтения JSON, со временем вы можете захотеть создать JSON из данных MQL5. Например, если вы генерируете торговые сигналы и хотите отправлять их на сервер в формате JSON или регистрировать свои сделки в структурированном JSON-файле, вам понадобится "кодер" (encoder) или "сериализатор" (serializer). Наш конечный парсер можно расширить для этой цели. Базовый код, который мы напишем для обработки строк и массивов, также может помочь в генерации 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 типа double, возможно, необходим дополнительный m_intValif типа long.
- Для булевых значений: 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".
Давайте рассмотрим упрощенную версию того, как мы можем проанализировать весь текст в нашем методе 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 – позиция символа в строке.
- Call ParseRoot(jsonText) – определяемая функция устанавливает m_type и заполняет m_children или m_value по мере необходимости.
- SkipWhitespace(jsonText,pos) – мы часто пропускаем любые пробелы, табуляции или символы переноса строк, которые могут появиться.
- Check final position (проверить конечное положение) – если все правильно проанализировано, 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); }
Для демонстрации мы проверяем первый "непробельный" символ и решаем, является ли он объектом ( {), массивом ( [) или чем-то еще (строкой, числом, логическим значением или нулем). Наша фактическая реализация может быть направлена на максимальную защиту, обрабатывая ошибки при появлении неожиданного символа.
Давайте посмотрим, как мы анализируем различные случаи:
- Анализ объекта: Когда мы видим открывающуюся фигурную скобку ( {), мы создаем узел объекта. Затем мы многократно ищем пары "ключ-значение", пока не встретим закрывающуюся фигурную скобку ( }). Вот концептуальный фрагмент того, как может работать ParseObject:
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_children и 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). Как только мы встречаем что-то еще — например, пробел, запятую или скобку — мы останавливаемся. Затем мы разбираем подстроку на тип double. Если в вашем коде необходимо отличать целые числа от десятичных, вы можете добавить дополнительные проверки.
Добавление нового функционала в парсер
К настоящему моменту у нас есть функциональный парсер JSON на MQL5, который может обрабатывать объекты, массивы, строки, числа, логические значения и значения NULL. В этом разделе мы рассмотрим дополнительные функции и улучшения. Мы рассмотрим, как извлекать дочерние элементы более удобным способом, как корректно обрабатывать потенциальные ошибки и даже как преобразовывать данные обратно в текст JSON. Внедрив эти усовершенствования в созданный нами анализатор, вы получите более надежный и гибкий инструмент, способный решать самые разные реальные задачи. -
Извлечение дочерних элементов по ключу или индексу
Если мы хотим, чтобы наш парсер был по-настоящему полезным, нам нужно легко извлекать значение заданного ключа в объекте или значение по определенному индексу в массиве. Например, предположим, что у нас есть такой JSON:
{ "symbol": "EURUSD", "lots": 0.02, "settings": { "slippage": 2, "retries": 3 } }Представим, что мы разобрали его в корневой объект CJsonNode с именем rootNode. Мы хотели бы сделать следующее:
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_keys, чтобы найти совпадающий. Если такой найден, возвращаем указатель на соответствующий дочерний элемент.
Аналогично мы можем определить метод для массивов:
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, чтобы вызывающий код мог увидеть, что пошло не так.
-
Продолжить анализ или прервать? Как только вы обнаружите критическую ошибку анализа (например, неожиданный символ или незакрытую фигурную скобку), вы можете прервать весь анализ и оставить свой узел в "неверном" состоянии. Также вы можете попробовать пропустить или восстановить, но это уже более продвинутый вариант.
Концептуальное изменение: внутри ParseArrayor 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 }Насколько подробно вы хотите детализировать эти сообщения, решать вам. Иногда для торгового сценария достаточно сообщения "parse failed" (ошибка анализа). В других случаях вам может потребоваться отразить больше тонкостей для отладки входных данных JSON.
-
-
Преобразование данных MQL5 обратно в JSON
Чтение JSON — это только половина дела. Что делать, если вам необходимо отправить данные обратно на сервер или записать собственные логи в формате JSON? Вы можете расширить свой класс CJsonNode с помощью метода "сериализатора", который просматривает данные узла и реконструирует текст 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 очень большой, тщательно протестируйте его.
Аналогично можно изменять размер массивов, но очень большие массивы (сотни тысяч элементов) могут стать громоздкими. -
Тестирование
Так же, как вы проверяете логику советника с помощью исторических данных, протестируйте свой анализатор JSON с помощью различных образцов входных данных:- Простые объекты
- Вложенные объекты
- Массивы смешанных данных
- Большие числа, отрицательные числа
- Булевое значение и ноль
- Строки со специальными символами или экранированными последовательностями
Чем больше вариантов вы попробуете, тем больше вы будете уверены в надежности вашего анализатора.
-
На этом этапе мы превратили наш базовый анализатор в мощную утилиту JSON. Мы можем преобразовывать строки JSON в иерархическую структуру, извлекать данные по ключу или индексу, обрабатывать ошибки анализа и даже сериализовать узлы обратно в текст JSON. Этого достаточно для многих случаев использования MQL5, таких как чтение файла конфигурации, извлечение данных из Интернета (если у вас есть мост для HTTP-запросов) или создание собственных JSON-логов.
В заключительном разделе мы представим полный листинг кода, объединяющий все, что мы обсудили. Вы сможете вставить его в редактор MQL5 как отдельный файл .mqh или .mq5script, адаптировать его к своим соглашениям об именовании и сразу же начать использовать данные JSON. Наряду с окончательным кодом я предложу заключительные идеи и некоторые рекомендации по дальнейшему расширению библиотеки, если у вас есть особые требования.
Полный код
Поздравляю! Мы проделали большой путь. Вы изучили основы JSON в MQL5, создали пошаговый парсер, расширили его функционал расширенными функциями и изучили передовой опыт использования в реальных условиях. Теперь пришло время поделиться единым интегрированным листингом кода, объединяющим все фрагменты в единый модуль. Вы можете поместить этот окончательный код в файл .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()); } } //+------------------------------------------------------------------+
Ожидаемый результат не требует пояснений. Можете смело его протестировать.
Заключение
С этим финальным кодом у вас есть все необходимое для анализа, обработки и даже генерации JSON непосредственно в MetaTrader 5:
- Анализ JSON: ParseString() преобразует необработанный текст в структурированную иерархию узлов.
- Запрос данных: GetChild(key) и GetChild(index) позволяют легко перемещаться по объектам и массивам.
- Валидация: Проверка IsValid() и GetErrorMsg() позволяет увидеть, был ли анализ успешным или возникли проблемы (например, несовпадающие фигурные скобки).
- Сериализация: ToJsonString() повторно собирает узел (и дочерние элементы) обратно в корректный текст JSON.
Вы можете свободно адаптировать эту библиотеку под свои нужды. Например, вы можете добавить более подробные отчеты об ошибках, специализированные числовые преобразования или потоковые возможности для очень больших наборов данных. Однако этой основы должно быть достаточно для большинства типичных случаев использования, таких как чтение параметров из файла или взаимодействие с веб-API.
На этом всё! Вы достигли конца нашего глубокого погружения в обработку JSON в MQL5. Независимо от того, внедряете ли вы сложную торговую систему, управляемую данными, или просто загружаете параметры конфигурации из локального файла, надежный парсер и сериализатор JSON могут значительно облегчить вам жизнь. Надеемся, что эта статья (и код в ней) помогут вам легко интегрировать JSON в ваши автоматизированные торговые процессы.
Удачного программирования! Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16791
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Создание анимированного советника для новостей в MQL5 (X) — Представление графика с несколькими символами для торговли на новостях
Самоорганизующиеся карты Кохонена в советнике MQL5
Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Окончание)
Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Энкодер)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Что ж, руки дойдут, сделаю сравнительный бенчмарк.
Сравнил скорость работы 4 библиотек, включая ToyJson3 из MQL5Book. Как образец json взял ответ Binance на "exchangeInfo" размером 768 Кб. Он при чтении библиотекой из строки парсится целиком, затем я выбираю один символ и считываю все его данные. В цикле 100 раз.
Результат (время обработки запроса):
99.5 ms - JAson 1.12 (https://www.mql5.com/ru/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/ru/code/53107)
1132 ms - JsonNode (данная библиотека)
38 ms - моя реализация на основе JSON
PS: Когда-то здесь, кажется, всплывала ещё одна, очень урезанная, библиотека. Но я потерял следы.
PPS: Скрипт для замера не публикую. Код в совершенно неприглядном виде.
Я сравнил скорость работы 4 библиотек, включая ToyJson3 из MQL5Book. В качестве образца json я взял ответ Binance на запрос "exchangeInfo" размером 768 Кб. Когда библиотека считывает его из строки, он разбирается целиком, затем я выбираю один символ и считываю все его данные. В цикле 100 раз.
Результат (время обработки запроса):
99,5 мс - JAson 1.12(https://www.mql5.com/ru/code/13663)
85,5 мс - JAson 1.13
46,9 мс - ToyJson3 (https://www.mql5.com/ru/forum/459079/page4#comment_57805801)
41 мс - JSON(https://www.mql5.com/ru/code/53107)
1132 мс - JsonNode (эта библиотека)
38 мс - моя реализация на основе JSON
PS: Когда-то здесь всплывала другая, очень урезанная, библиотека. Но я потерял ее из виду.
PPS: Я не публикую скрипт для измерения. Код находится в совершенно неприглядном виде.
Не могли бы вы выложить json-строку или файл, пожалуйста?
https://fapi.binance.com/fapi/v1/exchangeInfo
https://eapi.binance.com/eapi/v1/exchangeInfo
778 КБ (796 729 байт)
Не могли бы вы выложить json-строку или файл, пожалуйста?