第二篇

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

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

1. 需求背景

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

哪些链接可以继续爬 ?

是否要一直爬下去,要不要给一个终止符?

新的链接中,提取内容的规则和当前网页的规则不一致可以怎么办?

2. 设计

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

扫描网页中的链接,根据过滤规则匹配满足要求的链接

记录一个depth,用于表示爬取的深度,即从最原始的网页出发,到当前页面中间转了几次(讲到这里就有个循环爬取的问题,后面说)

不同的页面提取内容规则不一样,因此可以考虑留一个接口出来,让适用方自己来实现解析网页内容

基本实现

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

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

/**

* 批量查询的结果

*/

private List crawlResults = new ArrayList<>();

/**

* 爬网页的深度, 默认为0, 即只爬取当前网页

*/

private int depth = 0;

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

/**

* 执行抓取网页

*/

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> map = new HashMap<>(crawlMeta.getSelectorRules().size());

for (String rule : crawlMeta.getSelectorRules()) {

List 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中获取,获取网页中链接的方法

// 未超过最大深度, 继续爬网页中的所有链接

result = doParse(res);

Elements elements = result.getHtmlDoc().select("a[href]");

for(Element element: elements) {

doFetchNextPage(currentDepth + 1, element.attr("href"));

}

测试case

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

/**

* 深度爬

* @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 result = job.getCrawlResults();

System.out.println(result);

}

3. 改进

问题

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

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

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

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

- 添加链接的过滤

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

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

/**

* 正向的过滤规则

*/

@Setter

@Getter

private Set positiveRegex = new HashSet<>();

/**

* 逆向的过滤规则

*/

@Setter

@Getter

private Set negativeRegex = new HashSet<>();

public Set addPositiveRegex(String regex) {

this.positiveRegex.add(Pattern.compile(regex));

return this.positiveRegex;

}

public Set addNegativeRegex(String regex) {

this.negativeRegex.add(Pattern.compile(regex));

return this.negativeRegex;

}

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

// 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地址为什么

// 获取网页中的所有链接

@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());

}

看下取出的链接

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

改一行即可

// 解析为documnet对象时,指定 baseUrl

// 上面的代码结构会做一点修改,后面会说到

Document doc = Jsoup.parse(html, url);

// 获取链接时,前面添加abs

src = element.attr("abs:href");

- 保存结果

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

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

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

重新开始设计

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

/**

* 解析完网页后的回调方法

*

* @param crawlResult

*/

protected abstract void visit(CrawlResult crawlResult);

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

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

java网络爬虫如何控制爬取的深度_Java 动手写爬虫: 二、 深度爬取相关推荐

  1. python爬表情包_【从零开始写爬虫一】批量下载表情包

    序 打算写个关于node的爬虫菜鸟教程,接下来将带大家一步一步写一个表情包爬虫,从获取页面,解析表情包链接, 清洗脏数据,下载表情包到本地.开始之前你需要有对chrome调试工具和ES6有一定了解,包 ...

  2. php抓取dom处理后数据,写爬虫时PHP解析HTML最高效的方法那就是用DomCrawler!

    需求来源,需要用PHP解析HTML提取我想要的数据 用PHP写网站爬虫的时候,需要把爬取的网页进行解析,提取里面想要的数据,这个过程叫做网页HTML中数据结构化. 很多人应该知道用phpQuery像J ...

  3. Java 反射取类中类_Java反射机制(二):通过反射取得类的结构

    在反射运用过程中,如果你想得到一个类的完整结构,那么就要使用到java.lang.reflect包中的几个类: · Constructor  表示类中的构造方法 · Field  表示类中的属性 · ...

  4. java byte[] 文件流 转换成string是乱码_Java学习--IO(二)、多线程

    1.标准输入流 标准输入流是指从标准输入设备流向程序的数据. Java利用http://System.in来得到一个InputStream字节输入流 public static void main(S ...

  5. 第三十六期:学 Java 网络爬虫,需要哪些基础知识?

    说起网络爬虫,大家想起的估计都是 Python ,诚然爬虫已经是 Python 的代名词之一,相比 Java 来说就要逊色不少.有不少人都不知道 Java 可以做网络爬虫,其实 Java 也能做网络爬 ...

  6. python网络爬虫、Java 网络爬虫,哪个更好?

    说起网络爬虫,大家想起的估计都是 Python ,诚然爬虫已经是 Python 的代名词之一,相比 Java 来说就要逊色不少.有不少人都不知道 Java 可以做网络爬虫,其实 Java 也能做网络爬 ...

  7. 学 Java 网络爬虫,需要哪些基础知识?

    说起网络爬虫,大家想起的估计都是 Python ,诚然爬虫已经是 Python 的代名词之一,相比 Java 来说就要逊色不少.有不少人都不知道 Java 可以做网络爬虫,其实 Java 也能做网络爬 ...

  8. python爬虫怎么爬同一个网站的多页数据-如何用Python爬数据?(一)网页抓取

    如何用Python爬数据?(一)网页抓取 你期待已久的Python网络数据爬虫教程来了.本文为你演示如何从网页里找到感兴趣的链接和说明文字,抓取并存储到Excel. 需求 我在公众号后台,经常可以收到 ...

  9. python高级—— 从趟过的坑中聊聊爬虫、反爬、反反爬,附送一套高级爬虫试题

    前言: 时隔数月,我终于又更新博客了,然而,在这期间的粉丝数也就跟着我停更博客而涨停了,唉 是的,我改了博客名,不知道为什么要改,就感觉现在这个名字看起来要洋气一点. 那么最近到底咋不更新博客了呢?说 ...

  10. python高级—— 从趟过的坑中聊聊爬虫、反爬、反反爬,附送一套高级爬虫试题...

    前言: 时隔数月,我终于又更新博客了,然而,在这期间的粉丝数也就跟着我停更博客而涨停了,唉 是的,我改了博客名,不知道为什么要改,就感觉现在这个名字看起来要洋气一点. 那么最近到底咋不更新博客了呢?说 ...

最新文章

  1. php实时股票,php获得股票数据
  2. 在CentOS 6.9 64bit上安装jdk1.8
  3. 用无人机打点作画,密集恐惧症患者慎入!
  4. 008 python接口 unittest
  5. 20应用统计考研复试要点(part16)--应用多元分析
  6. delphi7升级delphi2007可以互用马_马爹利鼎盛周末饭局暂停营业,未来再见
  7. postgresql数据库导入导出
  8. TokenInsight:反映区块链行业整体表现的TI指数较昨日同期下跌1.54%
  9. 在opencv3中的机器学习算法
  10. hdu 1251 统计难题 (字典树入门题)
  11. VS code gopls requires a module at the root of your workspace
  12. 使用FileWriter和BufferedWriter向文本文件中写信息
  13. 架构之美第三章-美丽架构之道
  14. ffplay播放器原理剖析
  15. 企业微信标签在哪?如何设置?
  16. h5阿里云播放器 常规使用
  17. 微信小程序如何更换头像
  18. 软件测试之第一章 软件测试和测试环境
  19. 基于Springboot的网上商城
  20. 华为电脑管家装到D盘_科普 | 电脑C盘满了,不想重新分区怎样扩容?我教你

热门文章

  1. 买房贷款月供怎么算?贷款利息是多少?
  2. C# 设置或验证 PDF中的文本域格式
  3. 什么是 A 轮融资?有 B轮 C轮么?
  4. 数显之家快讯:【SHIO世硕心语】2021,对你的老板好一点!
  5. 深入理解Plasma(四):Plasma Cash
  6. UISlider滑条
  7. Photoshop-选区的应用
  8. 张驰咨询:关于企业选择六西格玛绿带培训人员,你需要知道这些
  9. 软件License设计思路与实现方案
  10. o.redisson.client.handler.CommandsQueue : Exception occured.