C#: HttpClient, API Яндекс.Словаря и JSON
Написали со студентами на занятиях по предмету «Сетевое программирование» консольную программу (веб-клиент) на языке C#, использующую API «Яндекс.Словаря» (https://yandex.ru/dev/dictionary). Мы работаем на компьютерах с операционной системой «Windows 10».
Под словом API (application programming interface) подразумевается интерфейс для пользователей-программ в отличие от интерфейса для пользователей-людей. То есть вы можете написать программы, которые будут пользоваться «Яндекс.Словарем» через его API. Технически в данном случае API представляет собой набор функций, которые можно вызывать удалённо, через компьютерную сеть. Это довольно распространенная технология в вебе. Самые лучшие и самые популярные веб-сервисы обычно предоставляют API для взаимодействия с ними программ-клиентов, написанных сторонними разработчиками.
Под названием «Яндекс.Словарь» подразумевается переводной (переводческий) словарь, который используют переводчики для перевода с одного языка общения на другой язык. У «Яндекс.Словаря» есть большое количество направлений перевода, мы пока что реализовали работу только в направлении «en-ru» (с английского на русский язык). Пользователь нашей программы задает слово на английском языке, программа возвращает возможные варианты перевода, разделив их на группы по частям речи (существительное, глагол, прилагательное и так далее).
Класс HttpClient
Наша программа работает на прикладном уровне компьютерной сети по протоколу HTTP (протокол передачи гипертекста). Правда, в нашем случае в теле HTTP-ответа мы хоть и получаем текст, но не гипертекст (по которому протокол HTTP получил свое название), а текст с данными в формате JSON. Формат JSON (JavaScript Object Notation) назван так, потому что в этом формате для данных используют синтаксис объектов и массивов из языка программирования JavaScript.
У меня есть методические указания, в которых рассказано, как работать по протоколу HTTP с помощью классов HttpWebRequest, HttpWebResponse, WebClient и тому подобных. Но в документации на сайте компании Microsoft сказано, что эти классы устарели (хотя они продолжают оставаться доступными для использования) и вместо них для написания новых программ рекомендуется использовать класс HttpClient.
На первом шаге разработки я взял пример из документации класса HttpClient и немного его переписал для получения гипертекста страницы одного из моих учебных статических сайтов. Вот как выглядит код:
namespace HTTPConsoleApp1
{
internal class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
try
{
string responseBody = await client.GetStringAsync(
"https://ilyachalov.github.io/my-site/");
Console.WriteLine(responseBody);
}
catch (HttpRequestException e)
{
Console.WriteLine($"Ошибка: {e.Message}");
}
}
}
}
Для проекта в среде «Visual Studio Community 2022» мы выбрали шаблон «Консольное приложение (Майкрософт)» на платформе «.NET 8». Для этого проекта нужные пространства имен уже подключены в отдельном файле (глобальные директивы using), в том числе там подключено необходимое для класса HttpClient пространство имен System.Net.Http.
Эта программа у меня успешно вывела в окно консоли код страницы указанного сайта на языке HTML (гипертекст).
Как можно видеть, программа в данном случае получилась небольшая, используем только один метод класса — GetStringAsync. Но класс HttpClient очень гибкий и у него есть много всяких возможностей, которые могут понадобиться в более сложных случаях. Например, нередко требуется доступ к заголовкам HTTP-запроса и/или HTTP-ответа и тому подобное, класс HttpClient такие возможности предоставляет.
При использовании класса HttpClient следует обратить внимание на то, что этот класс подразумевает, что в большинстве случаев одна программа будет создавать для себя один экземпляр класса HttpClient, через который и выполняются HTTP-запросы (если их требуется несколько). Не рекомендуется плодить экземпляры класса HttpClient по одному для каждого HTTP-запроса, это неэкономично и может привести к ошибкам из-за нехватки ресурсов.
Также следует обратить внимание, что в программе используются возможности асинхронного программирования, которое не является простым для понимания студентами.
Обращение к API Яндекс.Словаря
В API Яндекс.Словаря входят всего лишь две функции: getLangs и lookup. С помощью первой можно получить список (массив) направлений перевода, по которым умеет работать «Яндекс.Словарь». Эту функцию мы пока не использовали, так как наша программа пока что работает только с одним направлением — перевод с английского на русский язык.
С помощью второй функции можно получить определение (статью) с информацией о заданном слове, в том числе о переводах, синонимах и так далее. Ее мы и использовали.
Для вызова любой из функций API Яндекс.Словаря требуется указать так называемый «API-ключ» (API-key). Это строка, состоящая из длинной последовательности символов. API-ключ используется вместо пароля. Такой API-ключ можно получить бесплатно, следуя простой инструкции. Для получения API-ключа понадобится авторизоваться на сайте Яндекса (завести свой аккаунт или использовать уже имеющийся). API-ключей можно получить несколько (например, можно использовать разные API-ключи для разных своих программ-клиентов). Полученные API-ключи можно просмотреть на специальной странице (требует авторизации на сайте Яндекса).
С учетом этого всего мы взяли вышеприведенный код и дописали его. Вот что получилось:
namespace HTTPConsoleApp1
{
internal class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
try
{
string APIkey = "место для вставки API-ключа";
string lang = "en-ru";
Console.Write("Введите слово для перевода: ");
string? textToTrans = Console.ReadLine();
string responseBody = await client.GetStringAsync(
$"https://dictionary.yandex.net/api/v1/dicservice.json/" +
$"lookup?key={APIkey}&lang={lang}&text={textToTrans}");
Console.WriteLine(responseBody);
}
catch (HttpRequestException e)
{
Console.WriteLine($"Ошибка: {e.Message}");
}
}
}
}
Эта программа у меня успешно выводит в консоль текст в формате JSON с нужными данными от «Яндекс.Словаря». На следующем (последнем) этапе разработки нужно извлечь данные из текста в формате JSON и поместить их в соответствующие объекты в программе на языке C#.
Десериализация из формата JSON на языке C#
Понятие «сериализация» (serialization) произошло от слова serial (последовательный). Имеется в виду последовательная передача данных.
В интернетах часто пишут, что сериализация — это преобразование данных в поток байтов (или битов). Но в книгах и статьях для программистов под сериализацией нередко могут подразумевать один из этапов подготовки (преобразования) данных для последовательной передачи. То есть под сериализацией может подразумеваться разное в разных контекстах. Например, в данном случае (смотрите статью «How to write .NET objects as JSON (serialize)») под сериализацией подразумевается преобразование объектов в программе на языке C# в текст в формате JSON.
В нашей программе требуется обратное: мы получили текст с данными в формате JSON и нам нужно преобразовать эти данные в объекты в программе на языке C#. Такое преобразование в данном случае (смотрите статью «How to read JSON as .NET objects (deserialize)») называют «десериализацией».
Если заглянуть в упомянутые выше статьи, может показаться, что десериализация — это что-то довольно сложное. Однако, проделав ее всего один раз на практике, я очень хорошо понял, как она работает. То есть она только кажется сложной, а на самом деле всё устроено очень ясно, логично и очень удобно. Общий алгоритм десериализации у нас выглядит так:
- создать класс (несколько классов) для приема данных из текста в формате JSON;
- выполнить десериализацию с помощью метода JsonSerializer.Deserialize (для его использования понадобится подключить пространство имен
System.Text.Json); - вывести результаты в консоль.
Самое приятное, что при создании класса (нескольких классов) для приема данных вам достаточно описать только те свойства классов, которые вам нужны для решения вашей задачи. Например, «Яндекс.Словарь» кроме переводов возвращает еще кучу всяких данных, в том числе синонимы заданного слова. В данном случае эти дополнительные данные нам не нужны, поэтому мы можем не создавать для ненужных данных соответствующие классы или соответствующие свойства классов. Это очень упрощает работу с полученными данными. Выбираем только то, что нам нужно.
Вот окончательный код программы:
using System.Text.Json;
namespace HTTPConsoleApp1
{
public class DicResult
{
public Definition[]? def { get; set; } // массив определений
public int code { get; set; } // код ответа
}
public class Definition
{
public string? text { get; set; } // слово для перевода
public string? pos { get; set; } // часть речи
public Translation[]? tr { get; set; } // массив переводов
}
public class Translation
{
public string? text { get; set; } // перевод
}
internal class Program
{
static readonly HttpClient client = new HttpClient();
static async Task Main()
{
try
{
string APIkey = "место для вставки API-ключа";
string lang = "en-ru";
Console.Write("Введите слово для перевода: ");
string? textToTrans = Console.ReadLine();
string responseBody = await client.GetStringAsync(
$"https://dictionary.yandex.net/api/v1/dicservice.json/" +
$"lookup?key={APIkey}&lang={lang}&text={textToTrans}");
DicResult? dicResult =
JsonSerializer.Deserialize<DicResult>(responseBody);
Console.WriteLine($"Код ответа: {dicResult?.code}");
Console.WriteLine($"Направление перевода: {lang}");
for (int i = 0; i < dicResult?.def?.Length; i++)
{
Console.WriteLine();
Console.WriteLine(
$"Слово для перевода: {dicResult.def[i].text} " +
$"({dicResult.def[i].pos})");
for (int j = 0; j < dicResult?.def?[i].tr?.Length; j++)
{
Console.WriteLine(
$"Перевод: {dicResult?.def?[i].tr?[j].text}");
}
}
}
catch (HttpRequestException e)
{
Console.WriteLine($"Ошибка: {e.Message}");
}
}
}
}
Как можно видеть из вышеприведенного кода, я создал три класса для приема данных: DicResult, Definition и Translation. В устройстве возвращаемых «Яндекс.Словарем» данных я разобрался, анализируя полученный текст в формате JSON. По этим данным можно понять, что возвращается один объект (для него я создал класс DicResult), в который вложены другие объекты или массивы других объектов.
Например, в данном случае объект класса DicResult имеет свойство, содержащее массив объектов класса Definition (определения). Каждый объект класса Definition имеет свойство, содержащее массив объектов класса Translation (переводы). Названия классов можно придумывать какие угодно, а названия свойств должны совпадать с названиями свойств в тексте формата JSON.
В статьях на сайте компании Microsoft, на которые выше были даны ссылки, сказано, что создание классов по входящему тексту в формате JSON может выполнить искусственный интеллект. Охотно верю, что он на это способен, но сам я пока эту возможность не проверял. В реальной работе, думаю, это может значительно ускорить разработку подобных веб-клиентов.
Чтобы сократить программу для публикации, я убрал часть проверок на ошибки. Но в нормальной (не учебной) программе, понятное дело, проверок должно быть гораздо больше.
Тестирование программы
Результат работы вышеприведенной программы:

Видно, что переводы разбиты на группы по частям речи.