Работа с API онлайн-сервисов в Delphi.

advertisement
Работа с API онлайн-сервисов в Delphi.
Автор – Vlad (http://webdelphi.ru)
Содержание
1. Что необходимо знать и уметь?
2. Что делать, если время поджимает?
3. Начало работы с API. Какие компоненты выбрать?
4. Авторизация и получение доступа к API
o
Пример с etxt.ru
5. Выполнение запросов к API
o
Пример выполнения запросов к etxt.ru
6. Парсинг результатов запроса
o
Разбор данных etxt.ru
7. Выполнение взаимосвязанных методов
8. Готовое приложение для работы со списком пользователей etxt.ru
o
Получаем с сервера всех пользователей, удовлетворяющих параметрам
поиска
o
Вывод подробной информации о пользователе
9. Работа с исключениями API
10.Заключение
11.Файлы
Что необходимо знать и уметь?
Вначале определимся с тем, что должен знать разработчик, чтобы начать
работу над API любого онлайн-сервиса в Delphi. Вот, пожалуй, краткий
список того, чем необходимо владеть и, что необходимо понимать помимо
основ объектно-ориентированного программирования:
1. Знать принципы работы сетевых протоколов прикладного уровня, хотя
бы
работу
протокола
HTTP.
Умение
загружать
странички
в
TWebBrowser — не показатель знания и умения работы с HTTP. Не
поленитесь — почитайте информацию о методах, кодах состояния,
заголовках, о том, как составить правильный запрос, как понять, что вам
ответил сервер. Статей в Сети на эту тему — масса…тысячи их. Вот в этой
нашей статье дается краткая информация по HTTP.
2. Уметь
работать
с
библиотеками
и
компонентами
Delphi,
реализующих работу с сетевыми протоколами. От вас не требуется знать
все библиотеки досконально (тем более, что их, опять же, очень много), но
пользоваться хотя бы одной из них надо уметь ещё ДО того, как вы решили
сделать свой клиент для Twitter или ещё для чего-либо. Начните, например,
с Indy — она всегда у вас под рукой в Delphi. Потом, если вы хорошо
освоите п.1 из этого списка и посчитаете, что Indy вам не подходит, Вы
всегда с можете с легкостью понять работу и других библиотек и перейти,
например, на Synapse.
3. Необходимо уметь работать с различными кодировками текста. Это,
можно сказать, один из ключевых моментов и самых больших препятствий
для
начинающего
разработчика.
Всевозможные
форумы
по
программированию в Delphi просто пестрят сообщениями на эту тему:
«Кракозябры вместо текста«, «Знаки вопроса вместо текста«, «Indy
глючит — текст не читабельный» и т.д. и т.п. Поверьте мне на слово —
Indy тут не глючит :) Она выдает вам ровно то, что Вы просите. Не больше
и не меньше. Просто вы не умеете работать с кодировками. Что можно
почитать на тему кодировок в блоге:
1. Здесь я очень кратко рассказывал про некоторые кодировки, в принципе.
2. В статье «Нахождение кодировки веб-страницы» рассказывается про то
как определить кодировку странички, используя навыки работы с
библиотекой Synapse и регулярными выражениями.
3. Серия статей про MLang. В статье «MLang в Delphi. Работа с
кодировками Web-страниц» вы узнаете как использовать библиотеку
MLang для преобразования кодировок текста.
Статья «MLang.
Автоматическое определение кодировки веб-страницы» расскажет про
использование интерфейсов MLang для автоматического определения
кодировок страниц. В статье «Обновление MLang. Добавлена поддержка
Delphi 4-XE» вы найдете обновление модуля Delphi для работы с MLang.
4. Статья «3 варианта работы с кодировками веб-страниц в Delphi»
расскажет как использовать различные возможности Delphi для работы с
кодировками текста.
4. Уметь использовать функции хэширования, например MD5, SHA-1 и
кодирования, например, в Base64. На начальном этапе работы вам, в
принципе, будет достаточно прочитать небольшую статью под названием
«MD5, Base64, HMAC-SHA-1 в Delphi средствами Indy и Synapse«
5. Знать особенности различных способов и протоколов для авторизации
пользователей в Сети. Например, сейчас большое количество онлайнсервисов требуют авторизации пользователя по протоколу OAuth. Знать
особенности этого протокола необходимо, чтобы, как минимум, в
дальнейшем не «упростить» работу с протоколом так, что ваши
пользователи начнут шарахаться от вашего приложения как черт от ладана.
Про сам протокол можно прочитать, например, в wiki. Последняя статья в
блоге на эту тему касалась как раз того, что НЕ надо делать при работе с
протоколом OAuth. Некоторые сервисы предоставляют разработчикам свои
собственные способы авторизации, но здесь вам как раз помогут навыки,
полученные в п.1-4.
6. Уметь работать с такими форматами данных как XML и JSON. Если у
вас нет навыков работы с этими форматами, вы впервые слышите про то,
что они вообще существуют, то не начинайте работу над API пока не
исключите этот досадный пробел в своих знаниях – потеряете очень много
времени, впустую зависая на форумах.
В целом, изучение информации по пяти предоставленным пунктам должно
быть достаточно, чтобы начать работу над любым API онлайн-сервисов в
Delphi. При изучении каждого из пунктов старайтесь что-то делать в Delphi на
эту тему. Изучаете HTTP? Начните параллельно изучать, например, Indy для
работы с HTTP в Delphi — пробуйте скачивать странички сайтов из Сети,
обрабатывать исключения HTTP и т.д. Изучаете кодировки — напишите
простенькую программку для преобразования кодировок и т.д. Учитесь
постепенно — API от вас не убежит, поверьте. В общем — практика, практика
и ещё раз практика.
Частично, разобраться с вопросами, обозначенными выше, Вам поможет
представленный далее материал.
Что делать, если время поджимает?
Если время поджимает, а изучать что-то новое некогда/лень, но надо, кровь из
носу, получить компонент или программу для работы с API, то выход
есть…даже несколько выходов.
1. Отложить работу до тех пор, пока не появится время заняться изучением
вопросов, представленных выше.
2. Упрашивать кого-нибудь на форумах отдать вам готовые исходники,
обосновывая это дело своей жуткой занятостью. Не серьезное и не
достойное занятие для уважающего себя разработчика.
3. Вместо изучения пяти пунктов изучить один - научиться правильно
составлять техническое задание. Потом смело забросить ТЗ на какойнибудь freelance.ru и ждать того у кого было время изучить работу с HTTP,
кодировками и т.д.
Серьезно. Любой из упущенных моментов обучения ведет к серьезным
проблемам в дальнейшей работе:

не разберетесь в работе HTTP — замучаетесь составлять запросы к API,
работать с исключениями API и т.д.

не разберетесь с компонентами (хотя бы поверхностно) — вообще не
сможете приступить к работе, т.к. будете смотреть на эти компоненты
как…ну, в общем, с удивлением будете смотреть

не знаете, как работать с JSON — считайте, что 80% различных API вам
вообще недоступны.
А, имея хотя бы небольшой багаж знаний по представленным выше моментам,
Вы, как минимум, сможете внятно и обоснованно задавать вопросы на
форумах и получать с, большой долей вероятности, такие же внятные и
понятные вам ответы. Да и, скорее всего, вопросы задавать не понадобиться
вообще.
Начало работы с API. Какие компоненты
выбрать?
Онлайн-сервис, предоставляющий пользователям свой API, всегда сообщает
разработчикам информацию о протоколе по которому необходимо
осуществлять взаимодействие с сервером. Например, на самой первой
странице документации к любому API можно встретить такие фразы:
API определяет набор функций, к которым разработчики могут совершать
запросы и получать ответы. Взаимодействие происходит по протоколу
HTTP.
Или:
Для взаимодействия по протоколу SOAP см. документацию здесь, а для
взаимодействия по XML-RPC — здесь
Вместе с этим часто в документации можно встретить и такую информацию:
API реализовано на REST-принципах с использованием HTTP и XML для
обмена данными.
Что это всё значит для нас, как для разработчиков?
Эта краткая информация говорит нам о самом главном в дальнейшей работе — о том какие
компоненты и библиотеки надо будет задействовать в Delphi.
Например, первое сообщение говорит нам о том, что все запросы и ответы на
них передаются по протоколу HTTP. Что использовать для работы с HTTP в
Delphi? Библиотек и вариантов масса:
1. Indy — «родная» библиотека Delphi
2. Synapse
3. ICS
4. IP*Works
5. ещё с десяток различных платных и бесплатных компонентов и библиотек.
Что говорит нам второе сообщение? Второе сообщение говорит нам о том, что
взаимодействовать с сервером мы можем или по протоколу SOAP или
по протоколу XML-RPC. Что использовать в Delphi? Если прочитать, как и
посредством чего работают эти протоколы, то чисто теоретически, для
работы по этим протоколам можно ограничиться двумя компонентами:
1. Первый компонент — для транспорта. Это опять же, что то, что работает с
HTTP (см. выше перечень библиотек)
2. Второй компонент — для разбора ответов и составления запросов. Это
может быть или стандартный TXMLDocument или какая-нибудь сторонняя
библиотека, например, NativeXML.
Для работы по XML-RPC этих двух компонентов будет более, чем достаточно.
Но, если вы решили работать по
SOAP, то проще, лучше и
быстрее использовать уже готовые решения в Delphi — это инструмент для
импорта WSDL и компоненты с вкладки WebServices. О том, как ими
пользоваться я рассказывал на примере переводчика Bing в статье «API
переводчика Bing. Используем SOAP в Delphi«.
Третье сообщение интереснее, т.к. в нем появляется новое непонятное слово
— REST. Чтобы понять, что это такое — посмотрим на определение:
REST (Representational state transfer) – это стиль архитектуры программного
обеспечения для распределенных систем, таких как World Wide Web, который,
как правило, используется для построения веб-служб. Термин REST был
введен в 2000 году Роем Филдингом, одним из авторов HTTP-протокола.
Системы,
поддерживающие
REST,
называются
RESTful-системами.
В общем случае REST является очень простым интерфейсом управления
информацией без использования каких-то дополнительных внутренних
прослоек. Каждая единица информации однозначно определяется глобальным
идентификатором, таким как URL. Каждый URL в свою очередь имеет строго
заданный формат
HTTP — это протокол. SOAP — тоже протокол. А REST — стиль. Для того,
чтобы разобраться, что это за стиль такой и в чем его «соль» я бы
порекомендовал вам прочитать старенькую, но достаточно понятную статью
на Хабре по названием «Архитектура REST».
Пусть вас не пугает то, что «API реализовано на REST-принципах». Для
реализации таких API в Delphi опять же все есть. Главное знать в каком
формате сервер присылает данные. Если данные приходят в виде XMLдокументов, то вам достаточно опять же двух компонентов: для работы с
HTTP и для работы с XML. Если же данные возвращаются в формате JSON,
то тут возможности Delphi сейчас по-богаче:
1. Если Вы обладатель Delphi XE5-XE7, то в Delphi есть целая библиотека
- REST Client Library для реализации таких API.
2. Можно использовать любой компонент для работы с HTTP и, опять же,
целый ряд компонентов для работы с JSON — это и стандартная (с версии
Delphi 2010) библиотека для JSON и SuperObject и ещё несколько библиотек
для работы с JSON в Delphi.
Определившись с набором компонентов для дальнейшей работы, можно
приступать…к дальнейшему чтению документации.
Авторизация и получение доступа к API
Ключевой момент реализации любого API — авторизация пользователя.
Как уже было сказано ранее, в настоящее время наиболее активно
используется авторизация пользователей по протоколу OAuth, а точнее —
OAuth 2.0. Чтобы авторизовать пользователя по OAuth вы можете написать
свой собственный класс или же, если Вы используете Delphi XE5-XE7, то
можете воспользоваться компонентами REST Client Library. Чтобы не
повторяться о том, как это сделать, я просто приведу здесь ссылки на статьи
где рассматривалась авторизация по OAuth в разных онлайн-сервисах:
1. Серия статей про OAuth в Google:
1. Google API в Delphi. OAuth для Delphi-приложений,
2. Google API в Delphi. Обновление модуля для OAuth,
3. Решение проблем с Google OAuth 2.0. для Win-приложений,
4. Тестирование запросов к API Google средствами Delphi. Компонент
OAuthClient для Delphi XE — XE3.
2. Использование REST Client Library для OAuth:
1. Delphi XE5: REST Client Library,
2. Delphi: авторизация по OAuth 2.0 в Dropbox своими силами,
3. REST Client Library: использование API ВКонтакте
Однако встречаются и такие API в которых авторизация пользователя
проводится по собственным протоколам и правилам. В этом случае вам
необходимо самостоятельно реализовать процедуру авторизации (для чего
необходимо знать всё то, о чем сказано в первой части статьи). Рассмотрим
пример работы с подобными API.
Пример с etxt.ru
Начинаем читать документацию. Что нам говорит сервис:
В каждом запросе должен присутствовать набор обязательных параметров.
Также для каждой функции в ее документации определены дополнительные
параметры, нужные только для этой функции. Текстовые значения параметров
должны быть преданы в кодировке UTF-8. Одинаковые для всех функций
параметры:
1. method
(string)
—
название
вызываемого
метода,
например, users.getList; обязательный параметр
2. sign (string) — подпись запроса; обязательный параметр
3. token (string) — API-ключ текущего пользователя
Порядок следования параметров в запросе значения не имеет, порядок
параметров важен только при расчете подписи.
API-ключ token уникален для каждого пользователя и его можно узнать в
разделе «Мой профиль/Настройка интерфейса».
Подпись
sign
расcчитывается
по
алгоритмe,
приведенному
ниже. Подписываются только параметры, переданные по GET.
Важные моменты в этой части документации выделены жирным:
1. Все текстовые параметры запроса передаются в кодировке UTF-8
2. Для каждого запроса нам необходимо рассчитывать по специальному
алгоритму подпись.
3. Порядок следования параметров в самом запросе не важен
4. При расчёте подписи все параметры должны следовать в строго
определенном порядке.
Что это всё значит? Ну, с кодировкой, допустим, всё понятно. В остальном же
получается, что доступ к API предоставляется нам только, если мы передадим
на сервер два верных параметра — это token (он не меняется и получается при
регистрации пользователя) и sign — подпись, которая меняется при каждом
запросе. Следовательно, нам необходимо в своей программе предусмотреть
специальный метод, который будет рассчитывать нам эту подпись. Попробуем
написать такой метод.
Снова смотрим документацию. Вот, что говорит нам сервис про алгоритм
подписи запроса:
Алгоритм использует отдельный ключ api_pass, который мы настоятельно
рекомендуем вам хранить только на ваших серверах и использовать только
при запросах с них к серверу Биржи. Данный ключ задается в разделе «Мой
профиль/Настройки интерфейса».
sign = md5(params.md5(api_pass.'api-pass'))
Значение params — это конкатенация пар «имя=значение» отсортированных
в алфавитом порядке по «имя», где «имя» — это название параметра,
передаваемого в функцию API, «значение» — значение параметра.
Разделитель в конкатенации не используется. Параметр sign при расчете
подписи не учитывается, все остальные параметры запроса должны
учитываться при расчете.
Теперь попробуем составить алгоритм расчёта такой подписи. Итак, что нам
нужно:
1. Найти в ключ api_pass (он как и token нам выдается сервисом один раз и на
всю жизнь)
2. Необходимо отсортировать все параметры запроса в алфавитном порядке
3. Необходимо произвести конкатенацию, т.е. «склеивание» параметров
4. Рассчитать MD5 для строки api_pass.’api-pass’
5. Полученную в п.3 строку «склеить» с результатов п.4
6. Рассчитать для полученной в п.5 строки MD5- это и будет наша подпись.
Реализуем этот алгоритм в Delphi.
Для работы я буду использовать только те компоненты и классы, которые есть
в поставкеDelphi XE7 — это библиотека Indy и библиотека для работы с JSON,
которая в XE7 находится в модуле System.JSON.
Так как в дальнейшем предстоит использовать этот API, то я создал отдельный
класс, который постепенно будет «обрастать» новыми методами и свойствами
для работы с API. Класс этот вынесен в отдельный модуль и на данном этапе
выглядит так:
type
TEtxtAPI = class
private
FToken: string;
FApiPass: string;
public
procedure SignRequest(AParams: TStringList);
property Token: string read FToken write FToken;
property ApiPass: string read FApiPass write FApiPass;
end;
Свойства Token и ApiPass — это ключ доступа и пароль к API, которые, как
мы уже выяснили никогда не меняются. Теперь рассмотрим метод SignRequest,
который будет вычислять подпись и добавлять её к параметрам запроса:
procedure TEtxtAPI.SignRequest(AParams: TStringList);
var Params: string;
I: Integer;
md5indy: TIdHashMessageDigest;
hashPass: string;
begin
if not Assigned(AParams) then Exit;
//удаляем подпись, если она есть в параметрах
if AParams.IndexOfName('sign')>-1 then
AParams.Delete(AParams.IndexOfName('sign'));
//проверяем наличие ключа token и добавляем его в запрос
if AParams.Values['token']=EmptyStr then
AParams.Values['token']:=FToken;
//сортируем все параметры
AParams.Sort;
//конкатенация пар
for I := 0 to AParams.Count-1 do
Params:=Params+AParams[i];
md5indy:=TIdHashMessageDigest5.Create;
try
//рассчитываем md5 пароля
hashPass:=md5indy.HashStringAsHex(FApiPass+'api-pass');
//расчитываем md5 всей подписи и записываем её в параметры
AParams.Values['sign']:=LowerCase(md5indy.HashStringAsHex(params+LowerCase(hashPa
finally
md5indy.Free;
end;
end;
Так как параметр sign не участвует в расчёте подписи, то вначале мы
проверили есть ли такой параметр в списке и, если sign присутствует, то
удалили его. Далее мы проверили наличие параметра token и, при
необходимости, добавили его в список. После этого мы отсортировали весь
список параметров в алфавитном порядке и составили строку params. Для
расчёта
MD5
класса
мы
воспользовались
TIdHashMessageDigest,
который
возможностями
находится
в
модуле IdHashMessageDigest. Расчёт производился в два шага:
1. Рассчитали хэш пароля API.
2. Полученный хэш добавили к строке params и рассчитали новый хэш для
полученной строки
После
этого
добавили
подпись
в
параметр
sign
запроса.
Теперь
список AParams содержит все необходимые параметры для выполнения
запроса к API. Как проверить, что рассчитанная подпись верная? Очень просто
— попробовать выполнить какой-нибудь простой запрос к API.
Выполнение запросов к API
Выполнив авторизацию и получив доступ к API мы можем выполнять
различные запросы к API. Различные API требуют могут предъявлять разные
требования к выполнению запросов. И прежде, чем начинать писать код в
Delphi, опять же, следует внимательно прочитать документацию к API и
определиться с тем как могут выглядеть различные запросы к одному и тому
же API.
В URL запроса всегда присутствует общая для всех запросов часть. Так,
например, если используется API, использующий REST-принципы (любой
API Яндекса, Google, ВКонтакте и т.д.), то запросы к такому серверу могут
иметь следующий вид:

http://example.com/api/book/1

http://example.com/api/lists/

http://example.com/api/authors/123

и т.д.
Видите? В каждом из запросов есть http://example.com/api/. В различной
документации к API этот URL может называться по-разному: точка доступа,
Base URL или просто URL запроса. Base URL всегда следует выносить в
раздел констант. Объясню почему это стоит делать. Причин две:
1. Для того, чтобы не использовать в дальнейшем в своем коде одну и ту же
строку по 100 раз и избегать случайных опечаток, которые потом довольно
сложно обнаружить в большом массиве кода
2. Редко, но тем не менее встречается ситуация, когда сервер изменяет Base
URL полностью или частично. Если произойдет смена Base URL, например,
в новой версии API, то вам будет достаточно изменить всего одну константу
в коде.
Определившись с Base URL можно начать реализовывать выполнение
запросов к API в Delphi. Рассмотрим это, опять же, на примере API etxt.ru.
Пример выполнения запросов к etxt.ru
Определяем Base URL. В случае с etxt.ru этот URL указан в документации и
выглядит так:
https://www.etxt.ru/api/json/
Этот URL не изменяется — изменяются только параметры запроса. Так,
например, запрос к списку категорий может выглядеть так:
https://www.etxt.ru/api/json/?token=12345&method=categories.listCategories&sign=1234fde4567ef
при
запросе
списка
папок
запрос
будет
таким:
https://www.etxt.ru/api/json/?token=12345&method=folders.listFolders&sign=1dnt34dde4567ee
То есть, наша константа в Delphi может выглядеть так:
const
cBaseURL = 'https://www.etxt.ru/api/json/?%s';
Так как сервер требует доступа по https, то для дальнейшей работы нам
потребуются два компонента Indy:
TidHTTP и TIdSSLIOHandlerSocketOpenSSL, которые находятся,
соответственно, в модулях idHTTP иidSSLOpenSSL. Так же нам потребуются
две динамические библиотеки: libeay32.dll и ssleay32.dll, которые вы
можете скачать со страницы с исходниками в блоге webdelphi.ru.
Добавим TidHTTP и TIdSSLIOHandlerSocketOpenSSL в наш класс для работы
с API:
type
TEtxtAPI = class
private
FHTTP: TidHTTP;
FSSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
FToken: string;
FApiPass: string;
public
constructor Create;
destructor Destroy;override;
procedure SignRequest(AParams: TStringList);
property Token: string read FToken write FToken;
property ApiPass: string read FApiPass write FApiPass;
end;
constructor TEtxtAPI.Create;
begin
inherited;
FSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
FHTTP := TIdHTTP.Create(nil);
FHTTP.IOHandler := FSSLIOHandler;
FHTTP.HandleRedirects := True;
end;
destructor TEtxtAPI.Destroy;
begin
FHTTP.Free;
FSSLIOHandler.Free;
inherited;
end;
Динамические библиотеки необходимо положить в папку с exe-файлом
приложения. Теперь напишем новый метод нашего класса, который будет
выполнять GET-запрос на сервер и возвращать ответ. С учётом того, что у нас
уже написана процедура подписи запроса, наш новый метод может выглядеть
так:
function TEtxtAPI.GET(const AParams: TStringList): string;
begin
SignRequest(AParams);
AParams.Delimiter:='&';
FHTTP.Disconnect;
Result:=FHTTP.Get(Format(cBaseURL,[AParams.DelimitedText]));
end;
Метод получает на входе список параметров, затем подписывает запрос,
отправляет его на сервер и записывает полученный ответ в Result. Проверим
работу
нашего
метода.
Для этого создадим новое приложение VCL, подключим в uses модуль с
нашим классом, а на главную форму бросим всего два компонента
— TButton и TMemo:
В обработчике OnClick кнопки напишем следующий код:
procedure TForm3.Button1Click(Sender: TObject);
var Params: TStringList;
Etxt:TEtxtAPI;
begin
//создаем список для записи параметров
Params:=TStringList.Create;
try
//записываем обязательный параметр - method
Params.Values['method']:='categories.listCategories';
//создаем объект для работы с API
Etxt:=TEtxtAPI.Create;
try
Etxt.Token:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_TOKEN';
Etxt.ApiPass:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_ПАРОЛЬ';
//выполняем запрос и записываем результат в Memo
Memo1.Lines.Text:=Etxt.GET(Params);
finally
//освобождаем память
Etxt.Free;
end;
finally
//освобождаем память
Params.Free;
end;
end;
Если наша подпись была рассчитана верно, то в результате мы должны
получить в Memo JSON-объект с данными по категориям. Запускаем
приложение,
кликаем
по
кнопке
и
видим
следующий
результат:
Результат получен, следовательно, можно приступать к следующему шагу
работы над API — разбору результатов запроса.
Парсинг результатов запроса
На предыдущем шаге работы с API мы получили от сервера «сырые» для
будущего приложения
данные. То есть на данный момент ни наше
приложение ни наш класс для работы с API «не знают» что делать с данными
— это простая строка, которую необходимо правильно разобрать и
представить пользователю приложения.
В своей работе с самыми различными API я придерживаюсь следующих двух
положений:
1. Один класс используется непосредственно для обмена данными с сервером:
в этом классе реализованы методы выполнения GET-, POST-, DELETE- и
других запросов к API по HTTP(S). Результатом выполнения таких
методов всегда является строка.
2. Второй класс используется для работы с объектами и методами API — здесь
уже
реализуются
конкретные
методы
API
(из
документации),
производится парсинг данных и т.д. Этот класс использует методы и
свойства первого и может быть как дочерним, так и отдельным классом.
Мне такая схема работы представляется наиболее удобной в плане отладки.
Вы же в своих приложениях вольны делать как угодно.
Для разбора ответов сервера, как я упоминал в первой части статьи, Вы
должны понимать, хотя бы, что такое XML и JSON и как их можно разобрать
в Delphi. Данные, полученные от сервера, внутри своей программы вы можете
хранить и представлять как вам угодно — хранить в виде простой строки,
создавать свои собственные классы, записи и т.д. Внутри вашего
приложения — вы хозяин.
Опять же (и я не устану это повторять), прежде чем писать код необходимо
прочитать документацию по API. На этот раз надо изучить то:
1. какие свойства содержат возвращаемые объекты и типы данных этих
свойств
2. какие общие свойства есть у всех объектов API (и есть ли такие общие
свойства, в принципе).
Если упустить этот момент, то в итоге вы можете сильно «раздуть» свой код
повторяющимися свойствамиродственных объектов. Например, в API
Box.com можно встретить объекты Folder (папка) и MiniFolder (краткая
информация о той же папке). В этом случае лучше всего в Delphi сделать
класс TFolder дочерним от TMinifolder — упростит, в дальнейшем, отладку
приложения, сократит код и, плюс, поможет избежать ошибок при парсинге.
Чем просматривать ответы сервера? Если данные приходят в JSON, то могу
вам порекомендовать использовать онлайн-сервис http://jsonviewer.stack.hu/.
Вот как выглядит в этом сервисе объект, полученный в предыдущем примере:
Как видно на рисунке, все поля объектов представляют из себя обычные
строки. Разобрать такой объект будет достаточно просто.
Для примера, рассмотрим как можно разбирать, хранить и представлять
данные от сервера etxt.ru.
Разбор данных etxt.ru
Итак, класс для работы с сервером по HTTP у нас есть (впоследствии мы
можем добавить в него, например, метод выполнения POST-запроса к серверу
или любой другой по необходимости) — его содержимого нам пока хватит для
реализации разных методов API.
В предыдущем примере мы получили большой JSON-объект с тематическими
категориями.
Посмотрим
из
чего
состоит
объект
категории.
Читаем документацию:
categories.listCategories
Возвращает список тематических категорий заказов/статей, отсортированный по названию
категории.
Результат
Поле
Описание
id_category Идентификатор категории
id_parent
Идентификатор родительской категории
name
Название категории
keyword
Ключевое слово категории
На языке Delphi это может быть, например, такой класс:
TCategory = class
private
FId: string;
FParentID: string;
FName: string;
FKeyword: string;
public
procedure Parse(AValue: TJsonValue);
property Id: string read Fid;
property ParentID: string read FParentID;
property Name: string read FName;
property Keyword: string read FKeyword;
end;
Все поля класса доступны только для чтения, т.к. в API нет методов
создания/редактирования категорий, а следовательно и мы изменять значения
полей
не
будем.
Метод Parse производит разбор JSON-объекта категории и записывает
полученные значения в поля класса, т.е. вParse должен передаваться объект,
следующего
{"id_category"
содержания:
:
"1911",
"id_parent"
\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f
:
"0",
"name"
:
"--
\u043d\u0435
\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430", "keyword" : "not", "type" :
"object"
}
Метод Parse выглядит следующим образом:
function PairIndex(const AName: string; APairNames: array of string):integer;
var
I: Integer;
begin
for I := Low(APairNames) to High(APairNames) do
if SameText(AName, APairNames[i]) then
Exit(I);
Result:=-1;
end;
procedure TCategory.Parse(AValue: TJsonObject);
const
cPairs: array [0..3]of string = ('id_category','id_parent','name','keyword');
var I: Integer;
APairName: string;
begin
if not Assigned(AValue) then Exit;
for I := 0 to AValue.Count-1 do
begin
APairName:=AValue.Pairs[i].JsonString.Value;
case PairIndex(APairName, cPairs) of
0:FId:=AValue.Values[APairName].Value;//id_category
1:FParentID:=AValue.Values[APairName].Value;//id_parent
2:FName:=AValue.Values[APairName].Value;//name
3:FKeyword:=AValue.Values[APairName].Value;//keyword
end;
end;
end;
Внимание: этот код будет работать безошибочно только в Delphi XE5-XE7.
Для
более
ранних
версий
Delphi
вам
вместо
свойств Values и Pairs у TJsonObject надо использовать перечислители. О том
как их использовать можно посмотреть вот в этой статье про API ВКонтакте.
В этом методе мы разобрали один объект одной категории, а сервер в ответе
присылает нам набор объектов разных категорий. Разберемся как и где
парсить весь ответ сервера.
Как я сказал выше — для реализации методов API я, обычно, пишу отдельный
класс. Самое время начать его писать для etxt.ru. Новый класс можно
представить следующим образом:
TEtxtClient = class(TEtxtAPI)
private
FCategories: TObjectList;
FLastError: string;
function GetCatCount: integer;
function GetCategory(AIndex:integer):TCategory;
function GetJson(const AResponseString: string):TJSONObject;
public
constructor Create;
destructor Destroy;override;
procedure ListCategories;
property LastError: string read FLastError;
property CatCount: integer read GetCatCount;
property Category[AIndex: integer]:TCategory read GetCategory;
end;
При этом в родительском классе TEtxtAPI методы отправки/получения
данных по HTTP я перенес в секциюprotected:
type
TEtxtAPI = class
private
[...]
protected
function GET(const AParams: TStringList):string;
public
[...]
end;
Посмотрим из чего состоит класс TEtxtClient.
FCategories: TObjectList;
Список в котором будут храниться объекты категорий. Свойства:
property CatCount: integer read GetCatCount;
property Category[AIndex: integer]:TCategory read GetCategory;
Возвращают, соответственно, количество категорий в списке и объект
категории с индексом AIndex в списке.
Свойство
property LastError: string read FLastError;
Хранит информацию по последней ошибке доступа к API. Об ошибках и
исключениях при работе с API мы поговорим позже.
Метод:
procedure ListCategories;
Выполняет одноименный метод API (см. документацию сервиса), производит
разбор полученного JSON-объекта и заполняет список FCategories.
Посмотрим на него подробнее:
procedure TEtxtClient.ListCategories;
var AJson: TJSONObject;
AValue: TJsonValue;
AParams: TStringList;
AResponse: string;
I: Integer;
begin
//очистили список от результатов предыдущего запроса
FCategories.Clear;
AParams:=TStringList.Create;
try
//записали параметр method - остальные обязательные параметры запишутся при
//расчёте подписи к запросу в методе TEtxtAPI.SignRequest
AParams.Values['method']:='categories.listCategories';
//отправили запрос на сервер и получили "сырую" строку с JSON
AResponse:=GET(AParams);
//попробовали получить JSON-объект из строки
AJson:=GetJson(AResponse);
//не удалось получить объект, значит сервер вернул ошибку
if not Assigned(AJson) then
raise Exception.Create(FLastError);
try
//проходим по каждой паре в полученном JSON-объекте
for I := 0 to AJson.Count-1 do
begin
//получили значение пары
AValue:=AJson.Values[AJson.Pairs[i].JsonString.Value];
//значение пары является объектом
if AValue is TJSONObject then
begin
//добавляем в список новый объект категории
FCategories.Add(TCategory.Create);
//разбираем объект категории
FCategories.Last.Parse(AValue as TJSONObject);
end;
end;
finally
AJson.Free;
end;
finally
AParams.Free;
end;
end;
Чтобы было понятнее, что мы делали с JSON-объектом в этой процедуре, я
представлю
процесс
разбора
в
виде
картинки:
Теперь мы можем переписать код нашей программы (обработчик OnClick
кнопки) и выводить в Memo уже не «сырой» ответ сервера, а, например, имена
категорий:
procedure TForm3.Button1Click(Sender: TObject);
var Etxt:TEtxtClient;
I: Integer;
begin
Etxt:=TEtxtClient.Create;
try
Etxt.Token:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_TOKEN';
Etxt.ApiPass:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_ПАРОЛЬ';
//запросили с сервера список категорий
Etxt.ListCategories;
//прошли по списку и вывели имена категорий в Memo
for I := 0 to Etxt.CatCount-1 do
Memo1.Lines.Add(Etxt.Category[i].Name)
finally
Etxt.Free;
end;
end;
Сравните этот код с представленным ранее в части «Пример выполнения
запросов к etxt.ru» — код стал короче и понятнее.
Выполнение взаимосвязанных методов
Чтобы закрепить материал, реализуем ещё пару связанных методов — когда
от результата выполнения одного метода (или от значения какого-либо
параметра из данных предыдущего запроса) зависит результат выполнения
другого.
Например, попробуем получить список исполнителей и подробную
информацию по выбранному пользователю. Если мы не можем получить
список, то следовательно и получить подробную информацию по отдельному
пользователю мы не получим. Если мы не получим из списка верные значения
свойств объекта пользователя, то и результатом выполнения связанного
метода будет ошибка.
Снова
смотрим
документацию.
Получить
список
пользователей
(исполнителей) можно, выполнив метод users.getList. Этот метод, в отличие от
предыдущего, требует указания нескольких параметров, а именно:

count (integer) Число пользователей для выборке, не более 100 за запрос (по
умолчанию)

from (integer) Смещение от последней записи в выборке, по умолчанию 0

rate_from (integer) Фильтрация по рейтингу, начиная с данного значения

rate_out (integer) Фильтрация по рейтингу, заканчивая данным значением

online (integer) Флаг онлайн статуса на бирже, 1 — онлайн, 0 — офлайн, по
умолчанию все вместе
Соответственно, и новый метод в классе TEtxtClient должен будет на входе
принимать эти параметры. Например, можно создать такой метод:
procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut, AOnline: integer);
А можем, для повышения читабельности нашего кода и избежания
неправильного указания последнего параметра сделать так:
procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut:integer; AOnline: boolean);
Как вам будет угодно. Как по мне, так более удобно использовать второе
описание, т.е. когда статус пользователя передается в метод как
значение boolean.
Теперь посмотрим, какие объекты должен вернуть нам сервер. А сервер нам
вернет опять же объект в котором каждая пара будет представлять из себя
объект одного пользователя. JSON-объект будет выглядеть таким образом:
Следовательно, т.к. мы можем в дальнейшем попросить у сервера подробную
информацию на любого пользователя, то стоит сохранить полученный список
в программе. Снова создаем новый класс, который будет хранить информацию
о пользователе:
TUser = class
private
Fid: integer;
FLogin: string;
FFio:string;
FDescription: string;
FCountry: string;
FCity: string;
FOnline: boolean;
FRegdate: TDateTime;
FRate: integer;
public
procedure Parse(AValue: TJSONObject);virtual;
property Id: integer read FId;
property Login: string read FLogin;
property Fio:string read FFio;
property Description: string read FDescription;
property Country: string read FCountry;
property City: string read FCity;
property Online: boolean read FOnline;
property Regdate: TDateTime read FRegdate;
property Rate: integer read FRate;
end;
Метод Parse выглядит практически также, как и в классе TCategory:
procedure TUser.Parse(AValue: TJSONObject);
const
cPairs: array [0..8]of string = ('id_user','login','fio','description','country','c
ate','rate');
var I: Integer;
APairName: string;
begin
if not Assigned(AValue) then Exit;
for I := 0 to AValue.Count-1 do
begin
APairName:=AValue.Pairs[i].JsonString.Value;
case PairIndex(APairName, cPairs) of
0:Fid:=StrToInt(AValue.Values[APairName].Value);//'id_user'
1:FLogin:=AValue.Values[APairName].Value;//'login'
2:FFio:=AValue.Values[APairName].Value;//'fio'
3:FDescription:=AValue.Values[APairName].Value;//'description'
4:FCountry:=AValue.Values[APairName].Value;//'country'
5:FCity:=AValue.Values[APairName].Value;//'city'
6:FOnline:=AValue.Values[APairName].Value='1';//'online'
7:FRegdate:=UnixToDateTime(StrToInt(AValue.Values[APairName].Value));//'regda
end;
end;
end;
Теперь допишем класс TEtxtClient и реализуем в нем метод API users.getList:
TEtxtClient = class(TEtxtAPI)
private
[...]
FUsers: TObjectList;
[...]
function GetUserCount: integer;
function GetUser(AIndex: integer):TUser;
public
[...]
procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline: boolean);
[...]
property UserCount: integer read GetUserCount;
property User[AIndex: integer] read GetUser;
end;
Метод ListUsers можно представить так:
procedure TEtxtClient.ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline:
var AJson: TJSONObject;
AValue: TJsonValue;
AParams: TStringList;
AResponse: string;
I: Integer;
begin
AParams:=TStringList.Create;
try
AParams.Values['method']:='users.getList';
AParams.Values['count']:=IntToStr(ACount);
AParams.Values['from']:=IntToStr(AFrom);
AParams.Values['rate_from']:=IntToStr(ARateFrom);
AParams.Values['rate_out']:=IntToStr(ARateOut);
if AOnline then
AParams.Values['online']:='1'
else
AParams.Values['online']:='0';
AResponse:=GET(AParams);
if AResponse.IsEmpty then
Exit;
AJson:=GetJson(AResponse);
if not Assigned(AJson) then
raise Exception.Create(FLastError);
try
for I := 0 to AJson.Count-1 do
begin
AValue:=AJson.Values[AJson.Pairs[i].JsonString.Value];
if AValue is TJSONObject then
begin
FUsers.Add(TUser.Create);
FUsers.Last.Parse(AValue as TJSONObject);
end;
end;
finally
AJson.Free
end;
finally
AParams.Free;
end;
end;
В этом методе чистить список FUsers не надо, т.к. сервер может вернуть
вам максимум 100 пользователей за 1 запрос, а на деле их больше 500 000 и,
если вы вдруг решите, что вам нужен список всех пользователей, то придётся
выполнять этот метод несколько раз (до тех пор пока сервер не вернет вам
всех) и в этом случае очистка списка внутри приведет к тому, что в конце
концов вы сохраните только 100 последних пользователей. Для очистки
списка лучше предусмотреть отдельный метод класса:
procedure TEtxtClient.ClearUserList;
begin
FUsers.Clear;
end;
Перейдем к следующему методу API — получению подробной информации
по конкретному пользователю. Подробную информацию по конкретному
пользователю возвращает нам метод users.getUser. Снова смотрим в
документацию. Методу требуются следующие параметры:

id (integer) — Идентификатор пользователя, полуобязательный параметр,
имеет приоритет над параметром login

login (string) Логин пользователя, полуобязательный параметр, может
указываться при отсутствии параметра id
При успешном выполнении запроса сервер вернет нам информацию по
пользователю, включающую в себя всю информацию из класса TUser,
который мы уже написали и плюс к этому следующие свойства:

photo — путь до аватара пользователя

group — название группы пользователя

works — виды указанных пользователем работ, только для исполнителя

categories — категории, указанные пользователем, только для исполнителя

portfolio — число работ в портфолио, только для исполнителя
Объект подробной информации о пользователе может выглядеть вот так:
Как видно из рисунка, представленный объект уже имеет вложенные объекты,
такие как works и categories. Более того, в объекте categories содержаться не
объекты категорий или их названия, а только их id, то есть для вывода
названий категорий пользователя нам предварительно надо будет загрузить их
список, выполнив уже имеющийся метод ListCategories, а потом проводить по
полученному списку поиск категории с необходимым id (как это делать я
покажу позднее).
Теперь обратим внимание на вложенный объект works. Об этом объекте в
документации нет ни слова (такое тоже часто встречается). Чтобы понять , что
означает поле work можно посмотреть, например, несколько профилей разных
пользователей и определить, что это поле содержит следующие значения:
1. 1 — Копирайтинг
2. 2 — Рерайтинг
3. 3 — Перевод
4. 4 — SEO-копирайтинг
Других
вариантов
нет.
Теперь подумаем, как представить новый класс в Delphi? Представить новый
класс можно, например, так:
TUserInfo = class(TUser)
private
FPhoto: string;
FGroup: string;
FWorks: TStrings;
FCategories: TStrings;
FPortfolio: integer;
function GetWorksCount: integer;
function GetCatCount: integer;
function GetCategoryID(AIndex: integer): string;
function GetWork(AIndex:integer):string;
public
constructor Create;
destructor Destroy;override;
procedure Parse(AValue: TJSONObject);override;
property Photo: string read FPhoto;
property Group: string read FGroup;
property Work[AIndex:integer]: string read GetWork;
property CategoryID[AIndex:integer]: string read GetCategoryID;
property Portfolio: integer read FPortfolio;
property WorkCount:integer read GetWorksCount;
property CatCount: integer read GetCatCount;
end;
Как видите, так как в подробной информации повторяется вся информация
из TUser, то новый класс сделан дочерним для TUser, а метод Parse теперь
стал override и выглядит следующим образом:
procedure TUserInfo.Parse(AValue: TJSONObject);
procedure ParseCategories(ACategoryObject: TJSONObject);
var i:integer;
AName: string;
begin
for I := 0 to ACategoryObject.Count-1 do
begin
AName:=ACategoryObject.Pairs[i].JsonString.Value;
if SameText(AName,'type') then
continue;
FCategories.Add(ACategoryObject.Values[AName].Value);
end;
end;
procedure ParseWorks(AWorksObject: TJSONObject);
const
cWorks: array [1..4] of string = ('Копирайтинг','Рерайтинг','Перевод','SEO-копирайт
var i:integer;
AName: string;
AWork: TJSONObject;
AWorlID: integer;
begin
for I := 0 to AWorksObject.Count-1 do
begin
AName:=AWorksObject.Pairs[i].JsonString.Value;
if SameText(AName,'type') then
continue;
AWork:=AWorksObject.Values[AName] as TJSONObject;
AWorlID:=StrToInt(AWork.Values['work'].Value);
if (AWorlID>0)and(AWorlID<5) then
FWorks.Add(cWorks[AWorlID])
else
FWorks.Add('Неизвестная работа')
end;
end;
const
cPairs: array [0..4]of string = ('photo','group','works','categories','portfolio');
var I: Integer;
APairName: string;
begin
//выполняем родительский метод Parse
//заполняем поля id, fio и т.д.
inherited Parse(AValue);
//парсим оставшиеся поля
if not Assigned(AValue) then Exit;
for I := 0 to AValue.Count-1 do
begin
APairName:=AValue.Pairs[i].JsonString.Value;
case PairIndex(APairName, cPairs) of
0:FPhoto:=AValue.Values[APairName].Value;//'photo'
1:FGroup:=AValue.Values[APairName].Value;//'group'
2:ParseWorks(AValue.Values[APairName] as TJSONObject);//'works'
3:ParseCategories(AValue.Values[APairName] as TJSONObject);//'categories'
4:FPortfolio:=StrToInt(AValue.Values[APairName].Value);//'portfolio'
end;
end;
end;
Этот метод выглядит по-сложнее, чем ранее рассмотренные и, плюс ко всему,
имеет вложенные методы. На самом деле всё тут не так уж и сложно.
Вложенный
пользователя,
метод
т.е.
ParseCategories
разбирает
на
получает
входе
он
объект
вот
этот
с
категориями
JSON-объект:
При этом, проверяется условие
if SameText(AName,'type') then
continue;
То есть, если имя очередной пары type, то она пропускается, т.к. не содержит
информации о id категории.
Аналогичным образом работает и метод ParseWorks. Только здесь
используются вот эти объекты:
При этом в цикле мы получаем очередной объект работы:
AWork:=AWorksObject.Values[AName] as TJSONObject;
и вытаскиваем из этого объекта, только значение пары work:
AWorlID:=StrToInt(AWork.Values['work'].Value);
Так как мы не знаем будет ли в сервис добавляться новые виды работ для
исполнителей, то дополнительно проверяем значение AWorkId и, если это
значение укладывается в интервал от 1 до 4 (известные значения), то
записываем название работы, если нет — записываем в список строку
«Неизвестная работа»:
if (AWorlID>0)and(AWorlID<5) then
FWorks.Add(cWorks[AWorlID])
else
FWorks.Add('Неизвестная работа')
С парсингом объекта разобрались. Теперь добавляем очередной метод в
класс TEtxtClient.
TEtxtClient = class(TEtxtAPI)
private
[...]
FUserInfo: TUserInfo;
[...]
function GetUserInfo2(AUser: TUser): TUserInfo;overload;
function GetUserInfo(ALogin: string):TUserInfo;overload;
public
[...]
property UserInfo2[AUser:TUser]:TUserInfo read GetUserInfo2;
property UserInfo[ALogin: string]:TUserInfo read GetUserInfo;
end;
Здесь для примера я создал два метода получения подробной информации о
пользователе и для работы использую свойства:
property UserInfo2[AUser:TUser]:TUserInfo read GetUserInfo2;
property UserInfo[ALogin: string]:TUserInfo read GetUserInfo;
Посмотрим как работают методы:
function TEtxtClient.GetUserInfo(ALogin: string): TUserInfo;
var AJson: TJSONObject;
AParams: TStringList;
AResponse: string;
begin
AParams:=TStringList.Create;
try
AParams.Values['method']:='users.getUser';
AParams.Values['login']:=ALogin;
//выполнили запрос
AResponse:=GET(AParams);
//проверили ответ
if AResponse.IsEmpty then
Exit;
//получили JSON-объект
AJson:=GetJson(AResponse);
//проверили на ошибку
if not Assigned(AJson) then
raise Exception.Create(FLastError);
try
//объект содержит поля
if AJson.Count>0 then
begin
if not Assigned(FUserInfo) then
FUserInfo:=TUserInfo.Create;
//парсим ответ. В возвращаемом объекте есть информация только по одному польз
ребуется
FUserInfo.Parse(AJson.Values[AJson.Pairs[0].JsonString.Value] as TJSONObject)
Result:=FUserInfo;
end
else
Result:=nil;
finally
AJson.Free;
end;
finally
AParams.Free;
end;
end;
function TEtxtClient.GetUserInfo2(AUser: TUser): TUserInfo;
begin
Result:=GetUserInfo(AUser.Login);
end;
Как видите, второй метод самый простой и состоит всего из одной строки в
которой
мы
получаем
результат
GetUserInfo.
Теперь у нас есть все необходимые методы, чтобы написать полноценное
приложение.
Готовое приложение для работы со
списком пользователей etxt.ru
На данный момент мы реализовали три метода API:
1. Получение списка категорий
2. Получение списка пользователей
3. Получение подробной информации о пользователе
Этих трех методов вполне достаточно, чтобы написать простенькое
приложение для работы со списками пользователей сайта etxt.ru. Пусть
интерфейс приложения будет таким:
Приложение будет работать следующим образом:
1. Задаем параметры поиска (рейтинг пользователей, онлайн-статус).
2. Жмем поиск — получаем с сервера всех пользователей, удовлетворяющих
параметрам поиска
3. Все найденные пользователи выводятся в список слева
4. Выбираем пользователя в списке — получаем подробную информацию о
пользователе (функция GetUserInfo2)
5. Выводим подробную информацию на форму
Что нужно предусмотреть при работе приложения:
1. Как мы уже определили, когда писали метод получения подробной
информации, для тематических категорий пользователя возвращаются
только их ID. Следовательно, ещё ДО запроса подробной информации у нас
под рукой должен быть список всех категорий (должна выполнится
процедура ListCategories)
2. На данный момент функция ListUsers может возвращать максимум 100
пользователей
за
раз
(это
ограничение
самого
сервиса
etxt.ru).
Следовательно надо написать метод, который будет работать с ListUsers и
возвращать всех пользователей
3. Необходимо предусмотреть способ прерывания поиска пользователей.
Начнем реализовывать все функции программы по порядку.
Подключаем в 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;
Здесь стоит отметить следующий момент:
Сейчас я пишу приложение-пример работы с API, поэтому могу позволить
себе упустить некоторые проверки, которые в рабочем приложении
необходимо проводить. Так, приведенный выше код вполне рабочий, НО, в
рабочем приложении я бы перед выполнением метода ListCategories, как
минимум проверил бы наличие доступа в Интернет.
Итак, первый момент работы программы выполнен — у нас имеется список
категорий. Двигаемся далее.
Получаем с сервера всех пользователей,
удовлетворяющих параметрам поиска
На момент написания этой статьи количество пользователей etxt.ru составляло
более 500 000. Не все, конечно, активные, но тем не менее. Если
предположить, что мы зададим такие параметры поиска при которых сервер
нам должен будет вернуть всех пользователей, то мы можем получить
ситуацию в которой наше приложение «подвиснет» серьезно и на долго. Так,
если предположить, что сервер будет нам отвечать за 0,5 сек., а парсинг
результатов будет вообще практически моментальным, то на запрос всей базы
пользователей у нас уйдет больше 40 минут.
Обычно для таких ситуаций я предусматриваю отдельный поток в
приложении, но для примера мы обойдемся более простым решением. У нас
имеется переменная ACancel — именно её значение будет нам говорить о том,
надо ли прерывать процесс получения списка пользователей или нет, а вся
работа приложения будет проходить в одном (основном потоке).
Теперь ещё раз посмотрим на метод ListUsers, которым мы сейчас будем
пользоваться:
function ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline: boolean):int
Как нам отвечает сервер, при задании разных значений входных параметров
функции?
Нумерация пользователей в базе сервера начинается с нуля. Следовательно,
если мы задаем такие параметры:
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)
o
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.Checke
//начинаем выводить список
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 абсолютно те же самые, что и в
рассматриваемых примерах:
1. Изучили введение к API — определились с набором компонентов
2. Изучили часть документации по авторизации — написали свой метод
авторизации, проверили его на простом запросе.
3. Изучили основную часть — определились с классами и объектами в Delphi
4. Изучили документацию по исключениям API
— написали свои
обработчики
А то, насколько быстро и правильно напишете код Delphi напрямую зависит
от того насколько вы хорошо усвоили информацию о которой я говорил
в первой части и как внимательно вы прочитали документацию.
Файлы
Все исходные тексты программы и модуля для работы с API etxt.ru можно
скачать со страницы блога webdelphi.ru
Download