Спершу заздалегідь відмови від відповідальності: опубліковані фрагменти коду - всі основні приклади. Вам потрібно буде впоратися з тривіальними IOException
і RuntimeException
подібними NullPointerException
, ArrayIndexOutOfBoundsException
і виправляти себе.
Підготовка
Спершу нам потрібно знати хоча б URL-адресу та схему. Параметри необов’язкові і залежать від функціональних вимог.
String url = "http://example.com";
String charset = "UTF-8"; // Or in Java 7 and later, use the constant: java.nio.charset.StandardCharsets.UTF_8.name()
String param1 = "value1";
String param2 = "value2";
// ...
String query = String.format("param1=%s¶m2=%s",
URLEncoder.encode(param1, charset),
URLEncoder.encode(param2, charset));
Параметри запиту повинні бути у name=value
форматі та об'єднані &
. Зазвичай ви також кодуєте URL-адреси параметрів запиту за допомогою вказаної таблиці URLEncoder#encode()
.
Це String#format()
просто для зручності. Я вважаю за краще, коли мені потрібен буде оператор з’єднання рядків +
більше ніж удвічі.
Запуск HTTP GET- запиту з (необов'язково) параметрами запиту
Це тривіальне завдання. Це метод запиту за замовчуванням.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Будь-який рядок запиту слід приєднати до URL-адреси за допомогою ?
. Accept-Charset
Тема може натякнути на те , що сервері , що кодують параметри знаходяться. Якщо ви не відправите будь-який рядок запиту, то ви можете залишити Accept-Charset
заголовок геть. Якщо вам не потрібно встановлювати заголовки, тоді ви навіть можете скористатися URL#openStream()
методом швидкого доступу.
InputStream response = new URL(url).openStream();
// ...
У будь-якому випадку, якщо інша сторона є a HttpServlet
, тоді її doGet()
метод буде викликаний і параметри будуть доступні HttpServletRequest#getParameter()
.
З метою тестування ви можете роздрукувати тіло відповідей до stdout, як показано нижче:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Запуск HTTP POST- запиту з параметрами запиту
Установка URLConnection#setDoOutput()
для true
неявно встановлює метод запиту до POST. Стандартний HTTP POST, як це роблять веб-форми, має тип, у application/x-www-form-urlencoded
якому рядок запиту записується в тіло запиту.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true); // Triggers POST.
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + charset);
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes(charset));
}
InputStream response = connection.getInputStream();
// ...
Примітка: коли ви хочете програмно подати HTML-форму, не забудьте взяти name=value
пари будь-яких <input type="hidden">
елементів у рядок запиту, і, звичайно, також name=value
пару <input type="submit">
елементів, які ви хочете програмно "натиснути" (тому що зазвичай використовується на стороні сервера, щоб визначити, чи була натиснута кнопка, і якщо так, то яка).
Ви також можете привести отриманий URLConnection
в HttpURLConnection
і використовувати його HttpURLConnection#setRequestMethod()
замість. Але якщо ви намагаєтеся використовувати з'єднання для виведення ви все ще потрібно встановити URLConnection#setDoOutput()
в true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
У будь-якому випадку, якщо інша сторона є a HttpServlet
, тоді її doPost()
метод буде викликаний і параметри будуть доступні HttpServletRequest#getParameter()
.
Фактично запускається HTTP-запит
Ви можете надіслати запит HTTP явно за допомогою URLConnection#connect()
, але запит автоматично запускається на вимогу, коли ви хочете отримати будь-яку інформацію про HTTP-відповідь, наприклад, використовуючи орган відповіді URLConnection#getInputStream()
тощо. Наведені вище приклади роблять саме це, тому connect()
дзвінок насправді є зайвим.
Збір інформації про відповідь HTTP
Статус відповіді HTTP :
Вам потрібно HttpURLConnection
тут. Покиньте спочатку, якщо потрібно.
int status = httpConnection.getResponseCode();
Заголовки відповідей HTTP :
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
System.out.println(header.getKey() + "=" + header.getValue());
}
Кодування відповіді HTTP :
Коли параметр Content-Type
містить charset
параметр, то тіло відповіді, ймовірно, засноване на тексті, і тоді ми хотіли б обробити тіло відповіді кодуванням символів, вказаним на сервері.
String contentType = connection.getHeaderField("Content-Type");
String charset = null;
for (String param : contentType.replace(" ", "").split(";")) {
if (param.startsWith("charset=")) {
charset = param.split("=", 2)[1];
break;
}
}
if (charset != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
for (String line; (line = reader.readLine()) != null;) {
// ... System.out.println(line) ?
}
}
} else {
// It's likely binary content, use InputStream/OutputStream.
}
Підтримання сесії
Сеанс на стороні сервера зазвичай підтримується файлом cookie. Деякі веб-форми вимагають, щоб ви входили в систему та / або відслідковувались сеансом. Ви можете використовувати CookieHandler
API для підтримки файлів cookie. Ви повинні підготувати CookieManager
з CookiePolicy
з ACCEPT_ALL
перед відправкою всіх запитів HTTP.
// First set the default cookie manager.
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
// All the following subsequent URLConnections will use the same cookie manager.
URLConnection connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
Зауважте, що це, як відомо, не завжди працює належним чином за будь-яких обставин. Якщо це не вдасться вам, тоді найкраще зібрати вручну та встановити заголовки файлів cookie. В основному потрібно схопити всі Set-Cookie
заголовки з відповіді на вхід або перший GET
запит, а потім передати це через наступні запити.
// Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
// ...
// Then use the same cookies on all subsequent requests.
connection = new URL(url).openConnection();
for (String cookie : cookies) {
connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
}
// ...
split(";", 2)[0]
Знаходиться там , щоб позбутися від атрибутів печива , які НЕ мають відношення до стороні сервера , як expires
, path
і т.д. Крім того , можна також використовувати cookie.substring(0, cookie.indexOf(';'))
замість split()
.
Режим потокового передачі
HttpURLConnection
Буде за замовчуванням буфера всього тіла запиту перед тим як відправити його, незалежно від того, чи були ви встановити фіксовану довжину змісту самостійно , використовуючи connection.setRequestProperty("Content-Length", contentLength);
. Це може спричинити OutOfMemoryException
s, коли ви одночасно надсилаєте великі POST-запити (наприклад, завантаження файлів). Щоб цього уникнути, ви хочете встановити HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Але якщо довжина вмісту заздалегідь не відома, то ви можете скористатися режимом потокового потоку, встановивши HttpURLConnection#setChunkedStreamingMode()
відповідним чином. Це встановить Transfer-Encoding
заголовок HTTP, до chunked
якого змусить орган запиту надсилати шматки. Наведений нижче приклад відправить тіло шматками в 1 КБ.
httpConnection.setChunkedStreamingMode(1024);
Користувач-агент
Може статися, що запит повертає несподівану відповідь, при цьому він справно працює з реальним веб-браузером . Сторона сервера, ймовірно, блокує запити на основі User-Agent
заголовка запиту. За URLConnection
замовчуванням за замовчуванням встановлено його Java/1.6.0_19
там, де остання частина очевидно є версією JRE. Ви можете змінити це так:
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); // Do as if you're using Chrome 41 on Windows 7.
Використовуйте рядок User-Agent з недавнього браузера .
Помилка обробки
Якщо кодом відповіді HTTP є 4nn
(Помилка клієнта) або 5nn
(Помилка сервера), ви можете прочитати, HttpURLConnection#getErrorStream()
щоб побачити, чи надіслав сервер яку-небудь корисну інформацію про помилку.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Якщо код відповіді HTTP дорівнює -1, щось не вдалося попрацювати з підключенням та відповіддю. HttpURLConnection
Реалізація в старих JREs кілька баггі зі збереженням з'єднання в живих. Ви можете вимкнути його, встановивши для http.keepAlive
системного властивості значення false
. Ви можете це зробити програмно на початку програми:
System.setProperty("http.keepAlive", "false");
Завантаження файлів
Зазвичай ви використовуєте multipart/form-data
кодування для змішаного вмісту POST (двійкові та символьні дані). Кодування більш детально описано в RFC2388 .
String param = "value";
File textFile = new File("/path/to/file.txt");
File binaryFile = new File("/path/to/file.bin");
String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
String CRLF = "\r\n"; // Line separator required by multipart/form-data.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (
OutputStream output = connection.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
) {
// Send normal param.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
writer.append(CRLF).append(param).append(CRLF).flush();
// Send text file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
writer.append(CRLF).flush();
Files.copy(textFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// Send binary file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
writer.append("Content-Transfer-Encoding: binary").append(CRLF);
writer.append(CRLF).flush();
Files.copy(binaryFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// End of multipart/form-data.
writer.append("--" + boundary + "--").append(CRLF).flush();
}
Якщо інша сторона є a HttpServlet
, тоді її doPost()
метод буде викликаний, і частини будуть доступні HttpServletRequest#getPart()
(зверніть увагу, таким чином, не getParameter()
і так далі!). Однак getPart()
метод є відносно новим, він впроваджений у Servlet 3.0 (Glassfish 3, Tomcat 7 тощо). До Servlet 3.0 ваш найкращий вибір - це використовувати Apache Commons FileUpload для розбору multipart/form-data
запиту. Також дивіться цю відповідь для прикладів як FileUpload, так і підходів Servelt 3.0.
Справа з ненадійними або неправильно налаштованими HTTPS-сайтами
Іноді потрібно підключити URL-адресу HTTPS, можливо, тому, що ви пишете веб-скребок. У цьому випадку ви, можливо, зіткнетеся з a javax.net.ssl.SSLException: Not trusted server certificate
на деяких HTTPS-сайтах, які не оновлюють свої SSL-сертифікати, або на java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
або javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
неправильно налаштованих HTTPS-сайтах.
Наступний одноразовий static
ініціалізатор у вашому класі веб-скребків повинен зробити HttpsURLConnection
більш м'яким щодо цих HTTPS-сайтів і, таким чином, більше не кидати ці винятки.
static {
TrustManager[] trustAllCertificates = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
}
};
HostnameVerifier trustAllHostnames = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; // Just allow them all.
}
};
try {
System.setProperty("jsse.enableSNIExtension", "false");
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCertificates, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames);
}
catch (GeneralSecurityException e) {
throw new ExceptionInInitializerError(e);
}
}
Останні слова
Apache HttpComponents HttpClient є набагато зручнішим в цьому все :)
Розбір та вилучення HTML
Якщо все, що вам потрібно, - це розбір та вилучення даних з HTML, тоді краще скористайтеся HTML-аналізатором, таким як Jsoup