Меню Закрыть

Введение #

Delphi 10.3 Rio предоставляет две платформы (фреймворка, библиотеки) для обработки JSON:

  • JSON Objects Framework: этот фреймворк создает временные объекты для чтения и записи данных JSON.
  • Readers and Writers JSON Framework: этот фреймворк позволяет напрямую считывать и записывать данные JSON.

По сути, именно с “JSON Objects Framework” начиналась вся работа с JSON в Delphi. Это достаточно простая в использовании библиотека, с помощью которой можно организовать обмен данными с каким-нибудь сервером, например, реализовать API онлайн-сервиса, или же сделать возможность сохранения настроек своей программы в json-формате и так далее.

В свою очередь, “Readers and Writers JSON Framework” – это логическое продолжение развития работы с JSON в Delphi. Как было указано выше, эта библиотека позволяет читать/писать json напрямую, не создавая при этом каких-либо промежуточных объектов. Логично предположить, что именно эту библиотеку стоит использовать при работе с “большими” JSON-данными, когда “JSON Objects Framework” не справляется с задачей парсинга json и выкидывает “Out of memory”. Более того, согласно официальной документации, “Readers and Writers JSON Framework” поддерживает работу с BSON.

Включить звук

ИП Малащук К.В 127576, г. Москва, ул. Илимская 5 корпус 2, офис Z 611 ОГРН 315774600075981
ИП Малащук К.В 127576, г. Москва, ул. Илимская 5 корпус 2, офис Z 611 ОГРН 315774600075981

Собственно, в этой статье я буду рассматривать работу библиотеки “JSON Objects Framework” для работы с json в delphi – посмотрю на удобство работы (с моей точки зрения), скорость обработки “большого” json и так далее. Затем, рассмотрю вторую библиотеку и, в итоге сравню обе библиотеки по нескольким критериям.

JSON #

Как Вы, видимо, знаете, в основу JSON положены следующие понятия:

  1. Объект (запись) – неупорядоченный набор пар “ключ: значение”. Объект всегда начинается с “{” и заканчивается “}”.
  2. Массив – упорядоченный коллекция значений. Массив начинается с “[” и заканчивается “]”. Элементы массива разделяются запятыми.
  3. Значение – строка в двойных кавычках. Значение может быть: строкой, числом, true, false, null, объектом, массивом.
  4. Строка – набор Unicode-символов.
  5. Число – любое число в десятичной системе счисления.

Например, вот такой JSON:

{
  "host_id": "https:webdelphi.ru:443",
  "verified": true,
  "ascii_host_url": "https://webdelphi.ru/",
  "unicode_host_url": "https://webdelphi.ru/",
  "main_mirror": {
    "unicode_host_url": "https://webdelphi.ru/",
    "verified": false
  },
  "host_data_status": "INDEXED",
  "host_display_name": "Блог WebDelphi.ru"
}

Содержит один вложенный объект с именем main_mirror и пары host_idverifiedhost_display_name и так далее.

А этот JSON содержит массив вложенных объектов (original_texts) и пары (count и quota_remainder). При этом массив содержит вложенный объект (в примере объект один, однако в реальности таких объектов может быть сколько угодно), в котором содержатся свои пары (id, content_snippet и date).

{
  "original_texts": [
    {
      "id": "explicit error message",
      "content_snippet": "explicit error message",
      "date": "2016-01-01T00:00:00,000+0300"
    }
  ],
  "count": 1,
  "quota_remainder": 1
}

Таким образом, JSON может быть какой угодно сложности – содержать или не содержать массивы и вложенные объекты, пары могут содержать значения boolean, null, числа или строки, имена пар могут несколько раз повторяться и так далее.

Для просмотра небольших по объему JSON-объектов можете воспользоваться приложением “JSON Viewer”. Скачать программу можно отсюда (ссылка для скачивания находится в конце статьи) или отсюда.

JSON Viewer покажет вам json в виде дерева. Например, последний представленный выше пример json в программе будет выглядеть вот так:

Теперь, освежив немного в памяти, что из себя представляет JSON, можно приступать к рассмотрению библиотек для работы с JSON в Delphi.

JSON Objects Framework #

“JSON Objects Framework” поддерживает все типы JSON, которые в Delphi представлены соответствующими классами:

  • TJSONObject – объект
  • TJSONArray – массив
  • TJSONNumber – число
  • TJSONString – строка
  • TJSONTrueTJSONFalse – True/False
  • TJSONNull – Null

Все эти классы являются потомками абстрактного класса TJSONValue, который, в свою очередь, является потомком TJSONAncestor. В этом плане ничего не поменялось с момента выхода Delphi 2010.

Создание JSON в Delphi #

Простые примеры JSON-объектов #

Начинать работу по созданию нового JSON следует с создания нового объекта (TJSONObject).

Для примера, создадим JSON-объект, который будет содержать всего одну пару следующего вида “Привет: мир!”. То есть строка “Привет” – это будет имя пары, а строка “мир!” – её (пары) значения.

В Delphi такой JSON-объект создается элементарно:

var JsonObject: TJSONObject;
begin
  JsonObject:=TJSONObject.Create;
  JsonObject.AddPair('Привет', 'мир!');

В итоге, JsonObject будет содержать следующий json:

{
    "Привет": "мир!"
}

Метод AddPair добавляет в JSON-объект новую пару и имеет следующее описание:

function AddPair(const Pair: TJSONPair): TJSONObject; overload;
function AddPair(const Str: TJSONString; const Val: TJSONValue): TJSONObject; overload;
function AddPair(const Str: string; const Val: TJSONValue): TJSONObject; overload;
function AddPair(const Str: string; const Val: string): TJSONObject; overload;

Этих четырех реализаций метода AddPair вполне достаточно, чтобы создать любой по сложности JSON-объект. Например, создадим вот такой JSON:

{
    "String": "Строка",
    "Integer": 255,
    "Boolean": true,
    "Double": 10.532,
    "Null": null
}

Создать такой JSON достаточно просто:
Вариант №1. Последовательно добавить каждую новую пару в объект:

var JSON: TJSONObject;
begin
  JSON:= TJSONObject.Create;
  //Добавляем в json строку
  JSON.AddPair('String','Строка');
  //Добавляем в json число Integer
  JSON.AddPair('Integer',TJSONNumber.Create(255));
  //Добавляем в json boolean
  JSON.AddPair('Boolean',TJSONBool.Create(True));
  //Добавляем в json число Double
  JSON.AddPair('Double',TJSONNumber.Create(10.532));
  //Добавляем в json Null
  JSON.AddPair('Null',TJSONNull.Create);

Вариант №2. Использовать результат функции AddPair и добавлять новые пары “в одну строку”:

JSON.AddPair('String','Строка')
        .AddPair('Integer',TJSONNumber.Create(255))
        .AddPair('Boolean',TJSONBool.Create(True))
        .AddPair('Double',TJSONNumber.Create(10.532))
        .AddPair('Null',TJSONNull.Create);

Какой из вариантов вам больше нравится – тот и используйте. Аналогичным образом создаются и более сложные JSON-объекты – содержащие вложенные объекты, массивы и так далее.

Создание массивов и вложенных объектов JSON в Delphi #

Например, создадим json следующего содержания:

{
  "kind":"tasks#task",
  "title":"Новая задача",
  "links":
  [
        {
      "type":"href",
      "description":"desc",
      "link":"http://webdelphi.ru"
    }
,
    
    {
      "type":"href2",
      "description":"desc",
      "link":"http://webdelphi.ru"
    }
  ]
}

Такой json уже содержит вложенный массив из двух элементов, где каждый элемент – вложенных объект с тремя парами. В JSON Viewer этот json выглядит следующим образом:

Создадим такой JSON в Delphi, используя JSON Objects Framework:

var JSON: TJSONObject;
    JSONArray: TJSONArray;
    JSONNestedObject: TJSONObject;
begin
  JSON:= TJSONObject.Create;
  try
    //Добавляем в json строки
    JSON.AddPair('kind','tasks#task')
        .AddPair('title','Новая задача');
    //Создаем массив
    JSONArray:=TJSONArray.Create;
    {-- добавляем в массив первый объект --}
    // 1. заносим в массив пустой json-объект
    JSONArray.AddElement(TJSONObject.Create);
    //получаем ссылку на добавленный объект
    JSONNestedObject:=JSONArray.Items[pred(JSONArray.Count)] as TJSONObject;
    //заполняем объект данными
    JSONNestedObject.AddPair('type','href')
                    .AddPair('description','desc')
                    .AddPair('link','http://webdelphi.ru');
 
    {-- добавляем в массив второй объект --}
    // 1. заносим в массив пустой json-объект
    JSONArray.AddElement(TJSONObject.Create);
    //получаем ссылку на добавленный объект
    JSONNestedObject:=JSONArray.Items[pred(JSONArray.Count)] as TJSONObject;
    //заполняем объект данными
    JSONNestedObject.AddPair('type','href2')
                    .AddPair('description','desc')
                    .AddPair('link','http://webdelphi.ru');
 
    //записываем массив в json-объект
    JSON.AddPair('links', JSONArray);

Поверьте, мы сейчас создали именно такой json-объект, как представлено выше. Как видите, ничего особенно сложного в создании любых JSON-объектов нет. Главное, на что стоит обращать внимание при создании json в delphi – какая пара является родителем для других пар. Все JSON-объекты, которые мы сейчас создавали были “видны” и понятны только нам, так как мы не пытались их каким-любо образом отобразить, например, в Memo. Вместе с этим, JSON Objects Framework представляет сразу несколько методов для “печати” созданных JSON-объектов.

Преобразование JSON в String #

Для того, чтобы преобразовать созданный JSON-объект в строку у JSON Objects Framework имеются следующие методы:

function ToString: string; override;
function ToJSON: string;
function Format(Indentation: Integer = 4): string; overload;
function ToBytes(const Data: TArray; Offset: Integer): Integer;override;
procedure ToChars(Builder: TStringBuilder);override;

Метод ToBytes представляет JSON-объект в виде массива байтов. Этот метод удобно использовать, например, когда необходимо перекодировать полученную строку в какую-либо кодировку, например, ANSI или UTF-8. Этот метод используется в методе ToJSON.
Метод ToJSON представляет JSON-объект в виде строки в кодировке UTF-8. Так будет выглядеть JSON-объект из последнего примера:

{"kind":"tasks#task","title":"\u041D\u043E\u0432\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430","links":[{"type":"href","description":"desc","link":"https:\/\/www.webdelphi.ru"},{"type":"href2","description":"desc","link":"http:\/\/webdelphi.ru"}]}

В свою очередь, метод ToString переводит Json-объект в UnicodeString и результат представления будет уже таким:

{"kind":"tasks#task","title":"Новая задача","links":[{"type":"href","description":"desc","link":"https:\/\/www.webdelphi.ru"},{"type":"href2","description":"desc","link":"http:\/\/webdelphi.ru"}]}

Как видите, русские буквы будут представлены “как есть”.
Метод ToChars использует объект TStringBuilder для представления JSON-объекта. Используется метод следующим образом:

var SB: TStringBuilder;
    SB:=TStringBuilder.Create;
    JSON.ToChars(SB);
    SB.ToString

Соответственно, если после этого не проводить с TStringBuilder никаких манипуляций, то результатом выполнения метода ToChars будет всё та же строка, что и при использовании метода ToString:

{"kind":"tasks#task","title":"Новая задача","links":[{"type":"href","description":"desc","link":"https:\/\/www.webdelphi.ru"},{"type":"href2","description":"desc","link":"http:\/\/webdelphi.ru"}]}

Метод Format применяется в том случае, если JSON-объект необходимо представить в человеко-понятном виде (с отступами и форматированием). При этом, в качестве параметра функции Format можно указать количество отступов для дочерних объектов (по умолчанию значение равно 4). Результатом работы Format будет следующее представление JSON-объекта:

{
    "kind": "tasks#task",
    "title": "Новая задача",
    "links": [
        {
            "type": "href",
            "description": "desc",
            "link": "https:\/\/www.webdelphi.ru"
        },
        {
            "type": "href2",
            "description": "desc",
            "link": "http:\/\/webdelphi.ru"
        }
    ]
}

Также следует обратить внимание на то, что при представлении JSON-объекта в виде строки delphi используется экранирование таких символов как ‘/’, ‘”‘, ”’ и так далее.

Таким образом, любой JSON-объект в “JSON Objects Framework” можно представлять: в виде массива байтов, в виде строки UTF-8, в виде строки Unicode, в любой кодировке, используя массив байтов или объект TStringBuilder, в человеко-читаемом виде, настраивая при этом количество отступов дочерних элементов.

С созданием и представлением JSON-объектов в Delphi разобрались. Следующая задача – научиться разбирать JSON любой сложности в Delphi.

Парсинг JSON в Delphi #

Разбор json в delphi при использовании “JSON Objects Framework” следует начинать также как и при создании нового json-объекта, то есть создать объект TJSONObject. Далее, в зависимости от того, что нам необходимо получить в результате парсинга JSON мы можем:

  1. Использовать перечислители (TJSONObject.TEnumerator)
  2. Получать доступ к паре в json-объекте, указывая путь до неё
  3. Получать доступ к паре в json-объекте, указав её имя
  4. и так далее.

Как я уже сказал выше, парсинг начинается с создания (получения) объекта TJSONObject.

Сделать это можно следующими способами:
во-первых, воспользоваться классовым методом ParseJSONValue, который в Delphi имеет следующее описание:

class function ParseJSONValue(const Data: PByte; const Offset: Integer; const ALength: Integer; Options: TJSONParseOptions): TJSONValue; overload; static;
class function ParseJSONValue(const Data: TArray; const Offset: Integer; IsUTF8: Boolean = True): TJSONValue; overload; inline; static;
class function ParseJSONValue(const Data: TArray; const Offset: Integer; Options: TJSONParseOptions): TJSONValue; overload; inline; static;
class function ParseJSONValue(const Data: TArray; const Offset: Integer; const ALength: Integer; IsUTF8: Boolean = True): TJSONValue; overload; inline; static;
class function ParseJSONValue(const Data: TArray; const Offset: Integer; const ALength: Integer; Options: TJSONParseOptions): TJSONValue; overload; inline; static;
class function ParseJSONValue(const Data: string; UseBool: Boolean = False; RaiseExc: Boolean = False): TJSONValue; overload; static;
class function ParseJSONValue(const Data: UTF8String; UseBool: Boolean = False; RaiseExc: Boolean = False): TJSONValue; overload; static;

Параметры метода следующие:

  • Data – это массив байтов, строка или строка UTF-8 для анализа.
  • Offset – это количество байтов, которое нужно пропустить с начала Data.
  • ALength – это количество байтов для чтения из Data.
  • IsUTF8 указывает, является Data текстом UTF-8, который содержит метку порядка байтов (True) или нет (False).
  • UseBool определяет, используют ли экземпляры, представляющие логическое значение, класс TJSONBool (True) или классы TJSONTrue и TJSONFalse (False).
  • Options – это набор параметров синтаксического анализа ParseJSONValue.
  • RaiseExc – определяет поведение метода при неудачном анализе json. True – вызывает исключение типа EJSONParseException, False – метод вернет nil, не создавая исключения.

Например, используя метод ParseJSONValue, можно распарсить json и получить TJSONObject можно следующим образом:

var JSON: TJSONObject;
begin
   JSON:=TJSONObject.ParseJSONValue('{"firstName": "Петров","lastName": "Пётр"}', False, True) as TJSONObject;
   {работаем с полученным TJSONObject}

во-вторых, распарсить json в delphi можно, используя метод Parse:

function Parse(const Data: TArray; const Pos: Integer; UseBool: Boolean = False): Integer; overload;
function Parse(const Data: TArray; const Pos: Integer; const Count: Integer; UseBool: Boolean = False): Integer; overload;

Параметры метода, аналогичны параметрам ParseJSONValue, то есть:

  • Data – массив байтов для анализа
  • Pos – это количество байтов, которое нужно пропустить с начала Data
  • Count – это количество байтов для чтения из Data.
  • UseBool – определяет, используют ли экземпляры, представляющие логическое значение, класс TJSONBool (True) или классы TJSONTrue и TJSONFalse (False)

Метод Parse можно использовать, например, так:

const
  cJsonStr = '{'+
   '"firstName": "Петров",'+
   '"lastName": "Пётр",'+
   '"address": {'+
   '    "streetAddress": "ул. Дельфийская, 101, кв.101",'+
   '    "city": "Дельфийск",'+
   '    "postalCode": "100301"'+
   '},'+
   '"phoneNumbers": ['+
   '    "812 123-1234",'+
   '    "916 123-4567"'+
   ']'+
'}';
 
var JSON: TJSONObject;
    JSONArray: TJSONArray;
    JSONNestedObject: TJSONObject;
begin
   JSON:=TJSONObject.Create;
   JSON.Parse(TEncoding.UTF8.GetBytes(cJsonStr),0);

В приведенном примере мы использовали TEncoding.UTF8 для получения массива байтов в кодировке UTF-8. Если бы мы этого не сделали, то в результате вызова метода Parse мы получили бы исключение. Пример ошибочного вызова метода Parse (в примере используется тот же json, то и выше):

JSON:=TJSONObject.Create;
JSON.Parse(BytesOf(cJsonStr),0);

В результате, получим вот такое исключение:

UTF8: An unexpected continuation byte in 2-byte UTF8. Path ”, line 1, position 17 (offset 16)

Теперь, получив в свое распоряжение TJSONObject, можно приступать к получению из него необходимых на данных. Рассмотрим несколько примеров того, как можно как парсить json в Delphi 10.3 Rio, используя JSON Objects Framework.

Пример №1. Работа с парами JSON в Delphi #

Например, нам необходимо получить имя и фамилию человека из вот такого объекта JSON:

{
   "firstName": "Петров",
   "lastName": "Пётр",
   "address": {
       "streetAddress": "ул. Дельфийская, 101, кв.101",
       "city": "Дельфийск",
       "postalCode": "100301"
   },
   "phoneNumbers": [
       "812 123-1234",
       "916 123-4567"
   ]
}

Сделать это можно следующими способами:
1. Получить доступ к парам json-объекта, используя свойство Values у TJSONObject:

const
  cJsonStr = '{'+
   '"firstName": "Петров",'+
   '"lastName": "Пётр",'+
   '"address": {'+
   '    "streetAddress": "ул. Дельфийская, 101, кв.101",'+
   '    "city": "Дельфийск",'+
   '    "postalCode": "100301"'+
   '},'+
   '"phoneNumbers": ['+
   '    "812 123-1234",'+
   '    "916 123-4567"'+
   ']'+
'}';
 
 
var JSON: TJSONObject;
begin
   JSON:=TJSONObject.ParseJSONValue(cJsonStr,False, True) as TJSONObject;
   try
     ShowMessage(JSON.Values['firstName'].Value);
     ShowMessage(JSON.Values['lastName'].Value);
   finally
     JSON.Free;
   end;
end;

здесь мы указали в свойстве Value имя пары значение которой необходимо получить. Value возвращает объект TJSONValue свойство Value которого содержит значение пары. Аналогичным образом работает и метод TJSONObject GetValue:

function GetValue(const Name: string): TJSONValue; overload;

Поиск чувствителен к регистру и возвращает первую пару со строковой частью (именем), совпадающей с аргументом Name.

2. Использовать метод FindValue
В отличие от свойства Value этот метод позволяет указывать путь к определенному значению в json-объекте.

function FindValue(const APath: string): TJSONValue;

Если значение не будет найдено, то метод вернет значение nil.

Применительно к нашей задаче (чтение имени и фамилии из json) использование FindValue будет точно таким же, как и в предыдущем способе использование свойства Value, то есть код Delphi будет вот таким:

JSON:=TJSONObject.Create;
   try
     JSON.Parse(TEncoding.UTF8.GetBytes(cJsonStr),0);
     ShowMessage(JSON.FindValue('firstName').Value);
     ShowMessage(JSON.FindValue('lastName').Value);
   finally
     JSON.Free;
   end;

Однако, здесь мы не раскрыли всех преимуществ этой функции. Например, с помощью FindValue мы можем получить доступ к значениям, содержащимся во вложенных объектах JSON. В нашем тестовом JSON-объекте есть вложенный json-объект, содержащий почтовый адрес. Прочитать, например, название улицы не составит никакого труда:

JSON.FindValue('address.city').Value;

Здесь мы указали уже не имя пары JSON, а путь – прочитали значение пары “city” из вложенного json “address”.

3. Использовать перечислитель всех пар JSON-объекта
Этот способ удобно использовать, например, когда необходимо получить вообще все данные из json, а не только отдельные значения. Или же в том случае, если какой-либо пары в json-объекте может не быть в зависимости от наличия той или иной информации на сервере (такое, кстати, вполне возможно при работе с API онлайн-сервисов).

Вот как может выглядеть чтение строк из json с использованием перечислителя (TJSONObject.TEnumerator):

var JSON: TJSONObject;
    JSONEnum: TJSONObject.TEnumerator;
begin
   //создаем JSON-объект и парсим json
   JSON:=TJSONObject.Create;
   JSON.Parse(TEncoding.UTF8.GetBytes(cJsonStr),0);
   try
     //получаем доступ к перечислителю
     JSONEnum:=JSON.GetEnumerator;
     //пока есть не перечисленные пары в json - переходим на следующую пару
     while JSONEnum.MoveNext do
       //проверяем имя json-пары и, если находим подходящее - выводим сообщение
       if SameText(JSONEnum.Current.JsonString.Value, 'firstName')
         or SameText(JSONEnum.Current.JsonString.Value, 'lastName') then
           ShowMessage(JSONEnum.Current.JsonValue.Value);

Таким образом, прочитать строку из json в delphi можно, как минимум, тремя способами:

  1. Использовать свойство Value или метод GetValue объекта TJSONObject,
  2. Использовать метод FindValue,
  3. Использовать перечислитель TJSONObject.TEnumerator.

Ну и, напоследок, следующий код вернет значение пары или сообщение об ошибке, если пара отсутствует в json-объекте:

var S: string;
if JSON.TryGetValue('lastName', S) then
  ShowMessage(S)
else
  ShowMessage('Пара lastName не найдена')
Пример №2. Работа с массивом json в delphi #

Отдельный пример работы с JSON в Delphi – получение данных из массива json. В приведенном выше примере JSON массив содержит номера телефонов:

{
   ....
   "phoneNumbers": [
       "812 123-1234",
       "916 123-4567"
   ]
}

Как в этом случае перечислить все элементы массива json, используя возможности “JSON Objects Framework”?

Опять же, решение этой задачи в Delphi подразумевает использование нескольких вариантов.

1.Использование свойств TJSONArray для перечисления всех элементов массива json

Объект TJSONObject, в числе прочих, содержит следующие свойства:

  • Count – количество элементов массива
  • Items[Index: integer] – возвращает элемент массива с индексом Index в виде TJSONValue 

Используя эти свойства, перечислить все элементы массива из примера можно следующим образом:

   JSON:=TJSONObject.Create;
   JSON.Parse(TEncoding.UTF8.GetBytes(cJsonStr),0);
   try
    //получаем доступ к массиву json
    JSONArray:=JSON.Values['phoneNumbers'].AsType;
    //перечисляем все элементы массива
    for I := 0 to Pred(JSONArray.Count) do
      ShowMessage(JSONArray.Items[i].Value)

Здесь строит обратить внимание на последнюю строку:

ShowMessage(JSONArray.Items[i].Value)

так как мы знаем, что массив json содержит только строки, то мы смогли сразу использовать свойство Value и вывести значение элемента массива пользователю. Но массив json может содержать не только отдельные строки, но и, например, объекты json. В этом случае парсинг массива json в delphi немного усложняется.

2. Использование перечислителей массивов json
У объекта TJSONArray имеется свой тип перечислителя – TJSONArray.TEnumerator. работает он аналогично перечислителю объекта TJSONObject, который мы рассматривали выше. Например, вот так мы можем перечислить все номера телефонов из json, представленного выше:

var JSON: TJSONObject;
    JSONArray: TJSONArray;
    JsonArrEnum: TJSONArray.TEnumerator;
begin
   JSON:=TJSONObject.Create;
   JSON.Parse(TEncoding.UTF8.GetBytes(cJsonStr),0);
   try
    //получаем доступ к массиву json
    JSONArray:=JSON.Values['phoneNumbers'].AsType;
    //получаем перечислитель массива
    JsonArrEnum:=JSONArray.GetEnumerator;
    //перечисляем все элементы массива json
    while JsonArrEnum.MoveNext do
      ShowMessage(JSONArrEnum.Current.Value);

Таким образом, используя представленные выше варианты работы с массивом json в “JSON Objects Framework” можно получать любую интересующую вас информацию.

Рассмотрим более интересную задачу, которую можно решать в delphi при использовании с json – работу с вложенными объектами.

Пример №3. Работа с вложенными объектами json в delphi #

Попробуем разобрать вот такой JSON:

{
  "symbol":"AAPL",
  "stock_exchange_short":"NASDAQ",
  "timezone_name":"America/New_York",
  "intraday":
  {
    "2018-10-1915:59:00":
    {
      "open":"23",
      "low":"4"
    }
,
    "2018-10-1915:58:00":
    {
      "open":"25",
      "low":"21"
    }
  }
}

Дерево этого JSON в программе JSON Viewer будет выглядеть следующим образом:

Обратите внимание на пару “interday” – она является объектом json и, при этом, каждая пара этого объекта в качестве значения содержит вложенный json-объект. Что касается пар в начале объекта – symbol и прочих, то они уже для нас не представляют особого интереса – примеры по работе с ними представлены выше, а вот как разобрать intraday – рассмотрим более детально.

Итак, каждая пара в interday имеет свое уникальное имя (дату). Вложенный JSON-объект, в свою очередь, содержит две пары – open и low, которые и требуется прочитать.

Алгоритм парсинга json может быть следующим:

  1. Ищем в JSON объект intraday (как искать пары в json – см. выше)
  2. Используя перечислитель (enumerator), перечисляем пары, содержащиеся в JSON-объекте intraday;
  3. Используя возможности перечислителя проходим по парам вложенного объекта JSON и считываем значения из полей open и low.

Реализация этого алгоритма парсинга json, содержащего вложенные объекты может быть следующей:

  cJsonStr = '{' +
    '  "symbol":"AAPL",' +
    '  "stock_exchange_short":"NASDAQ",' +
    '  "timezone_name":"America/New_York",' +
    '  "intraday":' +
    '  {           ' +
    '    "2018-10-1915:59:00":' +
    '    {' +
    '      "open":"23",' +
    '      "low":"4"' +
    '    }' +
    ',' +
    '    "2018-10-1915:58:00":' +
    '    {' +
    '      "open":"25",' +
    '      "low":"21"   ' +
    '    }' +
    '  }' +
    '}';
 
var
  JSONValue: TJSONValue;
  JsonNestedObject: TJSONObject;
  quote, low, open: string;
begin
  JSONValue := TJSONObject.ParseJSONValue(cJsonStr).AsType;
  JsonNestedObject := JSONValue.FindValue('intraday').AsType;
  with JsonNestedObject.GetEnumerator do
    // цикл будет выполняться до тех пор, пока в JSON-объекте есть хотя бы одна не пройденная пара
    while MoveNext do
    begin
      // получаем название вложенного объекта (для первого прохода в цикле - это 2018-10-1915:59:00).
      quote := Current.JsonString.Value;
      // считываем значение пары low
      low := (Current.JSONValue as TJSONObject).Values['low'].Value;
      // считываем значение пары open
      open := (Current.JSONValue as TJSONObject).Values['open'].Value;
      // выводим полученные значения в Memo
      Memo1.Lines.Add(Format('%s, %s, %s', [quote, low, open]))
    end;
end;

Как видите, несмотря на кажущуюся сложность, разбор json в delphi оказался не таким уж и сложным, если воспользоваться знаниями о возможностях  “JSON Objects Framework”.

Жизненный цикл объектов в “JSON Objects Framework” #

В JSON родительский объект владеет любым из содержащихся в нем значений, если для свойства Owned не установлено значение False. В этом случае уничтожение объекта JSON пропускает каждый элемент, для которого установлен флаг False. Эта возможность позволяет объединять различные объекты в более крупные, сохраняя при этом право владения объектами. По умолчанию свойство имеет значение True, что означает, что все содержащиеся в нем экземпляры принадлежат их родителям.

Производительность “JSON Objects Framework” #

Чтобы оценить производительность “JSON Objects Framework” я решил провести следующие тесты:

  1. Оценить время создания нового JSON, содержащий пары без массивов и вложенных объектов
  2. Оценить время создания нового JSON с массивами и вложенными объектами
  3. Оценить время чтения JSON, содержащего пары без массивов и вложенных объектов
  4. Оценить время чтения JSON, содержащего пары с массивами и вложенными объектами.

На чем тестировалась работа:

  • Процессор Intel Core i5 8400 (6-ти ядерный)
  • ОЗУ: 16 ГБ
  • ОС: Windows 10 x64

Производительность “JSON Objects Framework” при создании JSON #

В качестве первого теста я создавал вот такой JSON:

{
  "Pair":"Value",
  "Pair":"Value",
  ...
}

Количество пар в json-объекте: 500 000
Время выполнения операции засекалось от момента создания экземпляра TJSONObject:

JSON := TJSONObject.Create;

До полного уничтожения объекта:

FreeAndNil(JSON);

Код теста:

var
  JSON: TJSONObject;
  PName, PValue: string;
  t1: TStopwatch;
  Count, Limit: integer;
  Str: TStringStream;
begin
  Count :=10;//количество повторений теста
  Limit :=500000;//количество пар в json
  PName :='Pair';
  PValue:='Value';
 
  StringGrid1.RowCount:=Count+1; //настраиваем количество строк таблицы
 
  for var I := 1 to Count do
  begin
    // засекаем всё время - от создания объекта до его уничтожения
    t1 := TStopwatch.StartNew;
    JSON := TJSONObject.Create;
    try
      for var J := 0 to Pred(Limit) do
        JSON.AddPair(PName, PValue);
    finally
      FreeAndNil(JSON);
    end;
    t1.Stop;
    //выводим результаты теста
    StringGrid1.Cells[0,i]:=I.ToString;
    StringGrid1.Cells[1,i]:=t1.ElapsedMilliseconds.ToString;
  end;
end;

Результаты тестирования:

  • Размер полученного JSON: 7500 kb
  • Максимальное время: 156 мс.
  • Минимальное время: 144 мс.

Для второго теста я создавал JSON следующего содержания:

{
  "Array":
  [
    "TestValue",
    "TestValue"
  ]
,
  "Array":
  [
    "TestValue",
    "TestValue"
  ]
}
  • Количество массивов в JSON: 10
  • Количество элементов массива: 500 000

Код теста:

var
  JSON: TJSONObject;
  JSONArray: TJSONArray;
  t1: TStopwatch;
  ArrayCount, ArrayLimit: integer;
  TestCount: integer;
begin
  ArrayCount :=  10;
  ArrayLimit := 500000;
  TestCount:=10;
 
  StringGrid2.RowCount:=TestCount+1;
 
  for var I := 1 to TestCount do
  begin
    // засекаем всё время - от создания объекта до его уничтожения
    t1 := TStopwatch.StartNew;
    JSON := TJSONObject.Create;
    try
      for var j := 1 to ArrayCount do
        begin
          JSONArray:=TJSONArray.Create;
          for var k := 1 to ArrayLimit do
            JSONArray.AddElement(TJSONString.Create('TestValue'));
          JSON.AddPair('Array',JSONArray);
        end;
    finally
      FreeAndNil(JSON);
    end;
    t1.Stop;
    StringGrid2.Cells[0,i]:=I.ToString;
    StringGrid2.Cells[1,i]:=t1.ElapsedMilliseconds.ToString;
  end;
end;

Результаты тестирования:

  • Размер полученного JSON: 60 Mb
  • Максимальное время: 601 мс.
  • Минимальное время: 565 мс.

В целом, учитывая, что тестирование проводилось на не самом слабом ПК, можно сказать, что производительность “JSON Objects Framework” по созданию новых объектов JSON в Delphi достаточно хорошая.

Теперь попробуем распарсить полученные JSON.

Производительность “JSON Objects Framework” при парсинге JSON #

Оба JSON-объекта, созданные в предыдущем тесте были сохранены в файл. В этой части статьи будем их парсить.

Разбор JSON, содержащего пары значений:

  • Количество пар в JSON: 500 000
  • Размер файла, содержащего JSON: 7500 kb

Код теста:

var
  S: TStringStream;
  JSON: TJSONObject;
  t_parse: TStopwatch;
  Bytes: TArray;
begin
  //считываем файл в TStringStream
  S := TStringStream.Create('', TEncoding.UTF8);
  try
    S.LoadFromFile('SimpleJson.txt');
    Bytes := S.Bytes;
  finally
    FreeAndNil(S);
  end;
 
  StringGrid2.RowCount:=11;
 
  for var I := 1 to 10 do
  begin
    //засекаем время от момента создания TJSONObject
    t_parse := TStopwatch.StartNew;
    JSON := TJSONObject.Create;
    try
      //парсим JSON
      JSON.Parse(Bytes, 0);
    finally
      FreeAndNil(JSON);
    end;
    //останавливаем таймер после уничтожения TJSONObject
    t_parse.Stop;
    //выводим результаты теста в TStringGrid
    StringGrid2.Cells[0, I] := I.ToString;
    StringGrid2.Cells[1, I] := t_parse.ElapsedMilliseconds.ToString;
  end;
end;

Результаты парсинга JSON:

  • Минимальное время: 225 мс.
  • Максимальное время: 232 мс.

Парсинг JSON, содержащего массивы значений. Здесь код теста идентичный предыдущему (меняется только название файла), поэтому приводить я его не буду, а сразу покажу результаты теста.

  • Минимальное время: 971 мс.
  • Максимальное время: 1021 мс.

Как видно из представленных результатов, парсинг JSON происходит примерно в 1,5 раза дольше, чем его (JSON) создание, но, в принципе, тоже приемлемо для небольших по объему JSON-объектов.

Однако, как я говорил ранее, в начале статьи, бывают случаи, когда “JSON Objects Framework” не справляется с задачей парсинга. Примером JSON, который нет возможности разобрать может служить набор открытых данных одного из органов государственной власти РФ. Архив с данными можно скачать здесь. Если по каким-либо причинам вы не можете скачать  этот файл, то здесь лежит его копия. Файл размером порядка 544 Мб. Очевидно, что в этом случае необходимо использовать второй фреймворк для работы с JSON в Delphi – Readers and Writers JSON Framework. И именно о нем мы поговорим с вами в следующей статье блога.

Работает на BetterDocs