Модераторы: LSD, AntonSaburov
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Работа с веб страницами с помощью HttpClient 
:(
    Опции темы
Platon
Дата 14.6.2008, 17:15 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Здравствуйте, уважаемые.
Решил оформить еще 1 фак для сообщества винградовцев. Это некий вводный туториал (tutorial), мануал (manual) или учебное пособие по работе с HttpClientHttpClient позволяет скачать страницы с интернета с возможностью управлять Cookie и отправлять заголовки на сервер.
Преамбула.

Недавно, буквально позавчера, мне пришлось столкнуться с проблемой выкачки страницы с сайта. Благо, я слышал о таком магическом инструменте как HttpClient от Apache (Apache как всегда рулит!!!) Его идея была ясна, и не хотелось рыться во всех этих документах, туториалах, примерах. Более того, как и все программисты я хочу всё и сразу и чтоб готовенькое. Начал рыться по винграду. Спасибо LSD по его ответам на форуме я сдвинулся с места. Но не хватало многих мелочей, которые таки заставили меня обратиться к официальным докам, туториалам и примерам. Чтоб уберечь нежные гениальные умы будущих поколений, я решил объединить все эти мелочи в одном месте. И так поехали.

Основная часть.

Выгружаю сразу код, в котором содержится весь материал.
Код

package ru.vingrad.platon.httpclient;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.cookie.CookiePolicy;

import java.io.*;
import java.net.URLEncoder;

public class HttpClientTest {

    private static final String DOMAIN = "localhost";
    private static final String SITE_ROOT = "http://" + DOMAIN + "/";

    private static final String DEFAULT_CHARSET = "UTF-8";
    private static final String SITE_CHARSET = "CP1251";

    public static void main(String[] args) throws UnsupportedEncodingException {
        HttpClient httpClient = new HttpClient();
        httpClient.getParams().setContentCharset(DEFAULT_CHARSET);
        httpClient.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);

        HttpState state = new HttpState();
        state.addCookie(new Cookie(DOMAIN, "name", "Platon", "/forum/", new Date(System.currentTimeMillis() + 3600*1000*24*365L), false));
        state.addCookie(new Cookie(DOMAIN, "password", "******", "/forum/", new Date(System.currentTimeMillis() + 3600*1000*24*365L), false));
         httpClient.setState(state);
        // Загрузка страницы с запросом методом GET
        GetMethod getMethod = new GetMethod(SITE_ROOT + "index.html?cat=1&search=" + URLEncoder.encode("Доктор Хаус", SITE_CHARSET));
        getMethod.getParams().setContentCharset(SITE_CHARSET);
        try {
            int result = httpClient.executeMethod(getMethod);
            if (result == HttpStatus.SC_OK) {
                // Выводим страницу на экран
                System.out.println(getMethod.getResponseBodyAsString());
            } else {
                System.out.println("А страничка-то и не загрузилась!!!");
                return;
            }
        } catch (IOException e) {
            System.out.println("Проблемы со связью");
            return;
        } finally {
            getMethod.releaseConnection();
        }

        // Загрузка страницы с запросом методом POST
        PostMethod postMethod = new PostMethod(SITE_ROOT + "index.html");
        postMethod.addParameter("cat", "1");
        postMethod.addParameter("name", "Доктор Хаус");
        postMethod.getParams().setContentCharset(SITE_CHARSET);
        try {
            int result = httpClient.executeMethod(postMethod);
            if (result == HttpStatus.SC_OK) {
                // Выводим страницу на экран
                printStream(postMethod.getResponseBodyAsStream());
            } else {
                System.out.println("А страничка-то и не загрузилась!!!");
                return;
            }
        } catch (IOException e) {
            System.out.println("Проблемы со связью");
            return;
        } finally {
            postMethod.releaseConnection();
        }

    }

    private static void printStream(InputStream res) throws IOException {
        InputStreamReader reader = new InputStreamReader(res, SITE_CHARSET);
        OutputStreamWriter writer = new OutputStreamWriter(System.out);
        int am;
        char[] buffer = new char[4096];
        while ((am = reader.read(buffer)) != -1)
            writer.write(buffer, 0, am);
    }
}


Засекаем секундомеры!
HttpClient можно расценивать как браузер, который не отображает страницу, а только содержит состояние, в котором работает пользователь. Это и все куки, которые устанавливаются в начале сеанса и которыми обменивается с сервером, это и данные авторизации (это оставляю на домашнее задание

Код

httpClient.getParams().setContentCharset(DEFAULT_CHARSET);
httpClient.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);

Для меня это были очень важные строчки, с ними я набил много шишек. 
Первая строчка устанавливает кодировку, в которой будут отправляться строки запроса методом GET и тело запроса методом POST
Вторая строчка важна для работы с «выпечкой» Самая подходящая политика для работы с «печенюшками» большинства сайтов это именно  CookiePolicy.BROWSER_COMPATIBILITY.

Теперь перейдем к самим «печенькам». Браузер всегда находится в каком-то состоянии. 
Код
HttpState state = new HttpState();
/*...*/
httpClient.setState(state);

К примеру, пользователь может быть авторизован и при его желании сессия может быть сохранена в cookies. У нас это name и password, гипотетически позволяет работать от имени заранее заведенного аккаунта.
Код
state.addCookie(new Cookie(DOMAIN, "name", "Platon", "/forum/", new Date(System.currentTimeMillis() + 3600*1000*24*365L), false));
state.addCookie(new Cookie(DOMAIN, "password", "******", "/forum/", new Date(System.currentTimeMillis() + 3600*1000*24*365L), false));


С инициализациией браузера разобрались, пробуем загрузить странички.
1. Методом GET
Код

GetMethod getMethod = new GetMethod(SITE_ROOT + "index.html?cat=1&search=" + URLEncoder.encode("Доктор Хаус", SITE_CHARSET));
getMethod.getParams().setContentCharset(SITE_CHARSET);


Всё интуитивно должно быть понятно. О различиях методов GET и POST читать в другом месте. Заострю лишь внимание на двух моментах:
1. Формируемый адрес обладает интересным URLEncoder.encode("Доктор Хаус", SITE_CHARSET) Не то, что бы интересен тут персонаж телесериала, хотя это тоже ^_^, сколько то, что он написан по-русски, а мы знаем, как буржуи дискредитируют нелатиницу, да еще и с проблелом. Все пробелы в адресных строках заменяются на +, а нелатиница заменяется спецстроками типа %FF в результате эта обертка URLEncoder'а поможет нам избавиться от головной боли с переводом в страшный текст: «%C4%EE%EA%F2%EE%F0+%D5%E0%F3%F1»
Заметим также, что SITE_CHARSET вносит здесь свою лепту. Строчка выше — это кириллический текст в кодировке windows-1251, в кодировке utf-8 это будет выглядеть так:
«%D0%94%D0%BE%D0%BA%D1%82%D0%BE%D1%80+%D0%A5%D0%B0%D1%83%D1%81»

Вторая строчка. Я не уверен, что она тут, вообще, действует, так что ее можно проигнорировать. Идем дальше. Пора бы запустить наш запрос.

Код

            int result = httpClient.executeMethod(getMethod);
            if (result == HttpStatus.SC_OK) {
                // Выводим страницу на экран
                System.out.println(getMethod.getResponseBodyAsString());
            } else {
                System.out.println("А страничка-то и не загрузилась!!!");
                return;
            }


1-я строчка. Запускаем метод из определенного состояния: на сервер отсылаются все cookies объекта HttpClient, относящиеся к сайту.
2-я строчка. result содержит статус страницы (Мы помним самые известные 200 — страница загружена, 404 — страница не сужествует, 302 — сервер произвел переадресацию)
4-я строчка. выводим извлеченную страничку.  getResponseBodyAsString(), от куда же метод знает в какой кодировке страничка? Можно подумать, что мы установили ее с помощью getMethod.getParams().setContentCharset(SITE_CHARSET);  Ничего подобного :( Из ответа от сервера, ему передается заголовок Content-Type откуда извлекается кодировка. Если сервер не передаст нам этого заголовка — дело плохо. Об альтернативных способах получения странички чуть позже.

2. Методом POST
Код

        PostMethod postMethod = new PostMethod(SITE_ROOT + "index.html");
        postMethod.addParameter("cat", "1");
        postMethod.addParameter("name", "Доктор Хаус");
        postMethod.getParams().setContentCharset(SITE_CHARSET);
        try {
            int result = httpClient.executeMethod(postMethod);
            if (result == HttpStatus.SC_OK) {
                // Выводим страницу на экран
                printStream(postMethod.getResponseBodyAsStream());
            } else {
                System.out.println("А страничка-то и не загрузилась!!!");
                return;
            }
        } catch (IOException e) {
            System.out.println("Проблемы со связью");
            return;
        }

Знакомый шаблон? Да. Единственное существенное различие — это метод отправки данных POST. Теперь так лихо все параметры в адресную строку не засунуть. Расстраиваться не стоит у Apache всегда есть чем подсластить программисту жизнь. postMethod.addParameter("name", "Доктор Хаус"); хитрый метод addParameter разберется абсолютно со всеми проблемами сам, нет необходимости в перекодировании кириллического текста. НО, теперь нам важна строчка postMethod.getParams().setContentCharset(SITE_CHARSET);, тело запроса будет кодироваться в установленной кодировке. Здорово!

Теперь бонус: Я писал о возможных проблемах метода HttpMethod.getResponseBodyAsString, если сервер не передаст кодировку, в которой написана страница. Альтернативой может послужить самостоятельная обработка сырых байтов переданных данных byte[] HttpMethod.getResponseBody(). Или же для больших документов можно обрабатывать страницу на лету с помощью метода HttpMethod.getResponseBodyAsStream(), если это, конечно, технически возможно. Например, со страницы может передаваться гигантских размеров xml структура, ее можно обрабатывать «на ходу» с помощью SAX Parser'а
Или как в нашем случае, не хранить данные, а сразу выдавать их в stdout.

Еще 1 важный аспект работы с HttpClient

Если в рамках одного экземпляра HttpClient вы работаете последовательно с несколькими объектами HttpMethod
К примеру, 
Код

GetMethod getMethod = new GetMethod(getLoginPath(login, password));
int result = browser.executeMethod(getMethod);
if (result == HttpStatus.SC_OK) {
    getMethod.releaseConnection();
    GetMethod fetchProfile = new GetMethod(getProfilePath());
    result = browser.executeMethod(getMethod);
    //... и так далее
}

В этом кусочке кода обратите внимание на 4 строчку, releaseConnection позволяет закрыть соединение.
Цитата

This is a crucial step to keep things flowing. We must tell HttpClient that we are done with the connection and that it can now be reused. Without doing this HttpClient will wait indefinitely for a connection to free up so that it can be reused.

Это важный шаг в сторону того, чтобы всё шло так, как оно должно быть. Мы должны сказать HttpClient что мы закончили с соединением и оно может быть переиспользовано. Без таких шагов текущий HttpClient будет ждать неопределенное время, пока соединение освободится, для того, чтобы оно могло быть переиспользовано.

А вообще, при работе с HttpClient стоит взять за правило конструкцию
Код

HttpMethod m = new GetMethod(SITE_URL);
try {
    int res = httpClient.executeMethod(m);
    if (res != HttpStatus.SC_OK)
        // ... проблемы с ответом от сервера
} finally {
    // ОЧЕНЬ ОБЯЗАТЕЛЬНО!!!
    m.releaseConnection();
}


О таймауте

Работая с библиотекой, сразу не замечаешь критических моментов, таких как например бесконечное ожидание отклика от сервера. Да и такое бывает, но к счастью в HttpClient предусмотрена и такая функция. Смотрим:
Код

HttpClient browser = new HttpClient();
browser.getParams().setSoTimeout(TIMEOUT_IN_MILLISECONDS);
GetMethod method = new GetMethod(SITE_URL);
int result = browser.execute(method);
if (result = HttpStatus.SC_TIMEOUT)
    System.out.println("Время ожидания ответа с сервера истекло");

Новшества тут 2-я строчка, которая устанавливает таймаут ожидания ответа, и 4-я строчка, в которой в переменной result хранится информация, о том, что HttpClient не дождался ответа с сервера
По умолчанию HttpClient может ждать вечность, так что это нужно учесть.
Есть также более тонкая настройка таймаутов для каждого запроса индивидуально, это делается аналогично 2-й строчке, только объект над которым будет совершаться действие - HttpMethod
method.getParams().setSoTimeout(TIMEOUT_IN_MILLISECONDS)

О SSL


Приятно был удивлен, что и с SSL HttpClient благополучно разбирается самостоятельно!!! Т.е. нам не надо искать какие-то сертификаты, что-то шурудить, наворачивать. Согласно гиду по SSL мы можем работать с протоколом SSL также как с обычным незащищенным ^_^  Даже комментировать в коде нечего, один и тот же шаблон.

Код

mport org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;

import java.io.IOException;

public class TestSSL {
    public static void main(String[] args) {
        HttpClient httpclient = new HttpClient();
        GetMethod httpget = new GetMethod("https://www.verisign.com/");
        try {
            httpclient.executeMethod(httpget);
            System.out.println(httpget.getStatusLine());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            httpget.releaseConnection();
        }
    }
}


Послесловие

Если бы кто-то до меня написал бы эту тему, я был бы очень рад. Но видимо прошло время потребления, пришло время производства! Принимайте, рецензируйте, пользуйтесь.


Да, спонсор статьи мой блог о Java Platon's Java life

Это сообщение отредактировал(а) Platon - 13.1.2009, 19:09
PM MAIL ICQ   Вверх
Platon
Дата 17.6.2008, 15:11 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Совсем забыл затронуть тему загрузки файлов на сервер. На форуме есть тема посвященная этому вопросу
PM MAIL ICQ   Вверх
Alenka_
Дата 19.6.2008, 22:48 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 61
Регистрация: 18.6.2007

Репутация: нет
Всего: нет



По поводу того, в какой кодировке сервер присылает ответ-
Цитата

The "charset" parameter is used with some media types to define the
   character set (section 3.4) of the data. When no explicit charset
   parameter is provided by the sender, media subtypes of the "text"
   type are defined to have a default charset value of "ISO-8859-1" when
   received via HTTP. Data in character sets other than "ISO-8859-1" or
   its subsets MUST be labeled with an appropriate charset value. See
   section 3.4.1 for compatibility problems.


http://rfc.net/rfc2616.html

То есть есть соглашение, что сервер должен использовать кодировку ISO-8859-1, когда не указывает ее в заголовках. Но все ли придерживаются этого соглашения?..
PM MAIL   Вверх
Shaggie
Дата 20.6.2008, 07:28 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Завсегдатай
Сообщений: 570
Регистрация: 21.12.2006
Где: outer space

Репутация: 4
Всего: 72



Platon, спасибо!!! Я давненько (признаться, не напрягаясь) искал аналог перлового LWP на Java, и HttpClient в этой роли выглядит как самое то!


--------------------
Цитата(alina3000 @  6.3.2014,  10:47 Найти цитируемый пост)
Сорри что не по теме 
PM MAIL ICQ GTalk Jabber   Вверх
Platon
Дата 21.7.2008, 13:36 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Странно, что никто меня не подправил. Обновил статью. Добавил информацию по поводу HttpMethod#releaseConnection()
PM MAIL ICQ   Вверх
Platon
Дата 2.8.2008, 08:08 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Обновил статью, добавил информацию о таймаутах.
PM MAIL ICQ   Вверх
sith
Дата 6.8.2008, 22:40 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 537
Регистрация: 11.2.2007

Репутация: нет
Всего: 1



... думаю что можешь обновить статью в плане работы с SSL... там все просто... но все же для полной картины...


--------------------
Там где ты ставишь глупые смайлики, я вбиваю восклицания знаки!!!
PM MAIL   Вверх
unkis
Дата 7.8.2008, 00:31 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 802
Регистрация: 8.9.2004

Репутация: нет
Всего: 1



Было бы действительно неплохо написать про SSL


--------------------
www.unkis.com
PM MAIL WWW   Вверх
Platon
Дата 8.8.2008, 20:39 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Эх-эх-эх, я в обычной жизни то с SSL тут не работал, а вы мне предлагаете написать о SSL, ок попробую, разберусь заодно с SSL
PM MAIL ICQ   Вверх
Platon
Дата 29.8.2008, 14:28 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 16
Всего: 40



Обновил статью опять о releaseConnection()
PM MAIL ICQ   Вверх
priboltik
Дата 4.3.2009, 11:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 7
Регистрация: 11.11.2007

Репутация: нет
Всего: нет



а почему не написали о том, что делать, если классы не создаются??
например у меня при  выполнении даже следующего кода:

Код

HttpClient httpClient = new HttpClient();


выкидывало исключение: 
Код

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory
        at org.apache.commons.httpclient.HttpClient.<clinit>(HttpClient.java:66)
        at Main.main(Main.java:20)



пока не подключил библиотеку commons-logging:Ссылка для скачивания
PM MAIL WWW Skype GTalk   Вверх
Nymph666
Дата 21.2.2013, 08:19 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 25
Регистрация: 28.11.2007

Репутация: нет
Всего: нет



Цитата(Platon @ 29.8.2008,  14:28)
Обновил статью опять о releaseConnection()

Сейчас как раз занимаюсь тем, что пытаюсь получить ответ от Google-поиска через метод Get.
Но столкнулась со следующей ошибкой:
если напрямую выводить в форму на страницу getMethod.getResponseBodyAsString(), то выдается warning, что Размер стринга слишком большой и рекомендуется использовать getMethod.getResponseBodyAsStream().
Пробую делать это с помощью стрима:
Сначала получаю нужный адрес:
Код

    private String getRequestUrl(String word) {
        final String base= "http://www.google.ru/";
        return base + "#q=" + word + "&fp=1";
    }

Код

После получаю документ с нашим сохраненным потоком.
    private Document getGoogleRssDocument(String word) throws IOException, ParserConfigurationException, SAXException {

        String url = getRequestUrl(word);

        HttpClient client = new HttpClient();
        GetMethod get = new GetMethod(url);
        try {

            int resultCode = client.executeMethod(get);

            if (resultCode == HttpStatus.SC_OK) {
                InputStream in = get.getResponseBodyAsStream();
                DocumentBuilder builder = builderFactory.newDocumentBuilder();
                return builder.parse(in);
            } else {
                throw new IOException("HTTP Communication problem, response code: "+resultCode);
            }

        } finally {
            get.releaseConnection();
        }
    }

Затем обрабатываю результат (Document) следующей функцией, которая как раз должна нам возвращать html-код:
Код

private String getGoogleHtml(Document rss) {
        try {
            Element item = (Element)rss.getElementsByTagName("item").item(0);
            Element desc = (Element)item.getElementsByTagName("description").item(0);
            return desc.getFirstChild().getNodeValue();

        }

Затем загоняю этот результат в переменную  private HTML googleHtml;
И вывожу ее с помощью panel от GWT:
Код

 googleHtml = new HTML();
        VerticalPanel googlePanel = new VerticalPanel();
        googlePanel.add(googleHtml);
        RootPanel.get().add(googlePanel);


Но! Вылезает ошибка The markup in the document preceding the root element must be well-formed. smile 
И черт поймет что не так в этом документе. Возможно кто-то тоже использовал Stream.
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Java"
LSD   AntonSaburov
powerOn   tux
javastic
  • Прежде, чем задать вопрос, прочтите это!
  • Книги по Java собираются здесь.
  • Документация и ресурсы по Java находятся здесь.
  • Используйте теги [code=java][/code] для подсветки кода. Используйтe чекбокс "транслит", если у Вас нет русских шрифтов.
  • Помечайте свой вопрос как решённый, если на него получен ответ. Ссылка "Пометить как решённый" находится над первым постом.
  • Действия модераторов можно обсудить здесь.
  • FAQ раздела лежит здесь.

Если Вам помогли, и атмосфера форума Вам понравилась, то заходите к нам чаще! С уважением, LSD, AntonSaburov, powerOn, tux, javastic.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Java: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.1172 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.