пятница, 5 июля 2013 г.

Веб-сервис Яндекс.Спеллера. Пример использования в Java.

Проверка орфографии из программы на Java с использованием веб-сервиса Яндекс.Спеллер

Написанию данного сообщения предшествовало два события. Во-первых, участвуя в одном проекте разработки документооборота, я занялся изучением работы с веб-сервисами из Java. Во-вторых, моя жена увлеклась отгадыванием слов в одной из игр, представленных в социальной сети одноклассников. Игра заключалась в том, что по ряду представленных фотографий необходимо было составить слово, которое могло бы явиться их обобщением. Под слово предлагался placeholder (извините, не удержался) из необходимого количества знакомест и касса из двадцати букв, часть из которых должна была составить искомое слово.

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

Как бы то не было, все это время я оставался программистом. И не просто программистом, а программистом изучающим разработку веб-сервисов и их клиентов на Java. Поэтому, я тут же нашел в Google страницу с описанием веб-сервиса Яндекс.Спеллер и написал класс, с помощью которого можно было бы отправить этому сервису набор букв и получить ответ о том, является ли этот набор букв словом из словаря Яндекс.Спеллера или нет. Таким образом, я рассчитывал написать на основе этого класса программу, которая бы перебирала из заданной кассы в 20 букв все возможные комбинации по заданному количеству букв, фильтровала бы их на основе простых фильтров (открытые буквы, мягкий знак в начале слова и т.д.) и спрашивала бы у Яндекс.Спеллера, принадлежит ли данное слово его словарю. Все получившиеся таким образом правильные слова я надеялся представить в виде списка из которого потом выбрать те, что могли бы подойти под содержание представленных фотографий.

Ублажая роботов поисковой системы Яндекса приведу ссылку на страницу, где я получил информацию об интересуемом меня веб-сервисе Яндекс.Спеллера: Яндекс API/Руководство разработчика/Web Service API/Метод checkText.

Забегая вперед скажу, что после запуска этой программы, Яндекс забанил меня примерно после двух десятков тысяч обращений к своему спеллеру и я уже сутки получаю в ответ на запросы к нему ответ "403 Forbidden", так что прикладная польза от этой затеи оказалась нулевая. Однако, программа была написана и соответствующая теория по работе с веб-сервисами была закреплена дополнительным практическим кодом. Вообще, вопрос об отказе обслуживания моих запросов остался. Почему? Разве не для программных обращений Яндекс выставил наружу свои публичные веб-сервисы? Что в моих запросах оказалось неправомерным с точки зрения его автоматов? Частота обращения? Простота запросов (по одному слову, вместо пакета, например, с сотней или тысячей слов)? Или огромное количество "слов" с ошибками? Ответа на этот вопрос я пока не знаю. Возможно, кто-то из читателей этих строк сможет грамотно прокомментировать ситуацию.

Сейчас я хочу добраться до сути данного сообщения в блоге. Пора представить код написанного класса и дать какие-нибудь вводные комментарии по этому коду. Сразу скажу, что интерфейс класса не полировался и в нем одновременно можно увидеть как избыточность, так и неполноту обработки ответного сообщения спеллера. Писал и проверял то, что было интересно, не забывая о главном. Так что если кто-то захочет это использовать, то, видимо, придется перекроить код интерфейса и обработки под свои нужды. Здесь я хочу просто показать пример того, как это можно сделать в Java используя известные сторонние библиотеки.

Обратите внимание. В коде используются возможности Java 7.

Представленный далее класс состоит из приватных полей внутренних данных, конструктора, акцепторов (интерфейс для получения некоторых внутренних данных) и, собственно, главного метода check(), который и выполняет синхронное обращение к веб-сервису Яндекса, получает ответ, разбирает его и сообщает о результате проверки.

package ru.knzsoft;

import java.io.UnsupportedEncodingException;

import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;

public class YandexSpellChecker 
{
    private Client client;
    private List<String> help_list = new ArrayList<>();
    private long spell_error_code = 0;
    private int http_status = 0;
 
    public YandexSpellChecker() {
        client = Client.create();
    }
 
    public List<String> getHelpList() {
        return Collections.unmodifiableList(help_list);
    }
 
    public long getSpellErrorCode() {
        return spell_error_code;
    }
 
    public int getHttpStatus() {
        return http_status;
    }
 
    public enum CheckResult {
        OK,
        SPELLING_ERROR,
        ENCODING_ERROR,
        PARSING_ERROR,
        HTTP_ERROR;
    }
 
    public CheckResult check(String word) {
        spell_error_code = 0;
        http_status = 0;

        try {
            word = URLEncoder.encode(word, "UTF8");
        } catch (UnsupportedEncodingException e) {
            return CheckResult.ENCODING_ERROR;
        }

        WebResource webResource = client
            .resource("http://speller.yandex.net/services/spellservice.json/checkText"
            + "?text=" + word);

        ClientResponse response = webResource.accept("application/json")
            .get(ClientResponse.class);

        http_status = response.getStatus(); 
        if (http_status != 200) {
            return CheckResult.HTTP_ERROR;
        }

        String output = response.getEntity(String.class);
        JSONParser parser = new JSONParser();
        Object obj;
        try {
            obj = parser.parse(output);
        } catch (ParseException e) {
            return CheckResult.PARSING_ERROR;
        }

        JSONArray array = (JSONArray) obj;
        if (array == null) {
            return CheckResult.PARSING_ERROR;
        }

        if (array.isEmpty()) {
            return CheckResult.OK;
        }

        @SuppressWarnings("unchecked")
        Iterator<Object> it = array.iterator();
        while (it.hasNext()) {
            Object o = it.next();
            JSONObject jo = (JSONObject) o;

            o = jo.get("code");
            if (o == null) {
                return CheckResult.PARSING_ERROR;
            } else {
                spell_error_code = (java.lang.Long) o;
            }

            help_list.clear();
            o = jo.get("s");
            JSONArray sarray = (JSONArray) o;
            if (sarray != null) {
                @SuppressWarnings("unchecked")
                Iterator<Object> sit = sarray.iterator();
                while (sit.hasNext()) {
                    o = sit.next();
                    String v = (String) o;
                    help_list.add(v);
                }
            }
        }
        return CheckResult.SPELLING_ERROR;
    }
}

Для работы с веб-сервисами, в моей реализации, используется библиотека Jersey. Это известная и популярная библиотека для работы с веб-сервисами поддерживающими архитектуру REST. Для тех, кто не знает смысла этой умной аббревиатуры, поясню. Такая архитектура подразумевает общение на уровне сообщений, где каждое сообщение несет в себе весь необходимый контекст запроса. Т.е. не нужно создавать сессии, в которых хранить и накапливать контекст таких диалогов. Как вариант, такая архитектура может строиться на основе HTTP. Этот протокол, в чистом виде, как раз является протоколом REST, например, в отличие от того же протокола SIP, который основан на HTTP, но уже в наборе своих заголовков несет информацию не только о номере сессии, но и о номере коннекта внутри сессии.

Следует обратить внимание, что библиотека Jersey несколько преобразилась начиная со второй версии. Насколько мне показалось, коды, использующие первую и вторую версию Jersey не совместимы ни в какую из сторон. Так как меня, на этот момент интересовала первая версия Jersey, то и приведенный код использования Jersey соответствует первой версии библиотеки. Конкретно, для компиляции этого кода я использовал в проектном файле системы сборки Maven указание на следующую зависимость.

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-client</artifactId>
    <version>1.8</version>
</dependency>

Собственно, весь код использования Jarsey можно свести к следующему фрагменту.

  // Создаем клиента для работы с сервисом REST
  Client client = Client.create();

  // Уточняем вид клиента и адрес ресурса. Создаем веб-клиента под конкретный ресурс
  WebResource webResource = client
      .resource("http://speller.yandex.net/services/spellservice.json/checkText"
      + "?text=" + word);

  // Важно! Синхронная операция. Делаем запрос GET и ждем получения ответа
  ClientResponse response = webResource.accept("application/json")
      .get(ClientResponse.class);

  // Из объекта ответа извлекаем статус операции. 
  // Для HTTP, код 200 - успешное выполнение
  http_status = response.getStatus(); 
  if (http_status != 200) {
      return CheckResult.HTTP_ERROR;
  }

  // Получаем строку с телом сообщения. В нашем случае, строку JSON.
  String output = response.getEntity(String.class);

Некоторые дополнительные подробности по работе с Jersey можно получить на странице Client API из документации по первой версии библиотеки.

Еще раз обратите внимание на синхронность операции получения ответа на запрос GET к веб-сервису. Обратите внимание, что на время получения этого ответа, исполнение этой программы приостановится. Если вас это не устраивает, то придется искать другой способ выполнения данной операции.

Дальнейший код связан с обработкой ответа полученного от веб-сервиса. Так как ответ приходит в виде строки JSON, необходимо выполнить парсинг данной строки. В данном коде используется простая библиотека от Google, с многоговорящим названием JSON-simple. Для включения этой библиотеки в зависимости проектного файла системы сборки Maven, я использую следующий код.

<dependency>
    <groupId>com.googlecode.json-simple</groupId>
    <artifactId>json-simple</artifactId>
    <version>1.1</version>
</dependency>

Схема использования JSON-siple проста. Создается объект парсера сообщения JSON - JSONParser. Объекту парсера отдается сообщение со строкой JSON. Результатом парсинга является объект базового класса, который мы потом пребразуем к ожидаемому типу данных. Так как мы ожидаем массив JSON, то следует воспользоваться типом JSONArray. Класс JSONArray реализует интерфейс List, поэтому мы сможем выполнить обход массива через объект итератора. Далее, извлекаем основные данные из ответа, в том числе и массив подсказок по вариантам исправления ошибочного слова.

Подробности по работе с классами из библиотеки JSON.simple можно посмотреть на страницах ресурса JSON-simple.

Комментируя данный код, следует обратить внимание на еще одну маленькую деталь - кодирование русских букв в слове запроса. С этой функцией успешно справляется класс java.net.URLEncoder из стандартной библиотеки JDK.

Комментариев нет: