jsoup使用
下面都是以笔趣阁网站为例,请谨慎的使用爬虫,避免给目标网站过大的压力。
以前想下载一本小说txt,找各种网站费老半天功夫,现在你最多只需要10s
1.上一个简单的demo
public static void main(String[] args) throws Exception {
String furl = "http://www.xbiquge.la/25/25430/12402769.html";
Document doc = Jsoup.connect(furl).timeout(5000).get();
Element content = doc.getElementById("content");
System.out.println(content.text());
}
上述示例分为几个步骤
- 根据一个url通过get请求获得html页面,最终封装成一个Document对象
- jsoup提供了类css 选择器功能
运行可以获得一章节的内容。
demo距离实际运用还需要:
- 能够同时爬取所有章节,并且按顺序保存到文件中
- 速度尽可能的快
2.来一个实用的demo
我们需要分以下几个步骤:
- 首先获得所有章节的url
- 开启多线程爬取,合并
- 保存到本地
测试示例url: http://www.xbiquge.la/25/25430/
public static void main(String[] args) throws Exception{
String furl = "http://www.xbiquge.la/25/25430/";
long start = System.currentTimeMillis();
Document doc = Jsoup.connect(furl).timeout(5000).get();
String title = doc.title();
String novelName = title.substring(0,title.indexOf("小说"));
Element list = doc.getElementById("list");
if(list == null){
return;
}
//获得所有章节的url
Elements chapterList = list.select("dl dd a");
if(chapterList == null || chapterList.size() == 0 ){
return;
}
//多线程,使用原子类进行自增保证线程安全
AtomicInteger num = new AtomicInteger(0);
//定义一个线程池,用于多线程爬取提高速度。
ThreadPoolExecutor executor = new ThreadPoolExecutor(32,32,30, TimeUnit.SECONDS,new LinkedBlockingDeque<>());
//存储所有章节的element对象(包含了内容)
Element[] successElemenetArr = new Element[chapterList.size()];
//存储每个章节名称
String[] chapNameArr = new String[chapterList.size()];
int i = 0;
for (Element cp:chapterList) {
int finalI = i++;
executor.submit(new Runnable() {
@Override
public void run() {
try {
String href = cp.attr("href");
String name = cp.text();
String tturl = "http://www.xbiquge.la" + href;
Document ttDoc = Jsoup.connect(tturl).get();
Element content = ttDoc.getElementById("content");
//此处使用了hash法进行存储,比使用线程安全的数据结构更加好
successElemenetArr[finalI % chapterList.size()] = content;
chapNameArr[finalI % chapterList.size()] = name;
System.out.println(num.incrementAndGet());
}catch (Exception e){}
}
});
}
//阻塞等待所有任务执行完毕,这个方式并不优雅,有兴趣可改为CountDownLatch。
while (num.get() != chapterList.size()){
Thread.sleep(100);
}
long end = System.currentTimeMillis();
//写入到本地磁盘
writeFile(novelName,successElemenetArr,chapNameArr);
System.out.println("耗时:" + (end - start));
executor.shutdown();
}
private static void writeFile(String name,Element[] elements,String[] chapNameArr) throws Exception{
//这里为了方便直接写在d盘了。
File file = new File("D:\\" +name + ".txt");
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
//写入小说名
String title = " " + name + "\n";
fos.write(title.getBytes("UTF-8"));
int i = 0;
for (Element element:elements) {
//写入章节名
String chapName = "\n "+chapNameArr[i++]+ "\n\n";
fos.write(chapName.getBytes("UTF-8"));
//写入小说每一行的内容
List<TextNode> textNodes = element.textNodes();
for (TextNode tn:textNodes) {
fos.write(tn.text().getBytes("UTF-8"));
fos.write("\n".getBytes("UTF-8"));
}
}
fos.flush();
fos.close();
}
注: 最后写入磁盘的代码,性能以及排版并不是很好,有兴趣自己优化一下。
3.代码中的多线程知识
-
AtomicInteger
在多线程环境下,i++,这种代码会存在脏读自增导致最后的结果偏小。
AtomicInteger使用了 volatile + cas机制避免了这个
-
ThreadPoolExecutor参数
关于线程池的几个参数:
-
核心线程数: 任务加入时没有空闲线程,并且没有到达最大核心线程数会开启新线程
-
最大线程数 :当阻塞队列满了,再次增加任务会开启新线程,但总的线程数不能超过这个值
-
空闲线程生存时间
-
阻塞队列:通常使用 有界,无界,优先级这三种。
-
丢弃策略: 队列满了,线程数已经到达最大线程数,执行的任务处理的策略,不一定是丢弃。
有丢弃,抛异常,让当前线程执行,加入队尾把队头的任务挤掉,或者自定义。
-
多线程的使用分为:io密集/cpu密集,此处为io密集通常为 3 * cpu数左右,cpu密集: cpu + 1。
此处为http调用,和带宽也有一定关系。
- hash法
代码中没有使用类似ConcurrentMap的实现类进行多线程的数据存储,而是采用array+hash法巧妙的避开了多线程处理以及后期排序问题。如果阅读过hashmap或者刷过leetcode应该会很熟悉。