1. 程式人生 > >Java爬蟲(二)-- httpClient模擬Http請求+jsoup頁面解析

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-urlencodedapplication/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 headerresponse 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)去賦值,導致報錯。