1. 程式人生 > >Java 和 HTTP 的那些事之模擬 HTTP 請求

Java 和 HTTP 的那些事之模擬 HTTP 請求

一、使用 HttpURLConnection 傳送 HTTP 請求

Java 自帶的 java.net 這個包中包含了很多與網路請求相關的類,但是對於我們來說,最關心的應該是 HttpURLConnection 這個類了。

1.1 建立 HTTP 連線物件

要得到一個 HttpURLConnection HTTP 連線物件,首先需要一個 URL,程式碼如下:

1

2

URL obj = new URL(url);

HttpURLConnection con = (HttpURLConnection) obj.openConnection();

1.2 新增 HTTP 請求頭

得到 HTTP 連線物件之後,我們就可以進行 HTTP 操作了,我們可以新增任意的 HTTP 請求頭,然後執行我們需要的 GET 或者 POST 請求。我們像下面這樣,新增兩個 HTTP 頭(User-Agent 和 Accept-Language):

1

2

con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");

con.setRequestProperty("Accept-Language",

"en-US,en;q=0.5");

對於有些爬蟲來說,這個設定是必要的,譬如有很多網站會對請求頭中的 Referer 進行檢查,以此來防爬或者防盜鏈。又譬如有些網站還會對 User-Agent 進行檢查,根據這個欄位來過濾一些非瀏覽器的請求。如果請求頭設定不對的話,很可能是爬不下正確的資料的。

1.3 HTTP GET

HTTP 協議中定義了很多種 HTTP 請求方法:GET、POST、PUT、DELETE、OPTIONS 等等,其中最常用到的就是 GET 和 POST,因為在瀏覽器中大多都是使用這兩種請求方法。

使用 HttpURLConnection 來發送 GET 請求是非常簡單的,通過上面的程式碼建立並初始化好一個 HTTP 連線之後,就可以直接來發送 GET 請求了。

1

2

3

con.setRequestMethod("GET");

int responseCode = con.getResponseCode();

String responseBody = readResponseBody(con.getInputStream());

可以看到,程式碼非常簡潔,沒有任何累贅的程式碼,甚至沒有任何和傳送請求相關的程式碼,請求就是在 getResponseCode() 函式中默默的執行了。其中 readResponseBody() 函式用於讀取流並轉換為字串,具體的實現如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 讀取輸入流中的資料

private String readResponseBody(InputStream inputStream) throws IOException {

BufferedReader in = new BufferedReader(

new InputStreamReader(inputStream));

String inputLine;

StringBuffer response = new StringBuffer();

while ((inputLine = in.readLine()) != null) {

response.append(inputLine);

}

in.close();

return response.toString();

}

1.4 HTTP POST

使用 HttpURLConnection 來模擬 POST 請求和 GET 請求基本上是一樣的,但是有一點不同,由於 POST 請求一般都會向服務端傳送一段資料,所以 HttpURLConnection 提供了一個方法 setDoOutput(true) 來表示有資料要輸出給服務端,並可以通過 getOutputStream() 得到輸出流,我們將要寫的資料通過這個輸出流 POST 到服務端。

1

2

3

4

5

6

con.setRequestMethod("POST");

con.setDoOutput(true);

DataOutputStream wr = new DataOutputStream(con.getOutputStream());

wr.writeBytes(parameter);

wr.flush();

wr.close();

POST 完成之後,和 GET 請求一樣,我們通過 getInputStream() 函式來讀取服務端返回的資料。

二、使用 HttpClient 傳送 HTTP 請求

使用 Java 自帶的 HttpURLConnection 類完全可以滿足我們的一些日常需求,不過對於網路爬蟲這種高度依賴於 HTTP 工具類的程式來說,它在有些方面還是顯得略為不足(我們之後會討論到),我們需要一種擴充套件性定製性更強的類。Apache 的 HttpClient 就是首選。

HttpClient 是 Apache Jakarta Common 下的子專案,用來提供高效的、最新的、功能豐富的支援 HTTP 協議的客戶端程式設計工具包。它相比傳統的 HttpURLConnection,增加了易用性和靈活性,它不僅讓客戶端傳送 HTTP 請求變得更容易,而且也方便了開發人員測試介面(基於 HTTP 協議的),即提高了開發的效率,也方便提高程式碼的健壯性。

好了,關於 HttpClient 介紹的大話空話套話結束,讓我們來看一段使用 HttpClient 來模擬 HTTP GET 請求的程式碼片段:

1

2

3

4

5

6

CloseableHttpClient httpclient = HttpClients.createDefault();

HttpGet request = new HttpGet(url);

request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");

CloseableHttpResponse response = httpclient.execute(request);

// read response

2.1 HTTP GET 與 HTTP POST

上面的示例程式碼展示瞭如何使用 HttpClient 來模擬 HTTP GET 請求,可以看出 HttpClient 對每一種 HTTP 方法都準備了一個類,GET 請求使用 HttpGet 類,POST 請求使用 HttpPost 類。

和上文中介紹的一樣,在傳送 POST 請求時,需要向服務端寫入一段資料,我們這裡使用 setEntity() 函式來寫入資料:

1

2

3

4

5

String parameter = "key=value";

HttpPost request = new HttpPost(url);

request.setEntity(

new StringEntity(parameter, ContentType.create("application/x-www-form-urlencoded"))

);

Entity 是 HttpClient 中的一個特別的概念,有著各種的 Entity ,都實現自 HttpEntity 介面,輸入是一個 Entity,輸出也是一個 Entity 。要注意的是,在這裡我採用一種取巧的方式,直接使用 StringEntity 來寫入 POST 資料,然後將 Content-type 改成 application/x-www-form-urlencoded ,這樣就和瀏覽器裡的表單提交請求一致了。但是我們要知道的是,一般情況下,我們可能還會使用 UrlEncodedFormEntity 這個類,只是在寫爬蟲的時候比較繁瑣,使用起來像下面這樣:

1

2

3

List<NameValuePair> nvps = new ArrayList <NameValuePair>();

nvps.add(new BasicNameValuePair("key", "value"));

request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));

2.2 讀取響應

正如上文所說,HttpClient 的輸入是一個 Entity,輸出也是一個 Entity 。這和 HttpURLConnection 的流有些不同,但是基本理念是相通的。對於 Entity ,HttpClient 提供給我們一個工具類 EntityUtils,使用它可以很方便的將其轉換為字串。

1

2

CloseableHttpResponse response = httpclient.execute(request);

String responseBody = EntityUtils.toString(response.getEntity());

2.3 HttpEntiy 介面

上面說到了 HttpClient 中的 HttpEntity 這個介面,這個介面在使用 HttpClient 的時候相當重要,這裡對其略做補充。

大多數的 HTTP 請求和響應都會包含兩個部分:頭和體,譬如請求頭請求體,響應頭響應體, Entity 也就是這裡的 “體” 部分,這裡暫且稱之為 “實體” 。一般情況下,請求包含實體的有 POST 和 PUT 方法,而絕大多數的響應都是包含實體的,除了 HEAD 請求的響應,還有 204 No Content、304 Not Modified 和 205 Reset Content 這些不包含實體。

HttpClient 將實體分為三種類型:

  1. 流型別(streamed):實體內容從流中讀取的,通常只能讀一次
  2. 自包含型別(self-contained):手工建立的,通常可重複讀取
  3. 包裝型別(wrapping):使用一種實體包裝另一種實體

上面的例子中我們直接使用工具方法 EntityUtils.toString() 將一個 HttpEntity 轉換為字串,雖然使用起來非常方便,但是要特別注意的是這其實是不安全的做法,要確保返回內容的長度不能太長,如果太長的話,還是建議使用流的方式來讀取:

1

2

3

4

5

6

7

8

9

10

11

12

CloseableHttpResponse response = httpclient.execute(request);

HttpEntity entity = response.getEntity();

if (entity != null) {

long length = entity.getContentLength();

if (length != -1 && length < 2048) {

String responseBody = EntityUtils.toString(entity);

}

else {

InputStream in = entity.getContent();

// read from the input stream ...

}

}

三、關於 HTTPS 請求

到這裡為止,我們一直忽略了 HTTP 請求和 HTTPS 請求之間的差異,因為大多數情況下,我們確實不需要關心 URL 是 HTTP 的還是 HTTPS 的,上面給出的程式碼也都能很好的自動處理這兩種不同型別的請求。

但是,我們還是應該注意下這兩種請求的差異,後面我們在介紹 HTTP 代理時將會特別看到這兩者之間的差異。另外還有一點,在呼叫 URL 的 openConnection() 方法時,如果 URL 是 HTTP 協議的,返回的是一個 HttpURLConnection 物件,而如果 URL 是 HTTPS 協議的,返回的將是一個 HttpsURLConnection 物件。

參考