今天想要做的是把应用宝网站数据爬取下来。
知识要点
- 解析html
- 解析json
- 线程池执行
爬取步骤
- 左边一栏是分类,右边是应用app数据。
- 首先解析左边一栏的数据,在html中class为menu-junior的li标签里。
- 那么我们要解析这个li标签,拿到应用的大类,然后根据大类再爬取数据。
解析提取html对应的数据
全局变量存放这个应用首页的地址和发送json请求的地址
public static String mainPageUrl =
"https://sj.qq.com/myapp/category.htm?orgame=1";
public static String jsonUrl =
"https://sj.qq.com/myapp/cate/appList.htm?orgame=1&categoryId=
{categoryId}&pageSize=20&pageContext={pageContext}";
发送http get请求,使用jsoup解析这个地址。
String content = HTTPUtil.sendGet(mainPageUrl).getContent();
Document document = Jsoup.parse(content);
Elements elements = document.getElementsByClass("menu-junior");
Elements tags = elements.get(0).getElementsByTag("li");
//循环li标签集合,拿到categoryId
for(int i=0; i<tags.size(); i++) {
String htmlId = tags.get(i).attr("id");
if(!StringUtils.isEmpty(htmlId)) {
String categoryId = htmlId.replace("cate-", "");
categoryIdList.add(categoryId);
}
}
categoryIdList存放大类的线程安全的队列。因为LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选。
private static ConcurrentLinkedQueue<String> categoryIdList =
new ConcurrentLinkedQueue<>();
jsop对应的pom文件
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.25</version>
</dependency>
并发爬取,以大类为一个线程任务
这里有22个大类,那么开启22个任务,开启6个线程池去轮询这22个任务。
实现思路是: 每个线程从队列中消费一个大类数据,以这个大类为参数发送http请求。
url的内容是。{categoryId}和{pageContext}是后面的传递参数。
https://sj.qq.com/myapp/cate/appList.htm?orgame=1&categoryId
={categoryId}&pageSize=20&pageContext={pageContext}
处理url发送的请求
url = url.replace("{categoryId}", categoryId).replace("{pageContext}", "0");
//发送http请求,解析json串
String content = HTTPUtil.sendGet(url).getContent();
JSONObject jsonObject = JSON.parseObject(content);
String objString = jsonObject.getString("obj");
//每一次请求,是分页进行的。为了获取全部数据,所以还要进行分页处理,每次pageSize增加,直至取到空数据为止。
int index = 0;
do {
JSONArray jsonArray = jsonObject.getJSONArray("obj");
for(int i=0; i<jsonArray.size(); i++) {
App app = jsonArray.getJSONObject(i).toJavaObject(App.class);
//-------获得app的数据,对app数据进行操作。----//
System.out.println(i+" "+app.getAppName() +" "+url);
//-----------------------------------------//
}
//每执行一次pageContext加上20
int pageContext = (++index)*20;
String againUrl = jsonUrl;
//解析url,替换pageContext
againUrl = againUrl.replace("{categoryId}", categoryId)
.replace("{pageContext}", pageContext+"");
content = HTTPUtil.sendGet(againUrl).getContent();
jsonObject = JSON.parseObject(content);
objString = jsonObject.getString("obj");
} while (!StringUtils.isEmpty(objString));
有了上面的方法后,那么就可以使用线程池调用他。
public void run() {
//开启线程池去处理数据
while(!CollectionUtils.isEmpty(categoryIdList)) {
final String categoryId = categoryIdList.poll();
executorService.submit(new Runnable() {
@Override
public void run() {
sendJsonUrl(jsonUrl, categoryId);
}
});
}
//线程池执行完毕后,关闭退出线程池
executorService.shutdown();
}
如果了解获得线程池分配得详解,欢迎点击查看
上面可能写的比较乱,这里有提供源码下载。
水平原因可能存在错误,希望指正 chenrui@marsdl.com