當咱們作一些Android練手項目時,苦於無數據,這時候能夠試試Jsoup爬蟲,爬取任何網頁上數據來豐富你App的內容;jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文本內容。它提供了一套很是省力的API,可經過DOM,CSS以及相似於jQuery的操做方法來取出和操做數據。使用起來也很是簡單:html
String html = "<html><head><title>First parse</title></head>"
+ "<body><p>Parsed HTML into a doc.</p></body></html>";
Document doc = Jsoup.parse(html);
複製代碼
其解析器可以盡最大可能從你提供的HTML文檔來創見一個乾淨的解析結果,不管HTML的格式是否完整。java
可是用Jsoup選擇某個節點有個問題,例如在很深的節點下用Jsoup選擇代碼以下:node
doc.select("body div.nav.head-nav.avt.clearfloat li.cat-item.cat-item")
複製代碼
若是不熟悉JQuery選擇器,請參考:JQuery選擇器jquery
一條選擇語句看着就很凌亂,若是多條的話,可想而知,看起來就很難看,並且發生錯誤很難糾錯,因此就就Gson模仿解析Json的方式,製作一個工具,經過註解和反射去解析凌亂的選擇器.Jsoup更多用法請參考中文官網android
首先定義一個註解類:git
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Pick {
String value() default "";
String attr() default Attrs.TEXT;
}
複製代碼
對註解知識不熟悉的情參考Java Annotation 及幾個經常使用開源項目註解原理簡析github
註解在RUNTIME時執行,做用對象爲成員變量和類,經過@Pick()去註解實體類網絡
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">
<head>
</head>
<body>
<div class="nav head-nav avt clearfloat">
<li class="on"><a href=/>首頁</a>
</li>
<li class="cat-item cat-item-1 "><a href="/movie/" title="最新電影">最新電影</a></li>
<li class="cat-item cat-item-2 "><a href="/television/" title="最新電視">最新電視</a></li>
<li class="cat-item cat-item-3 "><a href="/dongman/" title="動漫動畫">動漫動畫</a></li>
<li class="cat-item cat-item-4 "><a href="/video/" title="綜藝娛樂">綜藝娛樂</a></li>
<li class="cat-item cat-item-5 "><a href="/movie/dldy/" title="大陸電影">大陸電影</a></li>
<li class="cat-item cat-item-6 "><a href="/movie/gtdy/" title="港臺電影">港臺電影</a></li>
<li class="cat-item cat-item-7 "><a href="/movie/rhdy/" title="日韓電影">日韓電影</a></li>
<li class="cat-item cat-item-8 "><a href="/movie/omdy/" title="歐美電影">歐美電影</a></li>
<li class="cat-item cat-item-9 "><a href="/television/dljj/" title="大陸劇集">大陸劇集</a></li>
<li class="cat-item cat-item-10 "><a href="/television/gtjj/" title="港臺劇集">港臺劇集</a></li>
<li class="cat-item cat-item-11 "><a href="/television/rhjj/" title="日韓劇集">日韓劇集</a></li>
<li class="cat-item cat-item-12 "><a href="/television/omjj/" title="歐美劇集">歐美劇集</a></li>
</div>
</body>
</html>
複製代碼
當咱們在解析這樣一個Html片斷時,咱們須要的內容有href以及title的一個List,那咱們就能夠編寫實體類了ide
@Pick("body")
public class Entity {
@Pick("div.nav.head-nav.avt.clearfloat li.cat-item.cat-item")
private List<SortEntity> sortList=new ArrayList<>();
public static class SortEntity {
@Pick(value = "a")
private String name = "";
@Pick(value = "a", attr = Attrs.HREF)
private String linkUrl = "";
public SortEntity() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLinkUrl() {
return linkUrl;
}
public void setLinkUrl(String linkUrl) {
this.linkUrl = linkUrl;
}
}
}
複製代碼
須要注意的一點是:若是Entity是內部類,那麼Entity必定要用public static修飾工具
實現思路:經過body註解類Entity,就至關於用doc.select("body") ,而後用其返回值,一層層的向下解析,直到徹底解析,解析思路有了,那麼就開始編寫解析類去經過反射與註解來解析Entity類,實現過程以下:
public class JsoupUtils {
//傳入待解析的字符串與編寫好的實體類
public <T> T fromHTML(String html, Class<T> clazz) {
T t = null;
Pick pickClazz;
try {
//先用Jsoup實例化待解析的字符串
Document rootDocument = Jsoup.parse(html);
//獲取實體類的的註解
pickClazz = clazz.getAnnotation(Pick.class);
//構建一個實體類的無參構造方法並生成實例
t = clazz.getConstructor().newInstance();
//獲取註解的一些參數
String clazzAttr = pickClazz.attr();
String clazzValue = pickClazz.value();
//用Jsoup選擇到待解析的節點
Element rootNode = getRootNode(rootDocument, clazzValue);
//獲取實體類的全部成員變量
Field[] fields = clazz.getDeclaredFields();
//遍歷並解析這些成員變量
for (Field field : fields) {
dealFieldType(field, rootNode, t);
}
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
private Field dealFieldType(Field field, Element rootNode, Object t) throws Exception {
//設置成員變量爲可修改的
field.setAccessible(true);
Pick pickField = field.getAnnotation(Pick.class);
if (pickField == null) return null;
String fieldValue = pickField.value();
String fieldAttr = pickField.attr();
//獲取field的類型
Class<?> type = field.getType();
//目前此工具類只能解析兩種類型的成員變量,一種是String的,另外一種是帶泛型參數的List,泛型參數必須是自定義
//子實體類,或者String,自定義子實體類若是是內部類,必須用public static修飾
if (type == String.class) {
String nodeValue = getStringNode(rootNode, fieldAttr, fieldValue);
Filter filterField = field.getAnnotation(Filter.class);
if (filterField != null) {
String filter = filterField.filter();
boolean isFilter = filterField.isFilter();
boolean isMatcher = RegexUtils.getRegexBoolean(nodeValue, filter);
if (isFilter && isMatcher) {
field.set(t, nodeValue);
} else {
return null;
}
} else {
field.set(t, nodeValue);
}
} else if (type == List.class) {
Elements elements = getListNode(rootNode, fieldValue);
field.set(t, new ArrayList<>());
List<Object> fieldList = (List<Object>) field.get(t);
for (Element ele : elements) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) genericType).getActualTypeArguments();
Class<?> aClass = Class.forName(((Class) args[0]).getName());
Object object = aClass.newInstance();
Field[] childFields = aClass.getDeclaredFields();
for (Field childField : childFields) {
dealFieldType(childField, ele, object);
}
fieldList.add(object);
}
}
field.set(t, fieldList);
}
return field;
}
/**
* 獲取一個Elements對象
*/
private Elements getListNode(Element rootNode, String fieldValue) {
return rootNode.select(fieldValue);
}
/**
* 獲取返回值爲String的節點
*
* 因爲Jsoup不支持JQuery的一些語法結構,例如 :first :last,因此這裏手動處理了下,本身可參考JQuery選擇器
* 擴展其功能
*/
private String getStringNode(Element rootNode, String fieldAttr, String fieldValue) {
if (fieldValue.contains(":first")) {
fieldValue = fieldValue.replace(":first", "");
if (Attrs.TEXT.equals(fieldAttr))
return rootNode.select(fieldValue).first().text();
return rootNode.select(fieldValue).first().attr(fieldAttr);
} else if (fieldValue.contains(":last")) {
fieldValue = fieldValue.replace(":last", "");
if (Attrs.TEXT.equals(fieldAttr))
return rootNode.select(fieldValue).last().text();
return rootNode.select(fieldValue).last().attr(fieldAttr);
} else {
if (Attrs.TEXT.equals(fieldAttr))
return rootNode.select(fieldValue).text();
return rootNode.select(fieldValue).attr(fieldAttr);
}
}
/**
* 獲取根節點,一般在類的註解上使用
*/
private Element getRootNode(Document rootDocument, String value) {
return rootDocument.selectFirst(value);
}
}
複製代碼
在JsoupUtils已經將全部的解析工做都已經完成,重要實現步驟在上面都用註釋
咱們使用的方法就是Entity entity=new JsoupUtils().fromHtml(htmlStr,Entity.class)是否是很熟悉,相似於Gson解析Json的寫法,用戶只須要編寫實體類,而後就會自動對實體的各項進行賦值
如今網絡請求大多數都是使用Retrofit2+Rxjava2那麼有沒有辦法像GsonConverterFactory.create()直接將返回的數據轉換爲實體類,答案是有的,經過自定義Converter
public class HtmlConverterFactory extends Converter.Factory {
private JsoupUtils mPicker;
public static HtmlConverterFactory create(JsoupUtils fruit) {
return new HtmlConverterFactory(fruit);
}
public static HtmlConverterFactory create() {
return new HtmlConverterFactory(new JsoupUtils());
}
private HtmlConverterFactory(JsoupUtils fruit) {
mPicker = fruit;
}
//咱們只須要對返回值作修改,因此僅重寫responseBodyConverter方法
@Override
public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
return new HtmlResponseBodyConverter<>(mPicker, type);
}
}
複製代碼
自定義HtmlConverterFactory類繼承Converter.Factory,而且在create() 裏建立JsoupUtils實例,咱們只須要對返回值作修改,因此僅重寫responseBodyConverter方法,而且建立HtmlResponseBodyConverter類實現Converter<ResponseBody, T>接口
public class HtmlResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private JsoupUtils mPicker;
private Type mType;
HtmlResponseBodyConverter(JsoupUtils fruit, Type type) {
mPicker = fruit;
mType = type;
}
@Override
public T convert(ResponseBody value) throws IOException {
try {
String response = value.string();
return mPicker.fromHTML(response, (Class<T>) mType);
} finally {
value.close();
}
}
}
複製代碼
在covert方法中把獲取ResponseBody.string(),而後調用mPicker.fromHTML(response, (Class) mType),這樣就能夠在自動對實體類進行賦值了,Rxjava+Retrofit+JsoupUtils的請求代碼示例以下:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(RequestUrl.MAIN_URL)
.addConverterFactory(HtmlConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
retrofit.create(ServerApi.class)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Entity>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(DetailListInfo info) {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
複製代碼
封裝了這個JsoupUtils工具簡化代碼,同時學習了java知識註解與反射,對Retrofitde convert轉換有了深入的理解,建議感興趣的小夥伴能夠本身動手封裝下,加強本身的動手能力.項目庫以及demo地址(Github),以爲不錯的小夥伴能夠給個start.