1. 引言

从今天开始系统的学习网络爬虫。写这篇博客的目的在于,一来记录下自己的学习过程;二来希望可以给像我一样不懂爬虫但又对爬虫十分感兴趣的人带来一些帮助。

昨天去图书馆找有关爬虫书籍,居然寥寥无几,且都是泛泛而谈。之后上某宝淘来淘去,只找到一本相关书籍《自己动手写网络爬虫》,虽然在某瓣上看到此书的无数差评,但最终还是忍痛买下……

对我而言,学习爬虫不是学习如何使用API(学API看帮助文档就ok了),而是学习爬虫的算法和数据结构,即学习爬虫的爬取策略,任务调度,数据挖掘,数据存储以及整个系统的架构。因此我会花较多的篇幅去记录以上提到的点,而不会去过多地介绍API如何调用。

这篇文章作为自己第一篇学习爬虫的博文,只想记录一些最最基本的概念,并简单实现一个最最基本的爬虫:它能够根据种子节点以特定的策略来爬取页面,直到达到设定的条件,并将这些页面保存在磁盘中。 我们使用Java作为编程语言。

2. 分析

(1) 算法分析

我们现在从需求中提取关键词来逐步分析问题。

首先是“种子节点”。它就是一个或多个在爬虫程序运行前手动给出的URL(网址),爬虫正是下载并解析这些种子URL指向的页面,从中提取出新的URL,然后重复以上的工作,直到达到设定的条件才停止。

然后是“特定的策略”。这里所说的策略就是以怎样的顺序去请求这些URL。如下图是一个简单的页面指向示意图(实际情况远比这个复杂),页面A是种子节点,当然最先请求。但是剩下的页面该以何种顺序请求呢?我们可以采用深度优先遍历策略,通俗讲就是一条路走到底,走完一条路才再走另一条路,在下图中就是按A,B,C,F,D,G,E,H的顺序访问。我们也可以采用宽度优先遍历策略,就是按深度顺序去遍历,在下图中就是按A,B,C,D,E,F,G,H的顺序请求各页面。还有许多其他的遍历策略,如Google经典的PageRank策略,OPIC策略策略,大站优先策略等,这里不一一介绍了。我们还需要注意的一个问题是,很有可能某个页面被多个页面同时指向,这样我们可能重复请求某一页面,因此我们还必须过滤掉已经请求过的页面。

最后是“设定的条件”,爬虫程序终止的条件可以根据实际情况灵活设置,比如设定爬取时间,爬取数量,爬行深度等。

到此,我们分析完了爬虫如何开始,怎么运作,如何结束(当然,要实现一个强大,完备的爬虫要考虑的远比这些复杂,这里只是入门分析),下面给出整个运作的流程图:

(2) 数据结构分析

根据以上的分析,我们需要用一种数据结构来保存初始的种子URL和解析下载的页面得到的URL,并且我们希望先解析出的URL先执行请求,因此我们用队列来储存URL。因为我们要频繁的添加,取出URL,因此我们采用链式存储。下载的页面解析后直接原封不动的保存到磁盘。

(3) 技术分析

所谓网络爬虫,我们当然要访问网络,我们这里使用jsoup,它对http请求和html解析都做了良好的封装,使用起来十分方便。根据数据结构分析,我们用LinkedList实现队列,用来保存未访问的URL,用HashSet来保存访问过的URL(因为我们要大量的判断该URL是否在该集合内,而HashSet用元素的Hash值作为“索引”,查找速度很快)。

3. 实现

(1) 代码

以上分析,我们一共要实现2个类:

① JsoupDownloader,该类是对Jsoup做一个简单的封装,方便调用。暴露出以下几个方法:

—public Document downloadPage(String url);根据url下载页面

—public Set<String> parsePage(Document doc, String regex);从Document中解析出匹配regex的url。

—public void savePage(Document doc, String saveDir, String saveName, String regex);保存匹配regex的url对应的Document到指定路径。

② UrlQueue,该类用来保存和获取URL。暴露出以下几个方法:

—public void enQueue(String url);添加url。

—public String deQueue();取出url。

—public int getVisitedCount();获取访问过的url的数量;

下面给出具体代码:

JsoupDownloader.java

package com.dk.spider.spider_01;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; public class JsoupDownloader { public static final String DEFAULT_SAVE_DIR = "c:/download/";
private static JsoupDownloader downloader; private JsoupDownloader() {
} public static JsoupDownloader getInstance() {
if (downloader == null) {
synchronized (JsoupDownloader.class) {
if (downloader == null) {
downloader = new JsoupDownloader();
}
}
}
return downloader;
} public Document downloadPage(String url) {
try {
System.out.println("正在下载" + url);
return Jsoup.connect(url).get();
} catch (IOException e) {
e.printStackTrace();
}
return null;
} public Set<String> parsePage(Document doc, String regex) {
Set<String> urlSet = new HashSet<>();
if (doc != null) {
Elements elements = doc.select("a[href]");
for (Element element : elements) {
String url = element.attr("href");
if (url.length() > 6 && !urlSet.contains(url)) {
if (regex != null && !url.matches(regex)) {
continue;
}
urlSet.add(url);
}
}
}
return urlSet;
} public void savePage(Document doc, String saveDir, String saveName, String regex) {
if (doc == null) {
return;
}
if (regex != null && doc.baseUri() != null && !doc.baseUri().matches(regex)) {
return;
}
saveDir = saveDir == null ? DEFAULT_SAVE_DIR : saveDir;
saveName = saveName == null ? doc.title().trim().replaceAll("[\\?/:\\*|<>\" ]", "_") + System.nanoTime() + ".html" : saveName;
File file = new File(saveDir + "/" + saveName);
File dir = file.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
PrintWriter printWriter;
try {
printWriter = new PrintWriter(file);
printWriter.write(doc.toString());
printWriter.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}

UrlQueue.java

package com.dk.spider.spider_01;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Set; public class UrlQueue { private Set<String> visitedSet;// 用来存放已经访问过多url
private LinkedList<String> unvisitedList;// 用来存放未访问过多url public UrlQueue(String[] seeds) {
visitedSet = new HashSet<>();
unvisitedList = new LinkedList<>();
unvisitedList.addAll(Arrays.asList(seeds));
} /**
* 添加url
*
* @param url
*/
public void enQueue(String url) {
if (url != null && !visitedSet.contains(url)) {
unvisitedList.addLast(url);
}
} /**
* 添加url
*
* @param urls
*/
public void enQueue(Collection<String> urls) {
for (String url : urls) {
enQueue(url);
}
} /**
* 取出url
*
* @return
*/
public String deQueue() {
try {
String url = unvisitedList.removeFirst();
while(visitedSet.contains(url)) {
url = unvisitedList.removeFirst();
}
visitedSet.add(url);
return url;
} catch (NoSuchElementException e) {
System.err.println("URL取光了");
}
return null;
} /**
* 得到已经请求过的url的数目
*
* @return
*/
public int getVisitedCount() {
return visitedSet.size();
}
}
(2) 测试

下面进行测试,我们来抓取园子里排行No1的Artech的文章,以他的博客首页地址:http://www.cnblogs.com/artech/作为种子节点。通过分析发现,形如:http://www.cnblogs.com/artech/p/…和http://www.cnblogs.com/artech/archive/2012/09/08/…的链接都是有效的文章地址,而形如:http://www.cnblogs.com/artech/default/…的链接是下一页链接,这些都作为我们筛选url的依据。我们采用宽度优先遍历策略。Artech的文章数是500余篇,因此我们以请求页面数达到1000或遍历完所有满足条件的url为终止条件。下面是具体的测试代码:

package com.dk.spider.spider_01;

import java.util.Set;

import org.jsoup.nodes.Document;

public class Main {

    public static void main(String[] args) {
UrlQueue urlQueue = new UrlQueue(new String[] { "http://www.cnblogs.com/artech/" });
JsoupDownloader downloader = JsoupDownloader.getInstance();
long start = System.currentTimeMillis();
while (urlQueue.getVisitedCount() < 1000) {
String url = urlQueue.deQueue();
if (url == null) {
break;
}
Document doc = downloader.downloadPage(url);
if (doc == null) {
continue;
}
Set<String> urlSet = downloader.parsePage(doc, "(http://www.cnblogs.com/artech/p|http://www.cnblogs.com/artech/default|http://www.cnblogs.com/artech/archive/\\d{4}/\\d{2}/\\d{2}/).*");
urlQueue.enQueue(urlSet);
downloader.savePage(doc, "C:/Users/Administrator/Desktop/test/", null, "(http://www.cnblogs.com/artech/p|http://www.cnblogs.com/artech/archive/\\d{4}/\\d{2}/\\d{2}/).*");
System.out.println("已请求" + urlQueue.getVisitedCount() + "个页面");
}
long end = System.currentTimeMillis();
System.out.println(">>>>>>>>>>抓去完成,共抓取" + urlQueue.getVisitedCount() + "到个页面,用时" + ((end - start) / 1000) + "s<<<<<<<<<<<<");
}
}

运行结果:

4. 总结

仔细分析以上过程,还有许多值得优化改进的地方:

① 我们在请求页面时,只是做了简单的异常处理。好的做法是根据http响应的状态码来做不同的处理。如对于请求重定向的url我们重新定向;对于找不到资源的url直接丢弃;对于连接超时的url我们可以重新将其放入未访问url队列中…

② 我们的待访问和已访问url都是直接保存在内存中的。当url数量很多时,可能会发生内存溢出。因此需要将数据持久化到硬盘上,但是又要节约空间,能够快速访问数据。

③ UrlQueue的enqueue和dequeue方法实际上是有问题的,当解析url速度慢于下载页面速度或其他原因引起的dequeue快于enqueue时,会导致程序提前终止。我们可以采用多线程,阻塞队列(BlockingQueue)来解决这一问题。

④ 我们目前的爬虫效率太低,仅爬取600个左右页面就花费了1分多钟。我们可以采用多线程,分布式爬取,来提高爬虫效率。

⑤ 爬虫的架构过于简单,扩展性,灵活性不强。

但不管怎样,我们的实现基本满足了文章开始提出的需求,以后会在此基础上慢慢进行迭代。在下一篇中我们会引入多线程来提高爬虫的效率;并采用Bloom Filter(布隆过滤器)来构建visited集合;引入Berkeley DB来进行url数据的持久化。

爬虫入门——01的更多相关文章

  1. 【爬虫入门01】我第一只由Reuests和BeautifulSoup4供养的Spider

    [爬虫入门01]我第一只由Reuests和BeautifulSoup4供养的Spider 广东职业技术学院  欧浩源 1.引言  网络爬虫可以完成传统搜索引擎不能做的事情,利用爬虫程序在网络上取得数据 ...

  2. 【网络爬虫入门01】应用Requests和BeautifulSoup联手打造的第一条网络爬虫

    [网络爬虫入门01]应用Requests和BeautifulSoup联手打造的第一条网络爬虫 广东职业技术学院 欧浩源 2017-10-14  1.引言 在数据量爆发式增长的大数据时代,网络与用户的沟 ...

  3. python爬虫入门01:教你在 Chrome 浏览器轻松抓包

    通过 python爬虫入门:什么是爬虫,怎么玩爬虫? 我们知道了什么是爬虫 也知道了爬虫的具体流程 那么在我们要对某个网站进行爬取的时候 要对其数据进行分析 就要知道应该怎么请求 就要知道获取的数据是 ...

  4. 【网络爬虫入门05】分布式文件存储数据库MongoDB的基本操作与爬虫应用

    [网络爬虫入门05]分布式文件存储数据库MongoDB的基本操作与爬虫应用 广东职业技术学院  欧浩源 1.引言 网络爬虫往往需要将大量的数据存储到数据库中,常用的有MySQL.MongoDB和Red ...

  5. python爬虫入门02:教你通过 Fiddler 进行手机抓包

    哟~哟~哟~ hi起来 everybody 今天要说说怎么在我们的手机抓包 通过 python爬虫入门01:教你在Chrome浏览器轻松抓包 我们知道了 HTTP 的请求方式 以及在 Chrome 中 ...

  6. 爬虫入门系列(二):优雅的HTTP库requests

    在系列文章的第一篇中介绍了 HTTP 协议,Python 提供了很多模块来基于 HTTP 协议的网络编程,urllib.urllib2.urllib3.httplib.httplib2,都是和 HTT ...

  7. Python爬虫入门教程 37-100 云沃客项目外包网数据爬虫 scrapy

    爬前叨叨 2019年开始了,今年计划写一整年的博客呢~,第一篇博客写一下 一个外包网站的爬虫,万一你从这个外包网站弄点外快呢,呵呵哒 数据分析 官方网址为 https://www.clouderwor ...

  8. 转 Python爬虫入门三之Urllib库的基本使用

    静觅 » Python爬虫入门三之Urllib库的基本使用 1.分分钟扒一个网页下来 怎样扒网页呢?其实就是根据URL来获取它的网页信息,虽然我们在浏览器中看到的是一幅幅优美的画面,但是其实是由浏览器 ...

  9. Python简单爬虫入门三

    我们继续研究BeautifulSoup分类打印输出 Python简单爬虫入门一 Python简单爬虫入门二 前两部主要讲述我们如何用BeautifulSoup怎去抓取网页信息以及获取相应的图片标题等信 ...

随机推荐

  1. solr服务中集成IKAnalyzer中文分词器、集成dataimportHandler插件

    昨天已经在Tomcat容器中成功的部署了solr全文检索引擎系统的服务:今天来分享一下solr服务在海量数据的网站中是如何实现数据的检索. 在solr服务中集成IKAnalyzer中文分词器的步骤: ...

  2. MIP 官方发布 v1稳定版本

    近期,MIP官方发布了MIP系列文件的全新v1版本,我们建议大家尽快完成升级. 一. 我是开发者,如何升级版本? 对于MIP页面开发者来说,只需替换线上引用的MIP文件为v1版本,就可以完成升级.所有 ...

  3. .NET里简易实现AOP

    .NET里简易实现AOP 前言 在MVC的过滤器章节中对于过滤器的使用就是AOP的一个实现了吧,时常在工作学习中遇到AOP对于它的运用可以说是很熟练了,就是没想过如果自己来实现的话是怎么实现的,性子比 ...

  4. js中参数不对应问题

    因为js是一种弱类型的编程语言,对数据类型的要求没有其他编程语言的要求严格,所以在定义函数的时候不需要像java和C#一样对其传入参数的类型进行定义.那么传入参数的个数有没有影响呢?今天小猪就做了个实 ...

  5. 阿里签名中URLEncode于C#URLEncod不同之处

    问题 如上图所示,阿里云的PercentEncode 转换! 为 %21 PercentEncode 源码为: package com.aliyuncs.auth; import java.io.Un ...

  6. javaScript的原型继承与多态性

    1.prototype 我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,给人们的感觉 ...

  7. 关于面试题 Array.indexof() 方法的实现及思考

    这是我在面试大公司时碰到的一个笔试题,当时自己云里雾里的胡写了一番,回头也曾思考过,最终没实现也就不了了之了. 昨天看到有网友说面试中也碰到过这个问题,我就重新思考了这个问题的实现方法. 对于想进大公 ...

  8. 8、Struts2 运行流程分析

    1.流程分析: 请求发送给 StrutsPrepareAndExecuteFilter StrutsPrepareAndExecuteFilter 询问 ActionMapper: 该请求是否是一个 ...

  9. peer not authenticated的终极解决方案

    一.前述 使用httpclient发起https请求时,可能会遇到如下异常: javax.net.ssl.SSLPeerUnverifiedException: peer not authentica ...

  10. Java开发中的23种设计模式详解

    [放弃了原文访问者模式的Demo,自己写了一个新使用场景的Demo,加上了自己的理解] [源码地址:https://github.com/leon66666/DesignPattern] 一.设计模式 ...