java爬蟲(二)-- httpClient模擬Http請求+jsoup頁面解析

前言

在了解了爬蟲的大概原理和目前的技術(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.HttpClientorg.apache.http.client.HttpClient的區(qū)別:Commons的HttpClient項目現(xiàn)在是生命的盡頭纬向,不再被開發(fā)逾条,已被Apache HttpComponents項目HttpClientHttpCore模組取代,提供更好的性能和更大的靈活性

所以在查找的時候別搞混了哦担孔,英語好的同學(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, andHttpOptions`.

請求的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-urlencodedapplication/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 headerresponse 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為例被饿。

image

如果我要獲取當(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)致報錯腾仅。

這里推薦一個正則表達式工具.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乒裆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子推励,更是在濱河造成了極大的恐慌鹤耍,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件验辞,死亡現(xiàn)場離奇詭異惰蜜,居然都是意外死亡,警方通過查閱死者的電腦和手機受神,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來格侯,“玉大人鼻听,你說我怎么就攤上這事×模” “怎么了撑碴?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長朝墩。 經(jīng)常有香客問我醉拓,道長,這世上最難降的妖魔是什么收苏? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任亿卤,我火速辦了婚禮,結(jié)果婚禮上鹿霸,老公的妹妹穿的比我還像新娘排吴。我一直安慰自己,他們只是感情好懦鼠,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布钻哩。 她就那樣靜靜地躺著屹堰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪街氢。 梳的紋絲不亂的頭發(fā)上扯键,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機與錄音珊肃,去河邊找鬼荣刑。 笑死,一個胖子當(dāng)著我的面吹牛近范,可吹牛的內(nèi)容都是我干的嘶摊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼评矩,長吁一口氣:“原來是場噩夢啊……” “哼叶堆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起斥杜,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤虱颗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蔗喂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忘渔,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年缰儿,在試婚紗的時候發(fā)現(xiàn)自己被綠了畦粮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡乖阵,死狀恐怖宣赔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞪浸,我是刑警寧澤儒将,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站对蒲,受9級特大地震影響钩蚊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹈矮,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一砰逻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泛鸟,春花似錦诱渤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽递胧。三九已至,卻和暖如春赡茸,著一層夾襖步出監(jiān)牢的瞬間缎脾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工占卧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留遗菠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓华蜒,卻偏偏與公主長得像辙纬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叭喜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理贺拣,服務(wù)發(fā)現(xiàn),斷路器捂蕴,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 組織:中國互動出版網(wǎng)(http://www.china-pub.com/) RFC文檔中文翻譯計劃(http://...
    Palomar閱讀 1,573評論 0 6
  • API定義規(guī)范 本規(guī)范設(shè)計基于如下使用場景: 請求頻率不是非常高:如果產(chǎn)品的使用周期內(nèi)請求頻率非常高譬涡,建議使用雙通...
    有涯逐無涯閱讀 2,547評論 0 6
  • 一年前,朋友搬出了父母家啥辨。與此同時涡匀,某天,她在路邊撿到了一條泰迪犬溉知。 其實陨瘩,她并沒有什么錢,每個月的工資级乍,除去必須...
    抒簽閱讀 412評論 0 6
  • 其實 每天早上醒來之后都想再接著睡一會兒舌劳。為什么想要再睡一會兒呢,我的原因是因為幾乎每天我都會做夢...做完...
    Juststandthere閱讀 110評論 0 1