HttpClient入門及其應用 原

1. 先來看幾個需求

  • 項目中需要與一個基於HTTP協議的第三方的接口進行對接
  • 項目中需要動態的調用WebService服務(不生成本地源碼)
  • 項目中需要利用其它網站的相關數據

這些需求可能或多或少的會發生在平時的開發中,針對每種情況,可能解決方案不止一種。本文將會使用HttpClient這種工具來講解HttpClient的相關知識,以及如何使用HttpClient完成上述需求。

2. HttpClient是什麼

HttpClient是Apache Jakarta Common 下的子項目,可以用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,並且它支持 HTTP 協議最新的版本和建議。(來源於百度百科)

——| 有人說,HttpClient不就是一個瀏覽器嘛。。。

可能不少人對HttpClient會產生這種誤解,他們的觀點是這樣的:既然HttpClient是一個HTTP客戶端編程工具,那不就相當於是一個瀏覽器了嗎?無非它不能把HTML渲染出頁面而已罷了。

其實HttpClient不是瀏覽器,它是一個HTTP通信庫、一個工具包,因此它只提供一個通用瀏覽器應用程序所期望的功能子集。HttpClient與瀏覽器最根本的區別是:HttpClient中沒有用戶界面,瀏覽器需要一個渲染引擎來顯示頁面,並解釋用戶輸入(例如鼠標點擊顯示頁面上的某處之後如何響應、計算如何顯示HTML頁面、級聯樣式表和圖像、javascript解釋器運行嵌入HTML頁面或從HTML頁面引用的javascript代碼、來自用戶界面的事件被傳遞到javascript解釋器進行處理等等等等)。HttpClient只能以編程的方式通過其API用於傳輸和接受HTTP消息,它對內容也是完全不可知的。

3. 爲什麼要用HttpClient,它跟同類產品有什麼區別呢

提到HttpClient,就不得不提jdk原生的URL了。

jdk中自帶了基本的網絡編程,也就是java.net包下的一系列API。通過這些API,也可以完成網絡編程和訪問。

此外,另一個開源項目jsoup,它是一個簡單的HTML解析器,可以直接解析指定URL請求地址的內容,它可以通過DOM方式來取數據,也是比較方便的API。

那既然已經有這些工具了,爲什麼還是有好多好多使用HttpClient的呢?

 

這裏其實是有一個錯誤的認識:Jsoup是解析器不假,但它跟HttpClient不是同類產品(類似Hibernate和MyBatis),實際上日常使用通常會用HttpClient配合Jsoup做網頁爬蟲。

 

HttpClient還是有很多好的特點(摘自Apache HttpClient官網):

  • 基於標準、純淨的java語言。實現了HTTP1.0和HTTP1.1;
  • 以可擴展的面向對象的結構實現了HTTP全部的方法(GET, POST等7種方法);
  • 支持HTTPS協議;
  • 通過HTTP代理建立透明的連接;
  • 利用CONNECT方法通過HTTP代理建立隧道的HTTPS連接;
  • Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos認證方案;
  • 插件式的自定義認證方案;
  • 便攜可靠的套接字工廠使它更容易的使用第三方解決方案;
  • 連接管理器支持多線程應用;支持設置最大連接數,同時支持設置每個主機的最大連接數,發現並關閉過期的連接;
  • 自動處理Set-Cookie中的Cookie;
  • 插件式的自定義Cookie策略;
  • Request的輸出流可以避免流中內容直接緩衝到socket服務器;
  • Response的輸入流可以有效的從socket服務器直接讀取相應內容;
  • 在HTTP1.0和HTTP1.1中利用KeepAlive保持持久連接;
  • 直接獲取服務器發送的response code和 headers;
  • 設置連接超時的能力;
  • 實驗性的支持HTTP1.1 response caching;
  • 源代碼基於Apache License 可免費獲取。

4. HttpClient能幹嘛

正如你所想,上面的需求全部都可以使用HttpClient完成。

HttpClient的功能包括但不限於:

  • 模擬瀏覽器發送HTTP請求,並接收響應
  • RPC接口調用
  • 爬取網頁源碼
  • 批量事務請求
  • …………

 

說的HttpClient那麼好,它究竟怎麼用呢?

5. HttpClient的實際使用

搭建Maven工程,需要導入HttpClient的相關jar包。

注意有兩個HttpClient的工程,都導入,因爲這是兩個不同的項目,而我們在下面的用例中都會用到。

一個是單獨的HttpClient,另一個是commons的HttpClient,不要搞混了哦!

注:下述沒有標註commons的HttpClient都是通常講的HttpClient,只有標註了commons-HttpClient,那纔是工具包下的HttpClient哦(有點繞。。。)

(爲了方便後續的幾個需求,事先導入了Apache的commons相關工具包,jsoup解析器,和fastjson)

<dependencies>
     <dependency>
         <groupId>org.apache.httpcomponents</groupId>
         <artifactId>httpclient</artifactId>
         <version>4.5.6</version>
     </dependency>
     <dependency>
         <groupId>commons-httpclient</groupId>
         <artifactId>commons-httpclient</artifactId>
         <version>3.1</version>
     </dependency>
     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
         <version>3.7</version>
     </dependency>
     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-collections4</artifactId>
         <version>4.2</version>
     </dependency>
     <dependency>
         <groupId>commons-io</groupId>
         <artifactId>commons-io</artifactId>
         <version>2.6</version>
     </dependency>
     <dependency>
         <groupId>org.jsoup</groupId>
         <artifactId>jsoup</artifactId>
         <version>1.11.3</version>
     </dependency>
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.49</version>
     </dependency>
</dependencies>

至於具體的使用,我們來實現一下上面的三個需求吧!

6. HttpClient用例1:接口對接

6.1 基礎Demo

我們使用淘寶網提供的手機歸屬地查詢接口來進行接口對接:

https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=手機號

首先,我們很明顯可以看出這是使用HTTP的get請求。

之後我們來編寫源碼進行接口對接。

public class RpcConsumer {
    public static void main(String[] args) throws Exception {
        //1. 創建HttpClient對象
        CloseableHttpClient client = HttpClients.createDefault();
        //2. 聲明要請求的url,並構造HttpGet請求
        String url = "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13999999999";
        HttpGet get = new HttpGet(url);
        //3. 讓HttpClient去發送get請求,得到響應
        CloseableHttpResponse response = client.execute(get);
        //4. 提取響應正文,並打印到控制檯
        InputStream is = response.getEntity().getContent();
        String ret = IOUtils.toString(is, "GBK");
        System.out.println(ret);
    }
}

難度還是比較小的,但是我們在實際開發中絕對不能這麼寫,url和參數全被寫死了,那你估計也要被打死了(滑稽)。接下來,我們來把這個調用者改爲工具類。

6.2 通用抽取爲工具類

首先,作爲工具類,我們要動態接收url和參數,而不是在代碼中寫死。

構造RpcHttpUtil類,並從中封裝invokeHttp方法如下:

public class RpcHttpUtil {
    public static final String GET = "GET";
    public static final String POST = "POST";
    
    public static Map<String, String> invokeHttp(String url, String method, 
            Map<String, String> paramMap, List<String> returnParamList) throws UnsupportedOperationException, IOException {
        //1. 創建HttpClient對象和響應對象
        CloseableHttpClient client = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        //2. 判斷請求方法是get還是post
        if (StringUtils.equalsIgnoreCase(method, GET)) {
            //2.1 如果是get請求,要拼接請求url的參數
            StringBuilder urlSb = new StringBuilder(url);
            int paramIndex = 0;
            for (Entry<String, String> entry : paramMap.entrySet()) {
                //get請求要追加參數,中間有一個?
                if (paramIndex == 0) {
                    urlSb.append("?");
                }
                //拼接參數
                urlSb.append(entry.getKey() + "=" + entry.getValue() + "&");
            }
            //前面在拼接參數時最後多了一個&,應去掉
            urlSb.delete(urlSb.length() - 1, urlSb.length());
            HttpGet get = new HttpGet(urlSb.toString());
            //2.2 讓HttpClient去發送get請求,得到響應
            response = client.execute(get);
        }else if (StringUtils.equalsIgnoreCase(method, POST)) {
            HttpPost post = new HttpPost(url);
            //2.3 如果是post請求,要構造虛擬表單,並封裝參數
            List<NameValuePair> paramList = new ArrayList<>();
            for (Entry<String, String> entry : paramMap.entrySet()) {
                paramList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
            //2.4 設置請求正文的編碼
            UrlEncodedFormEntity uefEntity = new UrlEncodedFormEntity(paramList, "GBK");
            post.setEntity(uefEntity);
            //2.5 讓HttpClient去發送post請求,得到響應
            response = client.execute(post);
        }else {
            //其他請求類型不支持
            throw new RuntimeException("對不起,該請求方式不支持!");
        }
        //3. 提取響應正文,並封裝成Map
        InputStream is = response.getEntity().getContent();
        Map<String, String> returnMap = new LinkedHashMap<>();
        String ret = IOUtils.toString(is, "GBK");
        //循環正則表達式匹配(因爲有多個參數,無法預處理Pattern)
        for (String param : returnParamList) {
            Pattern pattern = Pattern.compile(param + ":['\"]?.+['\"]?");
            Matcher matcher = pattern.matcher(ret);
            while (matcher.find()) {
                String keyAndValue = matcher.group();
                String value = keyAndValue.substring(keyAndValue.indexOf("'") + 1, keyAndValue.lastIndexOf("'"));
                returnMap.put(param, value);
            }
            //如果沒有匹配到,則put進空串(jdk8的方法)
            returnMap.putIfAbsent(param, "");
        }
        return returnMap;
    }
    
    private RpcHttpUtil() {
    }
}

之後測試方法:

public class RpcConsumer {
    public static void main(String[] args) throws Exception {
        //初始化參數列表和返回值取值列表
        Map<String, String> paramMap = new LinkedHashMap<String, String>() {{
            put("tel", "13999999999");
        }};
        List<String> returnParamList = new ArrayList<String>() {{
            add("province");
        }};
        //調用工具類
        Map<String, String> ret = RpcHttpUtil.invokeHttp(
                "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm", 
                RpcHttpUtil.GET, paramMap, returnParamList);
        System.out.println(ret);
    }
}

運行結果:{province=新疆}

7. HttpClient用例2:動態調用WebService服務(不生成本地源碼)

使用commons-HttpClient,配合SOAP協議,可以實現不生成本地源碼的前提下,也能調用WebService服務。

7.1 爲什麼使用commons-HttpClient可以成功調用WebService服務呢?

我們說,WebService是基於SOAP協議的,我們使用本地源碼發送的請求,其實也就是這些基於SOAP的POST請求,收到的響應也是基於SOAP的響應。

那麼,如果我們自己構造基於SOAP協議的POST請求,是不是服務也就可以正常返回結果呢?當然是肯定的!

不過,唯一不太好的是:自行構造源碼,獲得響應後需要自行解析響應體。

 

接下來我們要先了解SOAP的xml請求體格式,然後才能使用commons-HttpClient進行WebService的請求。

7.2 SOAP協議的請求體xml格式(精簡)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <[method] xmlns="[namaspace]">
            <[args]>[text]</[args]>
        </[method]>
    </soap:Body>
</soap:Envelope>

上面的格式中,方括號內的標識爲具體WebService的請求。

舉個簡單的栗子吧:

url爲http://ws.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl

裏面的namespace要從wsdl中找:

之後構造請求xml(精簡):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <qqCheckOnline xmlns="http://WebXml.com.cn/">
            <qqCode>10000</qqCode>
        </qqCheckOnline>
    </soap:Body>
</soap:Envelope>

7.3 使用commons-HttpClient發送POST請求,調用WebService服務

public class App {
    public static void main(String[] args) throws Exception {
        String url = " http://ws.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("  <soap:Body>");
        sb.append("    <qqCheckOnline xmlns=\" http://WebXml.com.cn/\">");
        sb.append("      <qqCode>10000</qqCode>");
        sb.append("    </qqCheckOnline>");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        System.out.println(soapResponseData);
    }
}

請求結果(響應體真的沒有換行符號,直接一行出來了。。。):

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><qqCheckOnlineResponse xmlns="http://WebXml.com.cn/"><qqCheckOnlineResult>N</qqCheckOnlineResult></qqCheckOnlineResponse></soap:Body></soap:Envelope>

7.4 提取響應數據

我們完全可以使用Dom4j來提取響應體的數據,但是Dom4j只能一層一層的扒,太費勁。Jsoup不僅僅可以解析HTML文檔,也可以進行xml轉換和提取。

之後向剛纔的源碼追加如下內容,便可以只輸出想要的返回結果。

Document document = Jsoup.parse(soapResponseData);
String text = document.getElementsByTag("qqCheckOnlineResult").text();
System.out.println(text);
//輸出結果:N

更詳細的WebService資料,請移步:https://my.oschina.net/LinkedBear/blog/1928400

8. HttpClient用例3:網絡爬蟲

前邊我們說過,HttpClient配合Jsoup可以完成網絡爬蟲的任務,接下來我們來實際做一個爬蟲:爬取京東商城-筆記本電腦的商品信息。

8.1 爬取列表頁

京東商城-筆記本電腦-商品列表頁url:https://list.jd.com/list.html?cat=670,671,672

8.1.1 網頁分析

我們要爬取的位置在這裏:

所有的商品,構成一個ul,每一個商品都是一個li:

可以看出,一個a標籤中嵌套進了這個商品的圖片,我們只需要提取這個a標籤的鏈接即可。

8.1.2 HttpClient+Jsoup爬取列表頁

編寫爬蟲程序如下:

public class Crawler {
    public static void main(String[] args) throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://list.jd.com/list.html?cat=670,671,672");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
        Document document = Jsoup.parse(html);
        Elements goodsDivs = document.getElementsByClass("j-sku-item");
        for (Element goodsDiv : goodsDivs) {
            String href = "https:" + goodsDiv.getElementsByClass("p-img").get(0)
                    .getElementsByTag("a").get(0).attr("href");
            System.out.println(href);
        }
    }
}

可爬取商品鏈接如下:

之後遍歷這些連接,依次進入:

本次我們不做太難的數據處理,只爬取商品名、商品價格以及商品的基本參數。

8.2 爬取單個商品的信息

8.2.1 網頁分析

打開https://item.jd.com/7418428.html,可以提取到相關數據如下:

8.2.2 HttpClient+Jsoup爬取商品信息+詳情

編寫爬蟲程序如下:

public class Crawler2 {
    public static void main(String[] args) throws Exception {
        String goodsId = "7418428";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://item.jd.com/" + goodsId + ".html");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "GBK");
        Document document = Jsoup.parse(html);
        String goodsName = document.getElementsByClass("sku-name").get(0).text();
        System.out.println(goodsName);
        String goodsPrice = document.getElementsByClass("price J-p-" + goodsId).get(0).text();
        System.out.println(goodsPrice);
        Element paramList = document.getElementsByClass("p-parameter").get(0)
                .getElementsByClass("parameter2").get(0);
        Elements params = paramList.getElementsByTag("li");
        for (Element param : params) {
            System.out.println(param.attr("title") + " - " + param.text());
        }
    }
}

爬取結果:

價格沒有拿到!說明價格不在我們當前的頁面請求上,而是ajax請求獲取到的!

需要再用HttpClient請求一次獲取價格的鏈接,纔可以正常獲取商品價格。

加入修改後的商品價格請求的爬蟲源碼如下:

public class Crawler2 {
    public static void main(String[] args) throws Exception {
        String goodsId = "7418428";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://item.jd.com/" + goodsId + ".html");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "GBK");
        Document document = Jsoup.parse(html);
        //取商品名
        String goodsName = document.getElementsByClass("sku-name").get(0).text();
        System.out.println(goodsName);
        //取商品價格
        //String goodsPrice = document.getElementsByClass("price J-p-" + goodsId).get(0).text();
        //System.out.println(goodsPrice);
        //價格屬於ajax請求,需要單獨發送一個請求,獲取價格(此鏈接返回json數組且長度爲1)
        String priceUrl = "http://p.3.cn/prices/get?type=1&skuid=J_" + goodsId;
        HttpPost post = new HttpPost(priceUrl);
        CloseableHttpResponse priceResponse = client.execute(post);
        String jsonStr = IOUtils.toString(priceResponse.getEntity().getContent(), "UTF-8");
        JSONObject json = JSONArray.parseArray(jsonStr).getJSONObject(0);
        System.out.println(json.getString("p"));
        //加載商品詳情
        Element paramList = document.getElementsByClass("p-parameter").get(0)
                .getElementsByClass("parameter2").get(0);
        Elements params = paramList.getElementsByTag("li");
        for (Element param : params) {
            System.out.println(param.attr("title") + " - " + param.text());
        }
    }
}

運行,可以正常獲取結果。

(完)

本文相關源碼可從碼雲獲取:

https://gitee.com/linkedbear/HttpClient-Demo​​​​​​​

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章