前言
在了解了爬蟲的大概原理和目前的技術(shù)現(xiàn)狀之后念赶,我就開始了java爬蟲的蹣跚之旅晶乔。
首先我想到的是用框架牺勾,了解到的主流的Nutch、webmagic翻具、webcollector等等回还,都看了一遍柠硕,最好懂的是webmagic,因為是國人開發(fā)的闻葵,有中文文檔癣丧,看的很舒服胁编。剛開始寫練手的demo之后發(fā)現(xiàn)都很舒服,設(shè)置好對應(yīng)爬取規(guī)則早直、爬取深度之后市框,就能得到想要的數(shù)據(jù)。
但是當(dāng)我正式準(zhǔn)備開發(fā)的時候祥得,很快就發(fā)現(xiàn)我的業(yè)務(wù)場景并不適用于這些框架(Emm..當(dāng)然也有可能是我太菜了)级及。
為什么這么說呢额衙,讓我們先回到上篇中我摘錄的爬蟲原理窍侧,傳統(tǒng)爬蟲從一個或若干初始網(wǎng)頁的URL開始,獲得初始網(wǎng)頁上的URL硼啤,在抓取網(wǎng)頁的過程中斧账,不斷從當(dāng)前頁面上抽取新的URL放入隊列,直到滿足系統(tǒng)的一定停止條件咧织。
也就是,目標(biāo)數(shù)據(jù)所在的網(wǎng)頁的url都是在上一層頁面上可以抽取到的渠抹,對應(yīng)到頁面上具體的講法就是梧却,這些鏈接都是寫在html 標(biāo)簽的 href 屬性中的桃煎,所以可以直接抽取到。
那些demo中被當(dāng)做抓取對象的網(wǎng)站一般是douban三椿、baidu搜锰、zhihu之類的數(shù)據(jù)很大的公開網(wǎng)站耿战,url都是寫在頁面上的,而我的目標(biāo)網(wǎng)站時險企開放給代理公司的網(wǎng)站狈涮,具有不公開歌馍、私密的性質(zhì),一個頁面轉(zhuǎn)到下一個頁面的請求一般都是通過js動態(tài)生成url發(fā)起的暴浦,并且很多是post請求歌焦。
雖然那些框架有很多優(yōu)越誘人的特性和功能砚哆,本著先滿足需求窟社,在進行優(yōu)化的原則,我準(zhǔn)備先用比較底層的工具一步步的模擬這些http請求关炼。
正好匣吊,我發(fā)現(xiàn)webmagic底層模擬請求的工具用的就是Apache HttpClient
色鸳,所以就用這個工具來模擬了。
HttpClient
HttpClient
是 Apache Jakarta Common 下的子項目蒜哀,用來提供高效的撵儿、最新的狐血、功能豐富的支持 HTTP 協(xié)議的客戶端編程工具包。它相比傳統(tǒng)的 HttpURLConnection
浪默,增加了易用性和靈活性纳决,它不僅讓客戶端發(fā)送 HTTP 請求變得更容易,而且也方便了開發(fā)人員測試接口(基于 HTTP 協(xié)議的)花竞,即提高了開發(fā)的效率掸哑,也方便提高代碼的健壯性
在搜索相關(guān)資料的時候苗分,會發(fā)現(xiàn)網(wǎng)上有兩種HttpClient牵辣。
org.apache.commons.httpclient.HttpClient
與org.apache.http.client.HttpClient
的區(qū)別:Commons的HttpClient項目現(xiàn)在是生命的盡頭纬向,不再被開發(fā)逾条,已被Apache HttpComponents
項目HttpClient
和HttpCore
模組取代,提供更好的性能和更大的靈活性
所以在查找的時候別搞混了哦担孔,英語好的同學(xué)推薦閱讀HttpClient的官方文檔
實戰(zhàn)
所有HTTP請求都有由方法名糕篇,請求URI和HTTP協(xié)議版本組成的請求行酌心。
HttpClient支持開箱即用HTTP/1.1規(guī)范中定義的所有HTTP方法:GET
,HEAD
,POST
, PUT
, DELETE
,TRACE
and OPTIONS
安券。它們都有一個特定的類對應(yīng)這些方法類型: HttpGet
,HttpHead
, HttpPost
,HttpPut
, HttpDelete
,HttpTrace
, and
HttpOptions`.
請求的URI是統(tǒng)一資源定位符,它標(biāo)識了應(yīng)用于哪個請求之上的資源泰鸡。HTTP請求的URI包含協(xié)議方案盛龄,主機名,可選的端口啊鸭,資源路徑赠制,可選查詢和可選片段挟憔。
在開發(fā)過程中绊谭,主要處理都是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可以自己直接寫上去,包括包含的參數(shù)宪赶。例如:http://www.test.com/test?msg=hello&type=test
HttpClient 提供 URIBuilder
來簡化請求 URL的創(chuàng)建和修改.
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
發(fā)送POST請求時搂妻,需要向服務(wù)器寫入一段數(shù)據(jù)叽讳。這里使用setEntity()
函數(shù)來寫入數(shù)據(jù):
按照自己的經(jīng)驗,發(fā)送的數(shù)據(jù)由你要模擬的請求邑狸,按請求頭中Content-type
來分单雾,可以分為application/x-www-form-urlencoded
和application/json
對應(yīng)常見的HTML表單提交和json數(shù)據(jù)提交
// 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是包裝請求參數(shù)的list
if (nvps != null) {
post.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
}
// 執(zhí)行請求用execute方法硅堆,content用來幫我們附帶上額外信息
response = httpClient.execute(post);
// 得到相應(yīng)實體贿讹、包括響應(yīng)頭以及相應(yīng)內(nèi)容
HttpEntity entity = response.getEntity();
// 得到response的內(nèi)容
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 面殖,都實現(xiàn)自 HttpEntity 接口,輸入是一個 Entity相叁,輸出也是一個 Entity 辽幌。這和 HttpURLConnection 的流有些不同乌企,但是基本理念是相通的。對于 Entity ,HttpClient 提供給我們一個工具類 EntityUtils虽画,使用它可以很方便的將其轉(zhuǎn)換為字符串荣病。
大多數(shù)的 HTTP 請求和響應(yīng)都會包含兩個部分:頭和體个盆,譬如請求頭請求體颊亮,響應(yīng)頭響應(yīng)體, Entity 也就是這里的 “體” 部分腐泻,這里暫且稱之為 “實體” 失驶。一般情況下强饮,請求包含實體的有 POST 和 PUT 方法,而絕大多數(shù)的響應(yīng)都是包含實體的偿渡,除了 HEAD 請求的響應(yīng),還有 204 No Content霸奕、304 Not Modified 和 205 Reset Content 這些不包含實體溜宽。
HttpClient 將實體分為三種類型:
streamed(流式): 從流中獲取或者是動態(tài)生成內(nèi)容。尤其是這個類型包含了從HTTP響應(yīng)中獲取的實體质帅。流式實體是不可重復(fù)生成的适揉。
self-contained(自包含式): 通過內(nèi)存、使用獨立的連接涡扼、其他實體的方式來獲得內(nèi)容稼跳。自包含實體可以重復(fù)生成。這種類型的實體將主要被用于封閉HTTP請求吃沪。
wrapping(包裝式): 通過其他實體來獲得內(nèi)容.
上面的例子中我們直接使用工具方法 EntityUtils.toString()
將一個 HttpEntity 轉(zhuǎn)換為字符串汤善,雖然使用起來非常方便,但是要特別注意的是這其實是不安全的做法票彪,要確保返回內(nèi)容的長度不能太長红淡,如果太長的話,還是建議使用流的方式來讀冉抵:
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
在旱。在我自己開發(fā)的時候,有時候需要把一次request header
都模擬了推掸,因為服務(wù)器端有可能會對請求的header進行驗證桶蝎,有些網(wǎng)頁還會根據(jù)User-Agent不同返回不同的頁面內(nèi)容。也有時候需要對response header
進行解析谅畅,因為服務(wù)器會將用于下一步驗證所需的秘鑰放在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頁面拿到我們想要的數(shù)據(jù)呢。
這里就引出了jsoup頁面解析工具丹墨。
jsoup
Jsoup是一款 Java 的 HTML 解析器廊遍,可直接解析某個 URL 地址、HTML 文本內(nèi)容贩挣。它提供了一套非常省力的 API昧碉,可通過 DOM,CSS 以及類似于 jQuery 的操作方法來取出和操作數(shù)據(jù)揽惹。
以www.csdn.com為例被饿。
如果我要獲取當(dāng)前選中元素中的標(biāo)題文字。
String page = "..."; // 假設(shè)這是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標(biāo)簽的內(nèi)容
// 如果是input之類的標(biāo)簽搪搏,取value值就是用val()方法
上述代碼用的是css選擇器的方法狭握,熟悉前端dom操作的童鞋們應(yīng)該是蠻熟悉的。同時jsoup也支持直接獲取dom元素的方法疯溺。
// 通過Class屬性來定位元素论颅,獲取的是所有帶這個class屬性的集合
getElementsByClass()
// 通過標(biāo)簽名字來定位元素哎垦,獲取的是所有帶有這個標(biāo)簽名字的元素結(jié)合
getElementsByTag();
// 通過標(biāo)簽的ID來定位元素,這個是精準(zhǔn)定位恃疯,因為頁面的ID基本不會重復(fù)
getElementById();
// 通過屬性和屬性名來定位元素漏设,獲取的也是一個滿足條件的集合;
getElementsByAttributeValue();
// 通過正則匹配屬性
getElementsByAttributeValueMatching()
正則表達式
正則表達式實際上也是頁面解析中非常好用的一種方式,主要是因為我在分析我需要抓取數(shù)據(jù)的頁面上發(fā)現(xiàn)今妄,我需要的數(shù)據(jù)并不在dom元素中郑口,而是在js腳本中,所以直接用正則表達式獲取會比較方便盾鳞。
Matcher matcher;
String page; = "..."; // 頁面html
String regex = "..."; // 正則表達式
matcher = Pattern.compile(regex).matcher(page);
if (matcher.find())
// 子詢價單號
String rst = matcher.group(1);
剛開始犯了一個很傻的錯誤犬性,沒有執(zhí)行matcher.find()
方法就直接用matcher.group(1)
去賦值,導(dǎo)致報錯腾仅。
這里推薦一個正則表達式工具.