Android 爬虫,使用Jsoup解析Html像Json一样优雅

当我们做一些Android练手项目时,苦于无数据,这时候可以试试Jsoup爬虫,爬取任何网页上数据来丰富你App的内容;jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。使用起来也非常简单:

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的格式是否完整。

但是用Jsoup选择某个节点有个问题,例如在很深的节点下用Jsoup选择代码如下:

doc.select("body div.nav.head-nav.avt.clearfloat li.cat-item.cat-item")
复制代码

如果不熟悉JQuery选择器,请参考:JQuery选择器

一条选择语句看着就很凌乱,如果多条的话,可想而知,看起来就很难看,而且发生错误很难纠错,所以就就Gson模仿解析Json的方式,制做一个工具,通过注解和反射去解析凌乱的选择器.Jsoup更多用法请参考中文官网

首先定义一个注解类:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Pick {
    String value() default "";

    String attr() default Attrs.TEXT;
}
复制代码

对注解知识不熟悉的情参考Java Annotation 及几个常用开源项目注解原理简析

注解在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,那我们就可以编写实体类了

@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.

转载于:https://juejin.im/post/5a9e6547518825555d46c2cb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值