Это третья и заключительная часть статьи про работу с API онлайн-сервисов в Delphi. В этой части мы разработаем свое приложение, использующее модуль над которым мы работали в предыдущей части.
- Первая часть: Работа с API онлайн-сервисов в Delphi. Введение.
- Вторая часть: Работа с API онлайн-сервисов в Delphi. Авторизация и работа с методами API
Готовое приложение для работы со списком пользователей etxt.ru
На данный момент мы реализовали три метода API:
- Получение списка категорий
- Получение списка пользователей
- Получение подробной информации о пользователе
Этих трех методов вполне достаточно, чтобы написать простенькое приложение для работы со списками пользователей сайта etxt.ru. Пусть интерфейс приложения будет таким:
Приложение будет работать следующим образом:
- Задаем параметры поиска (рейтинг пользователей, онлайн-статус).
- Жмем поиск — получаем с сервера всех пользователей, удовлетворяющих параметрам поиска
- Все найденные пользователи выводятся в список слева
- Выбираем пользователя в списке — получаем подробную информацию о пользователе (функция GetUserInfo2)
- Выводим подробную информацию на форму
Что нужно предусмотреть при работе приложения:
- Как мы уже определили, когда писали метод получения подробной информации, для тематических категорий пользователя возвращаются только их ID. Следовательно, ещё ДО запроса подробной информации у нас под рукой должен быть список всех категорий (должна выполнится процедура ListCategories)
- На данный момент функция ListUsers может возвращать максимум 100 пользователей за раз (это ограничение самого сервиса etxt.ru). Следовательно надо написать метод, который будет работать с ListUsers и возвращать всех пользователей
- Необходимо предусмотреть способ прерывания поиска пользователей.
Начнем реализовывать все функции программы по порядку.
Подключаем в uses главной (и единственной) формы приложения наш модуль с классами для работы с API и создаем следующие переменные:
unit main; interface uses ..., etxt.api; type TForm3 = class(TForm) [...] private Etxt:TEtxtClient; ACancel: boolean; public end;
Как было сказано выше, нам необходимо получить список категорий до того, как мы запросим подробную информацию о пользователе. Например, мы можем это сделать сразу после создания объекта Etxt. Пишем такие обработчики OnCreate() и OnDestroy() для формы:
procedure TForm3.FormCreate(Sender: TObject); begin Etxt:=TEtxtClient.Create; Etxt.Token:='СЮДА_ПИШЕМ_СВОЙ_КЛЮЧ_ДОСТУПА'; Etxt.ApiPass:='СЮДА_ПИШЕМ_СВОЙ_ПАРОЛЬ'; //получаем список категорий Etxt.ListCategories; end; procedure TForm3.FormDestroy(Sender: TObject); begin Etxt.Free; end;
Здесь стоит отметить следующий момент:
Итак, первый момент работы программы выполнен — у нас имеется список категорий. Двигаемся далее.
Получаем с сервера всех пользователей, удовлетворяющих параметрам поиска
На момент написания этой статьи количество пользователей etxt.ru составляло более 500 000. Не все, конечно, активные, но тем не менее. Если предположить, что мы зададим такие параметры поиска при которых сервер нам должен будет вернуть всех пользователей, то мы можем получить ситуацию в которой наше приложение «подвиснет» серьезно и на долго. Так, если предположить, что сервер будет нам отвечать за 0,5 сек., а парсинг результатов будет вообще практически моментальным, то на запрос всей базы пользователей у нас уйдет больше 40 минут.
Обычно для таких ситуаций я предусматриваю отдельный поток в приложении, но для примера мы обойдемся более простым решением. У нас имеется переменная ACancel — именно её значение будет нам говорить о том, надо ли прерывать процесс получения списка пользователей или нет, а вся работа приложения будет проходить в одном (основном потоке).
Теперь ещё раз посмотрим на метод ListUsers, которым мы сейчас будем пользоваться:
function ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline: boolean):integer;
Как нам отвечает сервер, при задании разных значений входных параметров функции?
Нумерация пользователей в базе сервера начинается с нуля. Следовательно, если мы задаем такие параметры:
function ListUsers(100, 0, 0, 10, True)
то сервер будет обрабатывать наш запрос примерно следующим образом:
- выберет всех пользователей, который в данный момент находятся на сайте (у нас AOnline = True)
- из полученного списка отсеет всех пользователей у которых рейтинг более 10 (у нас начальный рейтинг равен 0, конечный — 10)
- из полученного списка выберет первых 100 пользователей (у нас ACount = 100)
- выдаст полученный список нам.
При этом, если в конечной выборке окажется меньше 100 пользователей, то сервер вернет нам всех найденных пользователей, т.е. список с сервера может содержать не более ACount записей.
Что будет, если мы зададим такие параметры:
function ListUsers(100, 10, 0, 10, True)
В этом случае сервер выполнит те же действия, что и в предыдущем случае, но в конечный список будет записывать пользователей, начиная с 11-го пользователя (помним, что первый пользователь у нас имеет порядковый номер ноль). Окончательную выборку пользователей в данном случае можно представить так:
Теперь напишем тот самый метод, который будет выдавать нам весь список пользователей, удовлетворяющих заданным нами условиям:
type TForm3 = class(TForm) [...] private [...] procedure ListAllUsers(ARateFrom, ARateTo:integer; AOnline: boolean); public end; implementation const cUserCount = 'Всего: %d чел.'; cButtonCancel = 'Отмена'; cButtonFind = 'Поиск'; cMaxUserCount = 100; procedure TForm3.ListAllUsers(ARateFrom, ARateTo: integer; AOnline: boolean); var AFrom: integer; begin //указываем, что на первом шаге мы будем получать список с самого начала AFrom:=0; //Указываем, что поиск должен проводиться ACancel:=False; //если сервер возвращает на очередном шаге цикла 100 записей //и пользователь не нажал "Отмену" //то продолжаем запросы //если сервер вернет не 100, а 99 или менее записей - значит это был последний ответ с данными //далее сервер будет возвращать пустой список //если пользователь нажал кнопку "Отмена", то надо прервать дальнейшие выполнения запросов на сервер while (Etxt.ListUsers(cMaxUserCount,AFrom,ARateFrom,ARateTo,AOnline)=cMaxUserCount)and (not ACancel) do begin inc(AFrom,cMaxUserCount); //выводим в Label количество уже найденных пользователей lbCount.Caption:=Format(cUserCount,[etxt.UserCount]); Application.ProcessMessages; end; //выводим окончательное количество найденных пользователей lbCount.Caption:=Format(cUserCount,[etxt.UserCount]); //меняем надпись на кнопке поиска с "Отмена" на "Поиск" btnFind.Caption:=cButtonFind; end;
Комментариев в методе больше, чем кода :). Но, думаю, что так любой желающий сможет разобраться с работой метода. Теперь напишем обработчик события OnClick кнопки «Поиск»:
procedure TForm3.btnFindClick(Sender: TObject); var I: Integer; begin //если надпись на кнопке "Поиск" - начинаем поиск if SameText(btnFind.Caption, cButtonFind) then begin //меняем надпись на кнопке поиска с "Поиск" на "Отмена" btnFind.Caption:=cButtonCancel; //очищаем список пользователей от результатов предыдущего поиска Etxt.ClearUserList; //чистим ListBox listUsers.Items.Clear; //ищем всех пользователей по заданным условиям ListAllUsers(StrToInt(edRateMin.Text),StrToInt(edRateMax.Text),chkOnline.Checked); //начинаем выводить список listUsers.Items.BeginUpdate; try for I := 0 to Etxt.UserCount-1 do lbUsers.Items.AddObject(Etxt.User[i].Fio, Etxt.User[i]); //сортируем список по алфавиту listUsers.Sorted:=True finally listUsers.Items.EndUpdate; end; end else //надпись на кнопке - "Отмена", значит прерываем поиск ACancel:=True; end;
Теперь можно запустить приложение и посмотреть как работает поиск:
Как можно видеть на рисунке, по заданным условиям поиска сервер вернул всего 75 пользователей. Теперь научимся выводить подробную информацию о пользователе.
Вывод подробной информации о пользователе
В начале определимся с тем как мы будем работать с тематическими категориями, которые пользователь указывает в своей информации. На данный момент класс TUserInfo содержит только список идентификаторов категорий:
TUserInfo = class(TUser) private [...] FCategories: TStrings; [...] public [...] property CategoryID[AIndex:integer]: string read GetCategoryID; [...] property CatCount: integer read GetCatCount; end;
Однако, в самом начале работы над программой мы уже загрузили список всех категорий с сайта. Логичным будет написать небольшую функцию, которая будет проводить поиск категории по её идентификатору. Расположить эту функцию вы можете где угодно. Например, я создал её в классе TEtxtClient:
TEtxtClient = class(TEtxtAPI) private [...] public function FindCategory(AID: string):TCategory; [...] end;
Функция достаточно просто:
function TEtxtClient.FindCategory(AID: string): TCategory; var I: Integer; begin Result:=nil; for I := 0 to FCategories.Count-1 do if FCategories[i].Id=AId then Exit(FCategories[i]); end;
Если в загруженном списке категорий найдется та, у которой значение идентификатора (id) будет равно AID, то она вернется в результате функции, иначе — метод вернет nil.
Теперь у нас есть все необходимые методы для вывода подробной информации о пользователе. Пишем обработчик события OnClick() у списка listUsers:
procedure TForm3.listUsersClick(Sender: TObject);
var UI: TUserInfo; I: Integer; Cat: TCategory; begin //если выбран какой-либо элемент списка (пользователь) if listUsers.ItemIndex>-1 then begin //очищаем список выполняемых пользователем работ listWorks.Items.Clear; //очищаем список тематических категорий пользователя listCategories.Items.Clear; //запрашиваем информацию о пользователе UI:=Etxt.UserInfo2[listUsers.Items.Objects[listUsers.ItemIndex] as TUser]; //информация была успешно получена if Assigned(UI) then begin //выводим информацию на форму lbFio.Caption:=UI.Fio; lbLogin.Caption:=UI.Login; lbCountry.Caption:=UI.Country; lbCity.Caption:=UI.City; lbRegdate.Caption:=DateTimeToStr(UI.Regdate); lbRate.Caption:=UI.Rate.ToString(); lbStatus.Caption:=cStatus[UI.Online]; lbGroup.Caption:=UI.Group; //формируем список выполняемых работ for I := 0 to UI.WorkCount-1 do listWorks.Items.Add(UI.Work[i]); //формируем список тематических категорий for I := 0 to UI.CatCount-1 do begin Cat:=Etxt.FindCategory(UI.CategoryID[i]); //если нашли категорию в списке if Assigned(Cat) then listCategories.Items.Add(Cat.Name) //категория не найдена - выводим в список только ID else listCategories.Items.Add(UI.CategoryID[i]) end; end; end; end;
Единственный момент в этом методе на который стоит обратить внимание — это:
UI:=Etxt.UserInfo2[listUsers.Items.Objects[listUsers.ItemIndex] as TUser];
Сделать такое стало возможным, т.к. при выводе списка пользователей мы делали так (см. обработчик события OnClick кнопки btnFind выше):
lbUsers.Items.AddObject(Etxt.User[i].Fio, Etxt.User[i]);
Опять же, в зависимости от потребностей и собственных предпочтений, теоретически, в список можно было бы писать не только объекты типа TUser, поэтому в рабочем приложении не лишним будет проверить, что мы получаем из списка именно объект типа TUser. Но в этом приложении-примере я точно знаю, что в списке могут быть только TUser и ничего другого.
Вот теперь наше приложение полностью готово. Можем снова его запустить и полюбоваться конечным результатом:
Надеюсь, что на данном этапе вы уже освоились с API, более или менее разобрались как формировать запросы к серверу и разбирать его ответы. Теперь можно перейти к другим моментам работы с API онлайн-сервисов, на которые стоит обращать внимание.
Работа с исключениями API
В этой части я не в коем случае не хочу затрагивать тему обработки исключений в Delphi вообще. Во-первых, я не пишу здесь учебник по Delphi для начинающих. А, во-вторых, на эту тему есть замечательная и очень большая статья в «Королевстве Delphi» под названием «Обработка ошибок«. Почитайте — лишним явно не будет. Здесь же я затрону только те моменты, которые касаются непосредственно работы с API.
Очень важный момент работы с любым API — это правильная обработка исключений. А точнее их интерпретация как для себя (разработчика), так и для пользователей. Внимательно изучите документацию API на предмет того как сервер сообщает вам об исключениях.
При работе с API онлайн-сервисов возможны следующие варианты отправки исключений серверами:
1. Сервер отправляет стандартный код статуса HTTP.
Например, сервер может ответить на очередной запрос «404 Not Found»
В этом случае, всё, что вы можете сделать, чтобы интерпретировать такое исключение — это обратиться к списку кодов состояния HTTP и узнать, что именно сообщил вам сервер. Или же, если у API есть подробная документация, то описание каждого кода статуса дается в этой документации.
Например, API Dropbox на код статуса 404 дает следующее описание исключения — «File or folder not found at the specified path»
Получив такой ответ от сервера Dropbox, можно понять, что в запросе указан неверный путь (иначе сервер ответил бы кодом 200 с пустым JSON-объектом).
2. Сервер всегда отвечает на запрос сообщением с кодом статуса «200 Ok», а информацию об исключении пересылает в теле сообщения
Пример такого сервиса рассмотрен выше. Сервер etxt.ru, как бы я не издевался над ним, пока писал примеры, всегда отвечал кодом статуса 200, а в теле сообщения возвращал или описание ошибки (в виде обычной строки) или JSON-объект с данными. Поэтому во всех методах где шло обращение к серверу, я делал такую простую проверку:
AJson:=GetJson(AResponse); if not Assigned(AJson) then raise Exception.Create(FLastError);
Не смогли получить JSON-объект, значит ответ сервера — это строка с описанием исключения, которую мы и показываем пользователю приложения. Других вариантов тут нет, т.к. если мы дошли до этой проверки, то никаких непредвиденных ситуаций в работе с HTTP не было (иначе исключение «выскочило» бы ещё в момент выполнения метода GET).
В этом случае, сервер выдает вполне понятное и внятное описание исключения, например, «Не верно указан пароль доступа к API» или «Подпись запроса неверна» и т.д.
Стоит отметить, что при таком способе выдачи исключений сервер может выдавать описание ошибки и в виде JSON-объекта. В этом случае, обработка исключения была бы чуть по-сложнее, например, я бы проверял ЧТО содержится в JSON:
//первая пара с именем "error" if (AJSON.Count>0) and SameText(AJSON.Pairs[0].JsonString.Value, 'error') then //тут разбираем JSON-объект, находим описание исключения и выдаем пользователю raise EMyAPIException.Create('Описание_исключения')
Описание JSON-объекта с исключением обязательно должно даваться в документации к API.
3. Сервер отправляет сообщение с разными кодами статуса (401, 403, 404 и т.д.) и дополняет свое сообщение расширенным описанием исключения.
Такой способ передачи исключений используется в Google API. К примеру, сервер, возвращает код статуса 404 и дополняет сообщение подробным описанием того, что пошло не так. Как получить описание такого исключения? Окончательный код обработки подобных исключений, конечно же, напрямую зависит от того, какую библиотеку для работы с HTTP Вы используете. Например, если используется Indy, то для получения описания исключения можно использовать такой код:
var HTTP:TidHTTP; Response: string; ErrorText: string; begin try Response:=HTTP.Get('http://example.com/api') except //сервер код исключения (3хх, 4хх или 5хх) on E: EIdHTTPProtocolException do begin //получаем описание исключения ErrorText:=E.ErrorMessage; raise EMyAPIException.Create(ErrorText) end else raise; end; end;
Это опять же только пример того как получить описание исключения API в Indy и конечный код обработки зависит от возможностей конкретного API: один сервис вернет простую строку, второй — json-объект, а третий может и XML выслать. Кроме того, не обязательно на любой код исключения будет дано описание в ErrorMessage. Например, сервер может дополнять сообщение об исключении подробным описанием только для кодов статуса 4хх, а 5хх возвращать без описания (опять же сталкивался с таким подходом в некоторых API Google).
Вот, пожалуй все те варианты выдачи исключений серверами с которыми я когда-либо сталкивался на практике. Может есть и другие способы, но я о них не знаю.
В любом случае помните, что всегда лучше затратить лишний час и написать методы обработки исключений API, чем оставить эту часть работы как есть, а потом получать от программы, что-то невнятное, вместо информации.
Заключение
На протяжении всего представленного материала по работе с API онлайн-сервисов в Delphi я старался как можно более подробно рассмотреть каждый шаг — давать самые подробнейшие комментарии в коде (иногда эти комментарии занимали места больше, чем сам код). Искренне надеюсь, что делал я это не зря.
И пусть вас не смущает тот момент, что я рассматривал всего один API, несмотря на то, что материал рассчитан на то, что, изучив его, вы сможете приступить к работе над любым API любого онлайн-сервиса. Разработав более десятка компонентов для работы с API онлайн-сервисов, среди которых и такие популярные как Google Calendar API, Contacts API, Twitter API, Dropbox API, ВКонтакте API, OneDrive API и прочие, я могу с уверенностью сказать — принципы работы с любым API абсолютно те же самые, что и в рассматриваемых примерах:
- Изучили введение к API — определились с набором компонентов
- Изучили часть документации по авторизации — написали свой метод авторизации, проверили его на простом запросе.
- Изучили основную часть — определились с классами и объектами в Delphi
- Изучили документацию по исключениям API — написали свои обработчики, если это необходимо
А то, насколько быстро и правильно напишете код Delphi напрямую зависит от того насколько вы хорошо усвоили информацию о которой я говорил в первой части и как внимательно вы прочитали документацию.
Книжная полка
Описание Подробно рассматривается библиотека FM, позволяющая создавать полнофункциональное программное обеспечение для операционных систем Windows и OS X, а также для смартфонов и планшетных компьютеров, работающих под управлением Android и iOS
|
||
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|