Уже давненько собирался подробнее разобраться в работе с SuperObject, да все как-то не было подходящего момента — в последнее время использую сугубо родной DBXJSON, т.к. его возможностей с лихвой хватает для решения моих задач, а чисто ради спортивного интересу вникать во что-то новое сейчас особенного желания нет. Исключение — конкурс по FireMonkey…даже не так — FireMonkey изучается совсем не ради спортивного интереса :). Ну так вот, что касается SuperObject и почему сегодня речь пойдет про эту библиотеку. Где-то в районе майских праздников пришло ко мне на почту письмо с просьбой помочь немного разобраться с одним JSON-объектом, а именно — распарсить его с использованием SuperObject и вывести кое-какие данные из этого объекта в программу. Вот я и решил немного помочь с этой задачкой, т.к. это уже не просто «спортивный интерес» — вдруг да пригодится кому мое решение, если не в 1 в 1 скопированное, то хотя бы какие-то моменты из него, да и, думаю, примерчик окажется полезным для тех, кто решит вникнуть в работу с SuperObject.
В двух словах суть работы на сегодня: есть JSON-объект, содержащий информацию по областям РФ и, расположенным в этих областях городах. Наша программка должна выполнять следующие действия: загружать список областей в ComboBox, по выбранной области выводить в другой ComboBox список городов и, наконец, после выбора определенного города выводить его название, используемое в дальнейшем, как я понял, для составления какого-то URL — название города на латинице.
Одно из решений задачи смотрим ниже.
Во-первых, что из себя представляет наш JSON-объект? Его внешний вид представлен на рисунке ниже:
Проанализируем немного этот объект:
- Каждый элемент нашего JSON-объекта представляет из себя массив. Имя каждого элемента — какое-либо число.
- В каждом массиве имеется ровно пять элементов, при этом:
- элемент с индексом 1 — это название области/края/республики. Это значение нам надо использовать в программе.
- элемент с индексом 5 — это JSON-объект, содержащий список населенных пунктов. При этом:
- каждый элемент этого объекта, как и в случае с основным json-объектом — это массив, в котором:
- элемент с индексом 0 — название города. Это значение используется в программе
- элемент с индексом 1 — URL города. Это значение также используется в программе.
- каждый элемент этого объекта, как и в случае с основным json-объектом — это массив, в котором:
Остальная информация нам в программе не пригодится. Теперь посмотрим как можно разобрать такой JSON-объект в программе. Сразу оговорюсь, ниже представлено решение при котором JSON постоянно храниться в «мозгах» и мы к нему активно обращаемся при работе программы. Это, может, не совсем эффективно, но зато в этом случае мы можем посмотреть на то как используются различные классы SuperObject, а на это, собственно, и рассчитывалась статья — показать максимум возможностей в SuperObject, при решении конкретной задачи. Потом, если у кого-то возникнет желание, можете оптимизировать код сколько душе угодно — исходники, как обычно, будут вас ждать в конце поста и на странице с одноименным названием. Итак, начнем с того, что создадим новый проект VCL Application и разместим на форме два ComboBox’а, несколько Label’ов, 1 Button, 1 OpenDialog и 1 TEdit как показано на рисунке ниже:
Да, JSON будет выгружаться сегодня из простого текстового файла. Да и, собственно, какая разница откуда его грузить? Теперь снова посмотрим на первый рисунок с видом JSON-объекта и напишем небольшой класс, который будет возвращать нам всю необходимую информацию. Назовем его TRegions. И первое, что мы сделаем — это напишем конструктор/деструктор и получим список всех районов:
type TRegions = class private FJSONObject: ISuperObject; FAvlEnum: TSuperAvlIterator; public constructor Create(const AJsonString: string); destructor Destroy; override; function GetRegions(List: TStrings):integer; end; implementation constructor TRegions.Create(const AJsonString: string); begin inherited Create; FJSONObject:=TSuperObject.ParseString(PChar(AJsonString),false); if not Assigned(FJSONObject) then raise Exception.Create('Невозможно распарсить JSON') else FAvlEnum:=FJSONObject.AsObject.GetEnumerator; end; destructor TRegions.Destroy; begin if Assigned(FAvlEnum) then FAvlEnum.Free; inherited; end; function TRegions.GetRegions(List: TStrings): integer; begin Result:=0; if (not Assigned(FAvlEnum))or(not Assigned(List)) then Exit; List.Clear; FAvlEnum.First; repeat List.Add(FAvlEnum.Current.Value.AsArray.S[cRegionNameID]); until not FAvlEnum.MoveNext; Result:=List.Count; end;
В конструкторе мы пробуем получить ISuperObject, используя классовый метод TSuperObject.ParseString. И, если ISuperObject будет успешно получен, то сразу же запрашиваем у FJSONObject перечислитель для его пар. Что здесь стоит отметить? Во-первых то, что кроме обозначенного выше метода TSuperObject.ParseString мы могли бы спокойно воспользоваться и такими:
class function ParseStream(stream: TStream; strict: Boolean; partial: boolean = true; const this: ISuperObject = nil; options: TSuperFindOptions = []; const put: ISuperObject = nil; dt: TSuperType = stNull): ISuperObject; class function ParseFile(const FileName: string; strict: Boolean; partial: boolean = true; const this: ISuperObject = nil; options: TSuperFindOptions = []; const put: ISuperObject = nil; dt: TSuperType = stNull): ISuperObject;
то есть получить представление JSON-объекта в нашей программе, передав в парсер поток или файл с данными. Или же воспользоваться одной из вспомогательных функций в модуле superobject.pas:
function SO(const s: SOString = '{}'): ISuperObject; overload; function SO(const value: Variant): ISuperObject; overload; function SO(const Args: array of const): ISuperObject; overload;
Например, могли бы написать так:
FJSONObject:=SO(AJsonString); //TSuperObject.ParseString(PChar(AJsonString),false,false);
и результат, в нашем случае, был бы идентичным. В общем, можно отметить, что SuperObject имеет массу всевозможных методов для парсинга JSON: из строк, файлов, потоков, переменных типа Variant и т.д.
Следующий момент — это запрос перечислителя TSuperAvlIterator. Перечислитель этого типа удобно использовать, когда нам надо получать не только значение (Value) какой-либо пары, но и её имя (Name). Класс TSuperAvlIterator имеет следующее описание:
TSuperAvlIterator = class private FTree: TSuperAvlTree; FBranch: TSuperAvlBitArray; FDepth: LongInt; FPath: array[0..SUPER_AVL_MAX_DEPTH - 2] of TSuperAvlEntry; public constructor Create(tree: TSuperAvlTree); virtual; procedure Search(const k: SOString; st: TSuperAvlSearchTypes = [stEQual]); procedure First; procedure Last; function GetIter: TSuperAvlEntry; procedure Next; procedure Prior; function MoveNext: Boolean; property Current: TSuperAvlEntry read GetIter; end;
В принципе, здесь все методы и свойства должны быть понятны: First — переход к первому элементу, Prior — к предыдущему, MoveNext — вернет True, если удалось перейти к следующему и т.д. Соответственно TSuperAvlEntry — это класс, представляющий отдельную пару (ака TJsonPair в DBXJSON). И этот перечислитель мы запрашиваем для того, чтобы потом с помощью него «бегать» по парам нашего JSON-объекта и получать необходимую информацию. Ну, а самый первый метод, который активно использует перечислитель этого типа — GetRegions. Здесь мы, как сказано выше, читаем названия регионов:
//cRegionNameID = 1 FAvlEnum.First; repeat List.Add(FAvlEnum.Current.Value.AsArray.S[cRegionNameID]); until not FAvlEnum.MoveNext;
Проясним один маленький момент относительно использования repeat..until. При создании нашего перечислителя типа TSuperAvlIterator индекс текущего элемента в списке устанавливается в значение -1, что позволяет нам сразу же использовать метод MoveNext, который вернет нам первый элемент. Если бы мы гарантированно (железно, на 100%) были уверены, что метод GetRegions вызовется всего один раз за все время работы программы и существования нашего класса, то мы могли бы сделать проще и написать так:
while FAvlEnum.MoveNext do List.Add(FAvlEnum.Current.Value.AsArray.S[cRegionNameID]);
Но, ИМХО, надеяться на такое положение дел не стоит и поэтому мы вначале гарантированно устанавливаем курсор на первый элемент списка и уже после этого спокойно проходим по всему списку пар и выводим необходимые данные в список List. Кстати, про сам вывод. Вот какая была последовательность получения классов и интерфейсов:
FAvlEnum.Current.Value.AsArray.S[cRegionNameID]; //FAvlEnum.Current - получили TSuperAvlEntry //FAvlEnum.Current.Value - получили Value пары в виде ISuperObject //FAvlEnum.Current.Value.AsArray - представили ISuperObject в виде массива TSuperArray //FAvlEnum.Current.Value.AsArray.S[cRegionNameID] - прочитали второй элемент массива как простую строку string.
Теперь, если мы напишем в нашей основной программе, например, вот такой обработчик OnClick кнопки:
var Stream: TStringStream; begin if OpenDialog1.Execute then begin Edit1.Text:=OpenDialog1.FileName; Stream:=TStringStream.Create; try Stream.LoadFromFile(Edit1.Text); Regions:=TRegions.Create(Utf8ToAnsi(Stream.DataString)); Regions.GetRegions(ComboBox1.Items); finally Stream.Free; end; end;
То в запущенной программе получим вот такой результат:
Двигаемся далее. Так как при чтении регионов мы ничего нигде не запоминали кроме того, что записывали название региона в список List, то, в этом случае, логично было бы предусмотреть такой метод, который вернул бы нам json-объект конкретного региона по его названию, например, если во входящем параметре метода будет задана строка «Омская область», то в результате будет получен такой объект:
Напишем такой метод, используя все тот же перечислитель, что и в предыдущем случае:
function TRegions.GetRegionObject(const ARegion: string; out RegionID:integer): ISuperObject; begin if not Assigned(FAvlEnum) then Exit; FAvlEnum.First; repeat if SameText(FAvlEnum.Current.Value.AsArray.S[1], ARegion)then begin RegionID:=StrToInt(FAvlEnum.Current.Name); Exit(FAvlEnum.Current.Value); end; until not FAvlEnum.MoveNext; end;
Работа метода аналогична тому, что было рассмотрено выше в методе GetRegions. Остается только отметить, что кроме самого объекта GetRegionObject также вернет в выходном параметре RegionID:integer — имя пары которая содержит необходимый нам объект, чтобы в последствии можно было избегать повторной «пробежки» по всему списку перечислителя и сразу обращаться к значению пары по её имени.
Двигаемся далее. Следующий наш шаг — по выбранному в ComboBox’е региону сформировать список городов. Назовем этот метод GetCities. Метод будет таким:
function TRegions.GetCities(const ARegion: string; List:TStrings): integer; {cCityNameID = 0; cCitiesArrID = 5;} var RegionObject, CityObject: ISuperObject; ID: integer; CityEnum: TSuperEnumerator; begin if (not Assigned(FAvlEnum))or(not Assigned(List)) then Exit; List.Clear; RegionObject:=GetRegionObject(ARegion,ID); if Assigned(RegionObject) then begin CityObject:=RegionObject.AsArray.O[cCitiesArrID]; if Assigned(CityObject) then begin CityEnum:=CityObject.GetEnumerator; try while CityEnum.MoveNext do List.Add(CityEnum.Current.AsArray.S[cCityNameID]) finally CityEnum.Free; end; Result:=ID; end; end; end;
Рассмотрим опять, что здесь происходит и почему. Во-первых, после проверок на существование всех необходимых нам объектов, мы используем наш метод GetRegionObject и получаем объект региона и запоминаем его имя, чтобы потом передать в результате функции:
RegionObject:=GetRegionObject(ARegion,ID);
Затем, если объект найден, мы запрашиваем объект, содержащий названия городов:
CityObject:=RegionObject.AsArray.O[cCitiesArrID];
Если смотреть по структуре JSON, то с помощью этой строчки кода мы получили вот эту часть:
А дальше мы снова запрашиваем перечислитель, но уже другого типа:
CityEnum: TSuperEnumerator; .... CityEnum:=CityObject.GetEnumerator;
Почему именно такой перечислитель? Все просто: во-первых, для примера, т.к. никто нам не запрещал воспользоваться уже известным нам TSuperAvlIterator и спокойно пройтись по всем парам объекта, а во-вторых, этот тип перечислителя удобно использовать в том случае, если нас не интересуют имена пар — TSuperEnumerator в свойстве Current возвращает значение пары (Value). Ну и, также, TSuperEnumerator «понимает» как получить значение не только пары из объекта, но и элемента массива, если TSuperEnumerator был запрошен у объекта типа TSuperArray. Итак, получив в распоряжение перечислитель, мы проходимся по всем парам объекта и считываем названия городов:
while CityEnum.MoveNext do List.Add(CityEnum.Current.AsArray.S[cCityNameID])
Здесь мы знаем, что перечислитель «убьется» сразу же, после того как все элементы будут просмотрены, поэтому спокойно используем цикл while..do. Ну, а последовательность была чуть по-проще, чем в GetRegions:
CityEnum.Current.AsArray.S[cCityNameID] //CityEnum.Current - получили ISuperObject //CityEnum.Current.AsArray - представили объект в виде TSuperArray //CityEnum.Current.AsArray.S[cCityNameID] - получили строку, содержащую название города.
Теперь можем дописать событие OnChange у первого CoboBox и не забыть при этом сохранить ID региона (оно нам понадобиться далее):
var CurrentRegion:integer; .... procedure TForm8.ComboBox1Change(Sender: TObject); begin CurrentRegion:=Regions.GetCities(ComboBox1.Items.Strings[ComboBox1.ItemIndex],ComboBox2.Items); end;
В результате получим вот такое поведение программы:
Остался последний штрих — по выбранному в списке городу получить его представление для URL, то есть добраться в объекте вот сюда:
На данный момент наша программка уже «помнит» какой регион мы выбрали, поэтому метод GetCityURL можно сделать таким:
function TRegions.GetCityURL(const ARegionID: integer; ACity: string): string; var Region: TSuperArray; CityEnum: TSuperEnumerator; begin if not Assigned(FJSONObject) then Exit; Region:=FJSONObject.A[IntToStr(ARegionID)]; if Assigned(Region) then begin CityEnum:=Region.O[5].GetEnumerator; try while CityEnum.MoveNext do begin if SameText(CityEnum.Current.AsArray.S[cCityNameID],ACity) then begin Result:=CityEnum.Current.AsArray.S[cCityURLID]; break; end; end; finally CityEnum.Free; end; end; end;
Здесь опять же ради примера, продемострирован ещё один простой способ получение данных из JSON-объекта, а именно:
Region:=FJSONObject.A[IntToStr(ARegionID)];
Мы ведь не зря запоминали имя пары, когда искали регион :) Вот нам это имя и пригодилось — мы по имени запросили объект и получили его сразу в виде TSuperArray. Ну, а дальше — дело техники: просим вернуть нам перечислитель на пятый элемент массива (это как раз список городов) и проходим по элементам массива и ищем название города и, если город с таким названием найден, то, получаем из массива элемент, содержащий его URL. То есть здесь мы «бегали» вот по этому массиву в JSON-объекте:
Остается только «прицепить» наш новый метод в готовой программе. Пишем обработчик OnChange второго ComboBox:
procedure TForm8.ComboBox2Change(Sender: TObject); begin Label5.Caption:=Regions.GetCityURL(CurrentRegion,ComboBox2.Items.Strings[ComboBox2.ItemIndex]) end;
Результат работы программы представлен на рисунке ниже:
Вот, пожалуй, и решение задачи. Что мы смогли узнать нового про SuperObject пока писали нашу программу?
- SuperObject имеет кучу методов для получения ISuperObject, начиная от классовых методов у TSuperObject и, заканчивая, вспомогательными методами SO в модуле superobject.pas
- Для парсинга JSON в SuperObject имеется сразу два типа перечислителей. Первый тип перечислителей TSuperAvlIterator если в ходе перечисления нам необходимо получить пару целиком. Второй тип перечислителей — TSuperEnumerator использует в работе TSuperAvlIterator и может перечислять только значения пар. Использование второго типа перечислителей оправдано в случае, если нас не интересуют имена пар, а также, в том, случае, если нам необходимо перечислить элементы в масиве TSuperArray.
- Для получения значений пар из JSON-объекта можно не использовать вообще перечислители напрямую, если нам известно имя пары — в этом случае мы можем просто воспользоваться одним из методов у ISuperObject и получить необходимое нам значение буквально в одну строку.
И, в заключение, пара слов об исходнике. Всё, что рассмотрено в статье выше разрабатывалось в Delphi XE2 Architect, ОС Windows 7 x64, SuperObject — последняя ревизия на момент публикации статьи.
Книжная полка
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|
ссылка SuperObjects ведёт на список статей с пометкой SuperObjects.
Но — что это? :)
Всеволод Леонов, что такое SuperObject? Это библиотека для работы с JSON в Delphi. По сравнению с DBXJSON имеет кучу всяких разных удобных фич, но, как я сказал в самом начале — мне эти фичи без надобности, поэтому вполне удовлетворен работой с родной парсилкой.
Спасибо! :)
Андрей, не за что. Надеюсь, что чем-нибудь да помог вам разобраться с SuperObject =)
Спасибо за статью. [B]Vlad[/B], не делал никаких тестов по сравнению быстродействия SuperObject и DBXJSON?
Chrome~, сам тестов не делал, но посмотри на форуме — там вроде бы Neoksi говорил, что DBXJSON по-быстрее парсит JSON.
Chrome~, при работе с большими объемами JSON данных, лучше использовать DBXJSON, так как алгоритмы с ним работаю раза в 4 быстрее, чем с SuperObject. Но как всегда есть своё «но», для полноценной работы с веб-сервисами, на которые нужно отсылать данные в формате JSON, нужно дополнительно обрабатывать генерируемую строку DBXJSON.
Такое програмирование мной приветствуется. Сразу видно что автор профи в своем деле. Пример индексации очень нагляден.
Жаль, что уже 2 месяца сайт без обновлений.
Влад, есть надежда, что появятся новые статьи?
Teruis, да, к сожалению, обновлений по статьям нету :( Но я здесь =) и думаю, что скоро смогу начать снова публиковать заметки в блоге
может кому пригодится:
скачал c SuperObject v.1.2.4, у меня Д7. все примеры вываливались IntegerOverflow в модуле в ф-ии class function TSuperAvlEntry.Hash(const k: SOString): Cardinal строка 5638: h := h*129 + ord(k[i]) + $9e370001;
где-то в инете нашел такое решение:
{$OVERFLOWCHECKS OFF} // Add this line
class function TSuperAvlEntry.Hash(const k: SOString): Cardinal;
var
h: cardinal;
i: Integer;
begin
h := 0;
//{$Q-}
for i := 1 to Length(k) do
h := h*129 + ord(k[i]) + $9e370001;
//{$Q+}
Result := h;
end;
{$OVERFLOWCHECKS ON} // Add this line
Может быть у кого-то есть пример записи данных используя SeupeObject, а то пока возникает ряд проблем, и может кто-то покажет готовый пример кода отвечающий за запись используя именно эти компоненты.
Для тех у кого Delphi7
//Надо заменить код конструктора на:
constructor TRegions.Create(const AJsonString: string);
begin
inherited Create;
FJSONObject:=SO(AJsonString);
if not Assigned(FJSONObject) then
raise Exception.Create(‘Невозможно обработать JSON’)
else
FAvlEnum:=FJSONObject.AsObject.GetEnumerator;
end;
//а именно была удалена строка
//FJSONObject:=TSuperObject.ParseString(PChar(AJsonString),false);
//так как в Delphi 2010, переписаны классы, которые поддерживают более многочисленные виды кодировок
плюс ко всему в Delphi 2010 так же был доработан тип TStringStream поэтому вместо него лучше использовать TStringList
!!! (var Stream:TStringList) !!!
Stream.LoadFromFile(edit1.text);
Regions:=TRegions.Create(Utf8ToAnsi(stream1.Strings[0]));
Regions.GetRegions(ComboBox1.Items);
json приходит в формате строки вот пример
[{«id»:»303″,»text»:»Геленджицька»,»wide»:»вулиця»},{«id»:»1444″,»text»:»Електрична»,»wide»:»вулиця»},{«id»:»1445″,»text»:»Електрозаводська»,»wide»:»вулиця»},{«id»:»1446″,»text»:»Електроніки»,»wide»:»вулиця»},{«id»:»1447″,»text»:»Електростальська»,»wide»:»вулиця»}]
нужно в listbox1 добавить данные вот так
Геленджицька (303)
Електрична (1444)
Електрозаводська (1445) и тд
неделю парюсь — нигде ответа не нашел, помогите плиз
Версия Delphi какая? Просто в XE7 код по-длиннее, в XE10 можно сделать по-короче.
Версия просто 7. Delphi 7. Спустя неделю, еле разобрался. а все было так просто
var res:string;
J: ISuperObject;
JsonArray: TSuperArray;
i:integer;
begin
в res то что писал выше
J:=SO(res);
JsonArray:=J.AsArray; // вот тут и была загвоздка, когда явно указал на массив объектов — все пошло как по маслу.. просто начав изучать и json и методы обработки в делфи 7- все было так запутанно.
for i:=0 to JsonArray.Length-1 do //обрабатываем циклом то количество массива
begin
listbox1.items.add(JsonArray.O[i].S[‘text’]+’ (‘+JsonArray.O[i].S[‘id’]+’)’);
end;
end;
Ну да, в Delphi 7 SuperObject — самое то. Просто в более поздних версиях Delphi есть свои модули для работы с JSON и они по-проще в освоении, чем SuperObject. Рад, что сами разобрались с проблемой. Удачных проектов :)
На счет простоты JSON в «родных» компонентах Delphi.
Имеем простейший JSON объект:
s := ‘{«BaseName»:»d:\base\test.fdb»}’;
Попробуем распарсить его стандартным TJSONObject:
TJSONObject.ParseJSONValue(s) // вернет nil
не распарсит — потому как в значении «d:\base\test.fdb» есть символ «:», и как Вы его не «экранируйте», пока не представите строку без символов «:», не прокатит. Ну и на кой мне, как разработчику, такой забавный геморрой?
И это «встроенный» класс парсинга!!! Вот что мешает распарсить по-людски, предполагая наличие знака двоеточия в строковом значении — нормальным явлением????
Пока встречаются такие тупые ошибки в парсинге, все ценность «встроенной библиотеки JSON» стремительно сводится к нулю.