当咱们作一些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.