Android 爬蟲,使用Jsoup解析Html像Json同樣優雅

當咱們作一些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的寫法,用戶只須要編寫實體類,而後就會自動對實體的各項進行賦值

結合Retrofit使用

如今網絡請求大多數都是使用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.

相關文章
相關標籤/搜索