Java爬蟲(二)-- httpClient模擬Http請求+jsoup頁面解析
前言
在瞭解了爬蟲的大概原理和目前的技術現狀之後,我就開始了java爬蟲的蹣跚之旅。
首先我想到的是用框架,瞭解到的主流的Nutch、webmagic、webcollector等等,都看了一遍,最好懂的是webmagic,因為是國人開發的,有中文文件,看的很舒服。剛開始寫練手的demo之後發現都很舒服,設定好對應爬取規則、爬取深度之後,就能得到想要的資料。
但是當我正式準備開發的時候,很快就發現我的業務場景並不適用於這些框架(Emm..當然也有可能是我太菜了)。
為什麼這麼說呢,讓我們先回到上篇中我摘錄的爬蟲原理,傳統爬蟲從一個或若干初始網頁的URL開始,獲得初始網頁上的URL,在抓取網頁的過程中,不斷從當前頁面上抽取新的URL放入佇列,直到滿足系統的一定停止條件。
也就是,目標資料所在的網頁的url都是在上一層頁面上可以抽取到的,對應到頁面上具體的講法就是,這些連結都是寫在html 標籤的 href 屬性中的,所以可以直接抽取到。
那些demo中被當做抓取物件的網站一般是douban、baidu、zhihu之類的資料很大的公開網站,url都是寫在頁面上的,而我的目標網站時險企開放給代理公司的網站,具有不公開、私密的性質,一個頁面轉到下一個頁面的請求一般都是通過js動態生成url發起的,並且很多是post請求。
雖然那些框架有很多優越誘人的特性和功能,本著先滿足需求,在進行優化的原則,我準備先用比較底層的工具一步步的模擬這些http請求。
正好,我發現webmagic底層模擬請求的工具用的就是Apache HttpClient,所以就用這個工具來模擬了。
HttpClient
HttpClient
是 Apache Jakarta Common 下的子專案,用來提供高效的、最新的、功能豐富的支援 HTTP 協議的客戶端程式設計工具包。它相比傳統的 HttpURLConnection
,增加了易用性和靈活性,它不僅讓客戶端傳送 HTTP 請求變得更容易,而且也方便了開發人員測試介面(基於 HTTP 協議的),即提高了開發的效率,也方便提高程式碼的健壯性
在搜尋相關資料的時候,會發現網上有兩種HttpClient。
org.apache.commons.httpclient.HttpClient與org.apache.http.client.HttpClient的區別:Commons的HttpClient專案現在是生命的盡頭,不再被開發,已被Apache HttpComponents專案HttpClient和的HttpCore模組取代,提供更好的效能和更大的靈活性
所以在查詢的時候別搞混了哦,英語好的同學推薦閱讀HttpClient的官方文件
實戰
所有HTTP請求都有由方法名,請求URI和HTTP協議版本組成的請求行。
HttpClient支援開箱即用HTTP/1.1規範中定義的所有HTTP方法:GET, HEAD,POST, PUT, DELETE,TRACE and OPTIONS。它們都有一個特定的類對應這些方法型別: HttpGet,HttpHead, HttpPost,HttpPut, HttpDelete,HttpTrace, and HttpOptions.
請求的URI是統一資源定位符,它標識了應用於哪個請求之上的資源。HTTP請求的URI包含協議方案,主機名,可選的埠,資源路徑,可選查詢和可選片段。
在開發過程中,主要處理都是get和post請求。
HTTP GET
模擬get請求
public static String sendGet(String url) {
CloseableHttpClient httpclient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String content = null;
try {
HttpGet get = new HttpGet(url);
response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
content = EntityUtils.toString(entity);
EntityUtils.consume(entity);
return content;
} catch (Exception e) {
e.printStackTrace();
if (response != null) {
try {
response.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return content;
}
url可以自己直接寫上去,包括包含的引數。例如:http://www.test.com/test?msg=hello&type=test
HttpClient 提供 URIBuilder 實用類來簡化請求 URL的建立和修改.
URI uri = new URIBuilder()
.setScheme("http")
.setHost("www.test.com")
.setPath("/test")
.setParameter("msg", "hello")
.setParameter("type", "test")
.build();
HttpGet httpget = new HttpGet(uri);
HTTP POST
傳送POST請求時,需要向伺服器寫入一段資料。這裡使用setEntity()
函式來寫入資料:
按照自己的經驗,傳送的資料由你要模擬的請求,按請求頭中Content-type
來分,可以分為application/x-www-form-urlencoded
和application/json
對應常見的HTML表單提交和json資料提交
// application/x-www-form-urlencoded
public static String sendPost(HttpPost post, List<NameValuePair> nvps) {
CloseableHttpClient httpclient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String content = null;
try {
// nvps是包裝請求引數的list
if (nvps != null) {
post.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
}
// 執行請求用execute方法,content用來幫我們附帶上額外資訊
response = httpClient.execute(post);
// 得到相應實體、包括響應頭以及相應內容
HttpEntity entity = response.getEntity();
// 得到response的內容
content = EntityUtils.toString(entity);
EntityUtils.consume(entity);
return content;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content;
}
// application/json
public static String sendPostJson (String url, JSONObject object) {
HttpPost httpPost = new HttpPost(url);
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
// json方式
StringEntity entity = new StringEntity(object.toString(),"utf-8");//解決中文亂碼問題
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json;charset=UTF-8");
httpPost.setEntity(entity);
HttpResponse resp = httpClient.execute(httpPost);
if(resp.getStatusLine().getStatusCode() == 200) {
HttpEntity he = resp.getEntity();
return EntityUtils.toString(he,"UTF-8");
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
HttpEntiy介面
Entity 是 HttpClient
中的一個特別的概念,有著各種的 Entity ,都實現自 HttpEntity 介面,輸入是一個 Entity,輸出也是一個 Entity 。這和 HttpURLConnection 的流有些不同,但是基本理念是相通的。對於 Entity ,HttpClient 提供給我們一個工具類 EntityUtils,使用它可以很方便的將其轉換為字串。
大多數的 HTTP 請求和響應都會包含兩個部分:頭和體,譬如請求頭請求體,響應頭響應體, Entity 也就是這裡的 “體” 部分,這裡暫且稱之為 “實體” 。一般情況下,請求包含實體的有 POST 和 PUT 方法,而絕大多數的響應都是包含實體的,除了 HEAD 請求的響應,還有 204 No Content、304 Not Modified 和 205 Reset Content 這些不包含實體。
HttpClient 將實體分為三種類型:
streamed(流式): 從流中獲取或者是動態生成內容。尤其是這個型別包含了從HTTP響應中獲取的實體。流式實體是不可重複生成的。
self-contained(自包含式): 通過記憶體、使用獨立的連線、其他實體的方式來獲得內容。自包含實體可以重複生成。這種型別的實體將主要被用於封閉HTTP請求。
wrapping(包裝式): 通過其他實體來獲得內容.
上面的例子中我們直接使用工具方法 EntityUtils.toString() 將一個 HttpEntity 轉換為字串,雖然使用起來非常方便,但是要特別注意的是這其實是不安全的做法,要確保返回內容的長度不能太長,如果太長的話,還是建議使用流的方式來讀取:
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 ...
}
}
HTTP Header
HTTP Header 分為request header
和response header
。在我自己開發的時候,有時候需要把一次request header
都模擬了,因為伺服器端有可能會對請求的header進行驗證,有些網頁還會根據User-Agent不同返回不同的頁面內容。也有時候需要對response header
進行解析,因為伺服器會將用於下一步驗證所需的祕鑰放在header中返回給客戶端。
新增頭部資訊:
HttpPost post = new HttpPost(url);
post.setHeader("Content-Type", "application/json;charset=UTF-8");
post.setHeader("Host", "www.test.com.cn");
addHeader()
和setHeader()
,前者是新增頭部資訊,後者可以新增或者修改頭部資訊。
讀取頭部資訊:
HttpResponse resp = httpClient.execute(···);
// 讀取指定header的第一個值
resp.getFirstHeader(headerName).getValue();
// 讀取指定header的最後一個值
resp.getLastHeader(headerName).getValue();
// 讀取指定header
resp.getHeaders(headerName);
// 讀取所有的header
resp.getAllHeaders();
頁面解析
頁面解析需要講的東西太少,就直接放到這一章裡面一起講了。
前面講了怎麼用httpClient模擬Http請求,那怎麼從html頁面拿到我們想要的資料呢。
這裡就引出了jsoup頁面解析工具。
jsoup
Jsoup是一款 Java 的 HTML 解析器,可直接解析某個 URL 地址、HTML 文字內容。它提供了一套非常省力的 API,可通過 DOM,CSS 以及類似於 jQuery 的操作方法來取出和操作資料。
以www.csdn.com為例。
如果我要獲取當前選中元素中的標題文字。
String page = "..."; // 假設這是csdn頁面的html
Document doc = Jsoup.parse(page); //得到document物件
Element feedlist = doc.select("#feedlist_id").get(0); // 獲取父級元素
String title = feedlist.select("a").get(0).text(); // 獲取第一個a標籤的內容
// 如果是input之類的標籤,取value值就是用val()方法
上述程式碼用的是css選擇器的方法,熟悉前端dom操作的童鞋們應該是蠻熟悉的。同時jsoup也支援直接獲取dom元素的方法。
// 通過Class屬性來定位元素,獲取的是所有帶這個class屬性的集合
getElementsByClass()
// 通過標籤名字來定位元素,獲取的是所有帶有這個標籤名字的元素結合
getElementsByTag();
// 通過標籤的ID來定位元素,這個是精準定位,因為頁面的ID基本不會重複
getElementById();
// 通過屬性和屬性名來定位元素,獲取的也是一個滿足條件的集合;
getElementsByAttributeValue();
// 通過正則匹配屬性
getElementsByAttributeValueMatching()
正則表示式
正則表示式實際上也是頁面解析中非常好用的一種方式,主要是因為我在分析我需要抓取資料的頁面上發現,我需要的資料並不在dom元素中,而是在js指令碼中,所以直接用正則表示式獲取會比較方便。
Matcher matcher;
String page; = "..."; // 頁面html
String regex = "..."; // 正則表示式
matcher = Pattern.compile(regex).matcher(page);
if (matcher.find())
// 子詢價單號
String rst = matcher.group(1);
剛開始犯了一個很傻的錯誤,沒有執行matcher.find()
方法就直接用matcher.group(1)
去賦值,導致報錯。