Java 动手写爬虫: 二、 深度爬取

文章目录
  1. 第二篇:深度爬取
    1. I. 需求背景
    2. II. 设计
      1. 1. 基本实现
        1. 说明
        2. 测试case
    3. III. 改进
      1. 1. 问题
      2. 2. 添加链接的过滤
      3. 3. 保存结果
        1. 1. AbstractJob 类中定义一个回调方法
        2. 2. DefaultAbstractCrawlJob 实现爬取网页逻辑的抽象类
        3. 3. SimpleCrawlJob
        4. 4,使用测试
      4. 4. 爬虫去重
        1. 1. IStorage 接口
        2. 2. RamStorage 利用Map实现的内存存储
        3. 3. StorageWrapper 封装类
        4. 4. 测试
    4. IV. 小结
      1. 相关博文
    5. V. 其他
      1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
      2. 声明
      3. 扫描关注

第二篇:深度爬取

前面实现了一个最基础的爬取单网页的爬虫,这一篇则着手解决深度爬取的问题

简单来讲,就是爬了一个网页之后,继续爬这个网页中的链接

I. 需求背景

背景比较简单和明确,当爬了一个网页之后,目标是不要就此打住,扫描这个网页中的链接,继续爬,所以有几个点需要考虑:

  • 哪些链接可以继续爬 ?
  • 是否要一直爬下去,要不要给一个终止符?
  • 新的链接中,提取内容的规则和当前网页的规则不一致可以怎么办?

II. 设计

针对上面的几点,结合之前的实现结构,在执行 doFetchPage 方法获取网页之后,还得做一些其他的操作

  • 扫描网页中的链接,根据过滤规则匹配满足要求的链接
  • 记录一个depth,用于表示爬取的深度,即从最原始的网页出发,到当前页面中间转了几次(讲到这里就有个循环爬取的问题,后面说)
  • 不同的页面提取内容规则不一样,因此可以考虑留一个接口出来,让适用方自己来实现解析网页内容

1. 基本实现

开始依然是先把功能点实现,然后再考虑具体的优化细节

先加一个配置项,表示爬取页面深度; 其次就是保存的结果,得有个容器来暂存, 所以在 SimpleCrawlJob 会新增两个属性

1
2
3
4
5
6
7
8
9
10
/**
* 批量查询的结果
*/
private List<CrawlResult> crawlResults = new ArrayList<>();


/**
* 爬网页的深度, 默认为0, 即只爬取当前网页
*/
private int depth = 0;

因为有深度爬取的过程,所以需要修改一下爬取网页的代码,新增一个 doFetchNetxtPage方法,进行迭代爬取网页,这时,结果匹配处理方法也不能如之前的直接赋值了,稍微改一下即可, 改成返回一个接过实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 执行抓取网页
*/
public void doFetchPage() throws Exception {
doFetchNextPage(0, this.crawlMeta.getUrl());
this.crawlResult = this.crawlResults.get(0);
}


private void doFetchNextPage(int currentDepth, String url) throws Exception {
HttpResponse response = HttpUtils.request(new CrawlMeta(url, this.crawlMeta.getSelectorRules()), httpConf);
String res = EntityUtils.toString(response.getEntity());
CrawlResult result;
if (response.getStatusLine().getStatusCode() != 200) { // 请求成功
result = new CrawlResult();
result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
result.setUrl(crawlMeta.getUrl());
this.crawlResults.add(result);
return;
}

result = doParse(res);

// 超过最大深度, 不继续爬
if (currentDepth > depth) {
return;
}


Elements elements = result.getHtmlDoc().select("a[href]");
for(Element element: elements) {
doFetchNextPage(currentDepth + 1, element.attr("href"));
}
}


private CrawlResult doParse(String html) {
Document doc = Jsoup.parse(html);

Map<String, List<String>> map = new HashMap<>(crawlMeta.getSelectorRules().size());
for (String rule : crawlMeta.getSelectorRules()) {
List<String> list = new ArrayList<>();
for (Element element : doc.select(rule)) {
list.add(element.text());
}

map.put(rule, list);
}


CrawlResult result = new CrawlResult();
result.setHtmlDoc(doc);
result.setUrl(crawlMeta.getUrl());
result.setResult(map);
result.setStatus(CrawlResult.SUCCESS);
return result;
}

说明

主要的关键代码在 doFetchNextPage 中,这里有两个参数,第一个表示当前url属于爬取的第几层,爬完之后,判断是否超过最大深度,如果没有,则获取出网页中的所有链接,迭代调用一遍

下面主要是获取网页中的跳转链接,直接从jsoup的源码中的example中获取,获取网页中链接的方法

1
2
3
4
5
6
// 未超过最大深度, 继续爬网页中的所有链接
result = doParse(res);
Elements elements = result.getHtmlDoc().select("a[href]");
for(Element element: elements) {
doFetchNextPage(currentDepth + 1, element.attr("href"));
}

测试case

测试代码和之前的差不多,唯一的区别就是指定了爬取的深度,返回结果就不截图了,实在是有点多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 深度爬
* @throws InterruptedException
*/
@Test
public void testDepthFetch() throws InterruptedException {
String url = "https://my.oschina.net/u/566591/blog/1031575";
CrawlMeta crawlMeta = new CrawlMeta();
crawlMeta.setUrl(url);


SimpleCrawlJob job = new SimpleCrawlJob(1);
job.setCrawlMeta(crawlMeta);
Thread thread = new Thread(job, "crawlerDepth-test");
thread.start();


thread.join();
List<CrawlResult> result = job.getCrawlResults();
System.out.println(result);
}

III. 改进

1. 问题

上面虽然是实现了目标,但问题却有点多:

  • 就比如上面的测试case,发现有122个跳转链接,顺序爬速度有点慢

    14987169282459.jpg

  • 链接中存在重复、页面内锚点、js等各种情况,并不是都满足需求

  • 最后的结果塞到List中,深度较多时,链接较多时,list可能被撑暴

2. 添加链接的过滤

过滤规则,可以划分为两种,正向的匹配,和逆向的排除

首先是修改配置类 CrawlMeta, 新增两个配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 正向的过滤规则
*/
@Setter
@Getter
private Set<Pattern> positiveRegex = new HashSet<>();


/**
* 逆向的过滤规则
*/
@Setter
@Getter
private Set<Pattern> negativeRegex = new HashSet<>();


public Set<Pattern> addPositiveRegex(String regex) {
this.positiveRegex.add(Pattern.compile(regex));
return this.positiveRegex;
}


public Set<Pattern> addNegativeRegex(String regex) {
this.negativeRegex.add(Pattern.compile(regex));
return this.negativeRegex;
}

然后在遍历子链接时,判断一下是否满足需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// doFetchNextPage 方法

Elements elements = result.getHtmlDoc().select("a[href]");
String src;
for(Element element: elements) {
src = element.attr("href");
if (matchRegex(src)) {
doFetchNextPage(currentDepth + 1, element.attr("href"));
}
}



// 规则匹配方法
private boolean matchRegex(String url) {
Matcher matcher;
for(Pattern pattern: crawlMeta.getPositiveRegex()) {
matcher = pattern.matcher(url);
if (matcher.find()) {
return true;
}
}


for(Pattern pattern: crawlMeta.getNegativeRegex()) {
matcher = pattern.matcher(url);
if(matcher.find()) {
return false;
}
}


return crawlMeta.getPositiveRegex().size() == 0;
}

上面主要是通过正则来进行过滤,暂不考虑正则带来的开销问题,至少是解决了一个过滤的问题

但是,但是,如果网页中的链接是相对路径的话,会怎么样

直接使用 Jsoup来测试一个网页,看获取的link地址为什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取网页中的所有链接
@Test
public void testGetLink() throws IOException {
String url = "http://chengyu.911cha.com/zishu_3_p1.html";

Connection httpConnection = HttpConnection.connect(url)
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("connection", "Keep-Alive")
.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");

Document doc = httpConnection.get();
Elements links = doc.select("a[href]");

print("\nLinks: (%d)", links.size());
}

看下取出的链接

1.png

根据上面的测试,获取的链接如果是相对地址,则会有问题,需要有一个转化的过程,这个改动比较简单,jsoup本身是支持的

改一行即可

1
2
3
4
5
6
7
// 解析为documnet对象时,指定 baseUrl
// 上面的代码结构会做一点修改,后面会说到
Document doc = Jsoup.parse(html, url);


// 获取链接时,前面添加abs
src = element.attr("abs:href");

3.png

3. 保存结果

当爬取的数据量较多时,将结果都保存在内存中,并不是一个好的选择,假色每个网页中,满足规则的是有10个,那么depth=n, 则从第一个网页出发,最终会得到

1 + 10 + ... + 10^n = (10^(n+1) - 1) / 9

显然在实际情况中是不可取的,因此可以改造一下,获取数据后给一个回调,让用户自己来选择如何处理结果,这时 SimpleCrawelJob 的结构基本上满足不了需求了

重新开始设计

1. AbstractJob 类中定义一个回调方法

1
2
3
4
5
6
/**
* 解析完网页后的回调方法
*
* @param crawlResult
*/
protected abstract void visit(CrawlResult crawlResult);

2. DefaultAbstractCrawlJob 实现爬取网页逻辑的抽象类

这个类实现爬取网页的主要逻辑,也就是将之前的SimpleCrwalJob的实现拷贝过来,区别是干掉了返回结果; 顺带修了一个小bug 😢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* Created by yihui on 2017/6/29.
*/
@Getter
@Setter
@NoArgsConstructor
public abstract class DefaultAbstractCrawlJob extends AbstractJob {
/**
* 配置项信息
*/
private CrawlMeta crawlMeta;


/**
* http配置信息
*/
private CrawlHttpConf httpConf = new CrawlHttpConf();


/**
* 爬网页的深度, 默认为0, 即只爬取当前网页
*/
protected int depth = 0;


public DefaultAbstractCrawlJob(int depth) {
this.depth = depth;
}


/**
* 执行抓取网页
*/
public void doFetchPage() throws Exception {
doFetchNextPage(0, this.crawlMeta.getUrl());
}


private void doFetchNextPage(int currentDepth, String url) throws Exception {
CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
HttpResponse response = HttpUtils.request(subMeta, httpConf);
String res = EntityUtils.toString(response.getEntity());
CrawlResult result;
if (response.getStatusLine().getStatusCode() != 200) { // 请求成功
result = new CrawlResult();
result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
result.setUrl(crawlMeta.getUrl());
this.visit(result);
return;
}


// 网页解析
result = doParse(res, subMeta);
// 回调用户的网页内容解析方法
this.visit(result);


// 超过最大深度, 不继续爬
if (currentDepth > depth) {
return;
}


Elements elements = result.getHtmlDoc().select("a[href]");
String src;
for(Element element: elements) {
// 确保将相对地址转为绝对地址
src = element.attr("abs:href");
if (matchRegex(src)) {
doFetchNextPage(currentDepth + 1, src);
}
}
}


private CrawlResult doParse(String html, CrawlMeta meta) {
// 指定baseUrl, 否则利用 abs:href 获取链接会出错
Document doc = Jsoup.parse(html, meta.getUrl());

Map<String, List<String>> map = new HashMap<>(meta.getSelectorRules().size());
for (String rule : crawlMeta.getSelectorRules()) {
List<String> list = new ArrayList<>();
for (Element element : doc.select(rule)) {
list.add(element.text());
}

map.put(rule, list);
}


CrawlResult result = new CrawlResult();
result.setHtmlDoc(doc);
result.setUrl(meta.getUrl());
result.setResult(map);
result.setStatus(CrawlResult.SUCCESS);
return result;
}


private boolean matchRegex(String url) {
Matcher matcher;
for(Pattern pattern: crawlMeta.getPositiveRegex()) {
matcher = pattern.matcher(url);
if (matcher.find()) {
return true;
}
}


for(Pattern pattern: crawlMeta.getNegativeRegex()) {
matcher = pattern.matcher(url);
if(matcher.find()) {
return false;
}
}


return crawlMeta.getPositiveRegex().size() == 0;
}
}

3. SimpleCrawlJob

重写这个简单爬虫任务的实现,因为主要逻辑在 DefaultAbstractCrawlJob中已经实现了,所以直接继承过来即可

主要关注的就是 visit 方法,这里就是爬取网页之后的回调,这个最简单的爬虫任务,就是将结果保存在内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 最简单的一个爬虫任务
* <p>
* Created by yihui on 2017/6/27.
*/
@Getter
@Setter
@NoArgsConstructor
public class SimpleCrawlJob extends DefaultAbstractCrawlJob {

/**
* 存储爬取的结果
*/
private CrawlResult crawlResult;


/**
* 批量查询的结果
*/
private List<CrawlResult> crawlResults = new ArrayList<>();



public SimpleCrawlJob(int depth) {
super(depth);
}


@Override
protected void visit(CrawlResult crawlResult) {
crawlResults.add(crawlResult);
}


public CrawlResult getCrawlResult() {
if(crawlResults.size() == 0) {
return null;
}

return crawlResults.get(0);
}
}

4,使用测试

和之前没有任何区别,先来个简单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testDepthFetch() throws InterruptedException {
String url = "http://chengyu.911cha.com/zishu_3_p1.html";
CrawlMeta crawlMeta = new CrawlMeta();
crawlMeta.setUrl(url);
crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_3_p([0-9]+).html");


SimpleCrawlJob job = new SimpleCrawlJob(1);
job.setCrawlMeta(crawlMeta);
Thread thread = new Thread(job, "crawlerDepth-test");
thread.start();


thread.join();
List<CrawlResult> result = job.getCrawlResults();
System.out.println(result);
}

运行截图

1.png

直接使用 DefaultAbstractCrawl 抽象类的回调来进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testSelfCwralFetch() throws InterruptedException {
String url = "http://chengyu.911cha.com/zishu_3_p1.html";
CrawlMeta crawlMeta = new CrawlMeta();
crawlMeta.setUrl(url);
crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_3_p([0-9]+).html");


DefaultAbstractCrawlJob job = new DefaultAbstractCrawlJob(1) {
@Override
protected void visit(CrawlResult crawlResult) {
System.out.println(crawlResult.getUrl());
}
};
job.setCrawlMeta(crawlMeta);
Thread thread = new Thread(job, "crawlerDepth-test");
thread.start();

thread.join();
System.out.println("over");
}

4. 爬虫去重

从上面可以发现,重复爬取是比较浪费的事情,因此去重是非常有必要的;一般想法是将爬过的url都标记一下,每次爬之前判断是否已经爬过了

依然先是采用最low的方法,搞一个Set来记录所有爬取的url,因为具体的爬虫任务设计的是多线程的,所以这个Set是要求多线程共享的

此外考虑到去重的手段比较多,我们目前虽然只是采用的内存中加一个缓存表,但不妨碍我们设计的时候,采用面向接口的方式

1. IStorage 接口

提供存记录,判断记录是否存在的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IStorage {

/**
* 若爬取的URL不在storage中, 则写入; 否则忽略
*
* @param url 爬取的网址
* @return true 表示写入成功, 即之前没有这条记录; false 则表示之前已经有记录了
*/
boolean putIfNotExist(String url, CrawlResult result);


/**
* 判断是否存在
* @param url
* @return
*/
boolean contains(String url);

}

2. RamStorage 利用Map实现的内存存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RamStorage implements IStorage {

private Map<String, CrawlResult> map = new ConcurrentHashMap<>();


@Override
public boolean putIfNotExist(String url, CrawlResult result) {
if(map.containsKey(url)) {
return false;
}

map.put(url, result);
return true;
}

@Override
public boolean contains(String url) {
return map.containsKey(url);
}
}

3. StorageWrapper 封装类

这个封装类要求多线程共享,所以我们采用单例模式,保证只有一个实例

一个最原始的实现方式如下(暂不考虑其中比较猥琐的storage实例化方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class StorageWrapper {

private static StorageWrapper instance = new StorageWrapper();


private IStorage storage;

public static StorageWrapper getInstance() {
return instance;
}


private StorageWrapper() {
storage = new RamStorage();
}


/**
* 判断url是否被爬取过
*
* @param url
* @return
*/
public boolean ifUrlFetched(String url) {
return storage.contains(url);
}


/**
* 爬完之后, 新增一条爬取记录
* @param url
* @param crawlResult
*/
public void addFetchRecord(String url, CrawlResult crawlResult) {
storage.putIfNotExist(url, crawlResult);
}
}

这样一个简单的保存爬取历史记录的容器就有了,那么在爬取时,就需要事前判断一下

对应的 DefaultAbstractCrawlJob#doFetchNextPage 方法更新如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// fixme 非线程安全
private void doFetchNextPage(int currentDepth, String url) throws Exception {
if (StorageWrapper.getInstance().ifUrlFetched(url)) {
return;
}


CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
HttpResponse response = HttpUtils.request(subMeta, httpConf);
String res = EntityUtils.toString(response.getEntity());
CrawlResult result;
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { // 请求成功
result = new CrawlResult();
result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
result.setUrl(crawlMeta.getUrl());
this.visit(result);
return;
}


// 网页解析
result = doParse(res, subMeta);
StorageWrapper.getInstance().addFetchRecord(url, result);

// 回调用户的网页内容解析方法
this.visit(result);


// 超过最大深度, 不继续爬
if (currentDepth > depth) {
return;
}


Elements elements = result.getHtmlDoc().select("a[href]");
String src;
for(Element element: elements) {
// 确保将相对地址转为绝对地址
src = element.attr("abs:href");
if (matchRegex(src)) {
doFetchNextPage(currentDepth + 1, src);
}
}
}

如果仔细看上面的方法,就会发现在多线程环境下,依然可能存在重复爬取的情况

如有两个CrawlJob任务,若爬取的是同一个url,第一个任务爬取完,还没有回写到Storage时,第二个任务开始爬,这时,事前判断没有记录,然后通过之后开始爬,这时就依然会出现重复爬的问题

要解决这个问题,一个简单的方法就是加锁,在判断一个url没有被爬时,到回写一条爬取结果这段期间,加一个保护锁

StorageWrapper 更新后如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* Created by yihui on 2017/6/29.
*/
public class StorageWrapper {

private static StorageWrapper instance = new StorageWrapper();


private IStorage storage;

private Map<String, Lock> lockMap = new ConcurrentHashMap<>();

public static StorageWrapper getInstance() {
return instance;
}


private StorageWrapper() {
storage = new RamStorage();
}


/**
* 判断url是否被爬取过; 是则返回true; 否这返回false, 并上锁
*
* @param url
* @return
*/
public boolean ifUrlFetched(String url) {
if(storage.contains(url)) {
return true;
}

synchronized (this) {
if (!lockMap.containsKey(url)) {
// 不存在时,加一个锁
lockMap.put(url, new ReentrantLock());
}

this.lock(url);


if (storage.contains(url)) {
return true;
}
// System.out.println(Thread.currentThread() + " lock url: " + url);
return false;
}
}


/**
* 爬完之后, 新增一条爬取记录
* @param url
* @param crawlResult
*/
public void addFetchRecord(String url, CrawlResult crawlResult) {
try {
if (crawlResult != null) {
storage.putIfNotExist(url, crawlResult);
this.unlock(url);
}
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + " result: " + url + " e: " + e);
}
}



private void lock(String url) {
lockMap.get(url).lock();
}


private void unlock(String url) {
lockMap.get(url).unlock();
}
}

使用处,稍稍变动如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private void doFetchNextPage(int currentDepth, String url) throws Exception {
CrawlResult result = null;
try {
// 判断是否爬过;未爬取,则上锁并继续爬取网页
if (StorageWrapper.getInstance().ifUrlFetched(url)) {
return;
}

CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
HttpResponse response = HttpUtils.request(subMeta, httpConf);
String res = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { // 请求成功
result = new CrawlResult();
result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
result.setUrl(crawlMeta.getUrl());
this.visit(result);
return;
}


// 网页解析
result = doParse(res, subMeta);
} finally {
// 添加一条记录, 并释放锁
StorageWrapper.getInstance().addFetchRecord(url, result);
}

// 回调用户的网页内容解析方法
this.visit(result);


// 超过最大深度, 不继续爬
if (currentDepth > depth) {
return;
}


Elements elements = result.getHtmlDoc().select("a[href]");
String src;
for(Element element: elements) {
// 确保将相对地址转为绝对地址
src = element.attr("abs:href");
if (matchRegex(src)) {
doFetchNextPage(currentDepth + 1, src);
}
}
}

4. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Test
public void testSelfCwralFetch() throws InterruptedException {
String url = "http://chengyu.t086.com/gushi/1.htm";
CrawlMeta crawlMeta = new CrawlMeta();
crawlMeta.setUrl(url);
crawlMeta.addPositiveRegex("http://chengyu.t086.com/gushi/[0-9]+\\.htm$");


DefaultAbstractCrawlJob job = new DefaultAbstractCrawlJob(1) {
@Override
protected void visit(CrawlResult crawlResult) {
System.out.println("job1 >>> " + crawlResult.getUrl());
}
};
job.setCrawlMeta(crawlMeta);



String url2 = "http://chengyu.t086.com/gushi/2.htm";
CrawlMeta crawlMeta2 = new CrawlMeta();
crawlMeta2.setUrl(url2);
crawlMeta2.addPositiveRegex("http://chengyu.t086.com/gushi/[0-9]+\\.htm$");
DefaultAbstractCrawlJob job2 = new DefaultAbstractCrawlJob(1) {
@Override
protected void visit(CrawlResult crawlResult) {
System.out.println("job2 >>> " + crawlResult.getUrl());
}
};
job2.setCrawlMeta(crawlMeta2);



Thread thread = new Thread(job, "crawlerDepth-test");
Thread thread2 = new Thread(job2, "crawlerDepth-test2");
thread.start();
thread2.start();


thread.join();
thread2.join();
}

输出如下

1
2
3
4
5
6
7
job2 >>> http://chengyu.t086.com/gushi/2.htm
job1 >>> http://chengyu.t086.com/gushi/1.htm
job1 >>> http://chengyu.t086.com/gushi/3.htm
job2 >>> http://chengyu.t086.com/gushi/4.htm
job1 >>> http://chengyu.t086.com/gushi/5.htm
job1 >>> http://chengyu.t086.com/gushi/6.htm
job1 >>> http://chengyu.t086.com/gushi/7.htm

IV. 小结

这一篇的博文有点多,到这里其实上面一些提出的问题还没有解决,留待下一篇博文来fix掉, 下面则主要说明下本篇的要点

  1. 深度爬取

    这里使用了迭代的思路,爬到一个网页之后,判断是否需要停止,不停止,则把该网页中的链接捞出来,继续爬;关键点

    • 利用 Jsoup 获取网页中所有链接(注意相对路径转绝对路径的用法)
    • 循环迭代
  2. 过滤

    过滤,主要利用正则来匹配链接;这里需要注意一下几点

    • 正向过滤
    • 负向过滤
  3. 去重

    如何保证一个链接被爬了之后,不会被重复进行爬取?

    • 记录爬取历史
    • 注意多线程安全问题
    • 加锁(一把锁会导致性能降低,这里采用了一个url对应一个锁,注意看实现细节,较多的坑)

遗留问题

  1. 失败重试
  2. 爬网页中链接不应该串行进行
  3. 频率控制(太快可能会被反扒干掉)

源码地址: https://github.com/liuyueyi/quick-crawler/releases/tag/v0.003

对应tag :v0.003

相关博文

Quick-Crawel爬虫系列博文

V. 其他

一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

扫描关注

QrCode

# 爬虫

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×