Java 和 HTTP 的那些事之模擬 HTTP 請求
一、使用 HttpURLConnection 傳送 HTTP 請求
Java 自帶的 java.net
這個包中包含了很多與網路請求相關的類,但是對於我們來說,最關心的應該是 HttpURLConnection
這個類了。
1.1 建立 HTTP 連線物件
要得到一個 HttpURLConnection
HTTP 連線物件,首先需要一個 URL
,程式碼如下:
1 2 |
|
1.2 新增 HTTP 請求頭
得到 HTTP 連線物件之後,我們就可以進行 HTTP 操作了,我們可以新增任意的 HTTP 請求頭,然後執行我們需要的 GET 或者 POST 請求。我們像下面這樣,新增兩個 HTTP 頭(User-Agent 和 Accept-Language):
1 2 |
"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 |
|
可以看到,程式碼非常簡潔,沒有任何累贅的程式碼,甚至沒有任何和傳送請求相關的程式碼,請求就是在 getResponseCode()
函式中默默的執行了。其中 readResponseBody()
函式用於讀取流並轉換為字串,具體的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1.4 HTTP POST
使用 HttpURLConnection
來模擬 POST 請求和 GET 請求基本上是一樣的,但是有一點不同,由於 POST 請求一般都會向服務端傳送一段資料,所以 HttpURLConnection
提供了一個方法 setDoOutput(true)
來表示有資料要輸出給服務端,並可以通過 getOutputStream()
得到輸出流,我們將要寫的資料通過這個輸出流 POST 到服務端。
1 2 3 4 5 6 |
|
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 |
|
2.1 HTTP GET 與 HTTP POST
上面的示例程式碼展示瞭如何使用 HttpClient
來模擬 HTTP GET 請求,可以看出 HttpClient
對每一種 HTTP 方法都準備了一個類,GET 請求使用 HttpGet
類,POST 請求使用 HttpPost
類。
和上文中介紹的一樣,在傳送 POST 請求時,需要向服務端寫入一段資料,我們這裡使用 setEntity()
函式來寫入資料:
1 2 3 4 5 |
|
Entity 是 HttpClient
中的一個特別的概念,有著各種的 Entity ,都實現自 HttpEntity 介面,輸入是一個 Entity,輸出也是一個 Entity 。要注意的是,在這裡我採用一種取巧的方式,直接使用 StringEntity
來寫入 POST 資料,然後將 Content-type 改成 application/x-www-form-urlencoded ,這樣就和瀏覽器裡的表單提交請求一致了。但是我們要知道的是,一般情況下,我們可能還會使用 UrlEncodedFormEntity
這個類,只是在寫爬蟲的時候比較繁瑣,使用起來像下面這樣:
1 2 3 |
|
2.2 讀取響應
正如上文所說,HttpClient
的輸入是一個 Entity,輸出也是一個 Entity 。這和 HttpURLConnection
的流有些不同,但是基本理念是相通的。對於 Entity ,HttpClient
提供給我們一個工具類 EntityUtils
,使用它可以很方便的將其轉換為字串。
1 2 |
|
2.3 HttpEntiy 介面
上面說到了 HttpClient
中的 HttpEntity
這個介面,這個介面在使用 HttpClient 的時候相當重要,這裡對其略做補充。
大多數的 HTTP 請求和響應都會包含兩個部分:頭和體,譬如請求頭請求體,響應頭響應體, Entity 也就是這裡的 “體” 部分,這裡暫且稱之為 “實體” 。一般情況下,請求包含實體的有 POST 和 PUT 方法,而絕大多數的響應都是包含實體的,除了 HEAD 請求的響應,還有 204 No Content、304 Not Modified 和 205 Reset Content 這些不包含實體。
HttpClient
將實體分為三種類型:
- 流型別(streamed):實體內容從流中讀取的,通常只能讀一次
- 自包含型別(self-contained):手工建立的,通常可重複讀取
- 包裝型別(wrapping):使用一種實體包裝另一種實體
上面的例子中我們直接使用工具方法 EntityUtils.toString()
將一個 HttpEntity 轉換為字串,雖然使用起來非常方便,但是要特別注意的是這其實是不安全的做法,要確保返回內容的長度不能太長,如果太長的話,還是建議使用流的方式來讀取:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
三、關於 HTTPS 請求
到這裡為止,我們一直忽略了 HTTP 請求和 HTTPS 請求之間的差異,因為大多數情況下,我們確實不需要關心 URL 是 HTTP 的還是 HTTPS 的,上面給出的程式碼也都能很好的自動處理這兩種不同型別的請求。
但是,我們還是應該注意下這兩種請求的差異,後面我們在介紹 HTTP 代理時將會特別看到這兩者之間的差異。另外還有一點,在呼叫 URL
的 openConnection()
方法時,如果 URL 是 HTTP 協議的,返回的是一個 HttpURLConnection 物件,而如果 URL 是 HTTPS 協議的,返回的將是一個 HttpsURLConnection 物件。