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());
}
}
}
運行,可以正常獲取結果。
(完)
本文相關源碼可從碼雲獲取: