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. webapi - 使用依赖注入

    本篇将要和大家分享的是webapi中如何使用依赖注入,依赖注入这个东西在接口中常用,实际工作中也用的比较频繁,因此这里分享两种在api中依赖注入的方式Ninject和Unity:由于快过年这段时间打算 ...

  2. 【SQLServer】记一次数据迁移-标识重复的简单处理

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 今天在数据迁移的时候因为手贱遇到一个坑爹问题,发来大家乐乐,也传授新手点经验 迁移惯用就 ...

  3. WebAPi之SelfHost自创建证书启动Https疑难解惑及无法正确返回结果

    前言 话说又来需求了,之前对于在SelfHost中需要嵌套页面并操作为非正常需求,这回来正常需求了,客户端现在加了https,老大过来说WebAPi访问不了了,这是什么情况,我去试了试,还真是这个情况 ...

  4. PHP-----文件系统的交互

    本文讲解php中于文件交互中所使用的函数 代码示例 <html> <head> <title> File Detail </title> </he ...

  5. Entity Framework 教程——DBContext

    DBContext: 在之前的章节<创建实体数据模型>中,EDM为我们创建了SchoolDBEntities 类,它派生子System.Data.Entity.DbContext这个类,这 ...

  6. BPM的魅力何在?

    BPM(Business Process Management , 企业流程管理平台) 是带动企业流程自动化的帮 手,也是最能忠实反应出企业作业流程问题症结的系统工具,在管理上,BPM可以让管理者利用 ...

  7. HotApp小程序服务范围资质查询器

    微信小程序提交审核需要选择资质服务范围,如果服务范围不对,审核会不通过, 开发小程序之前,最好先查询所开发小程序的资质范围,否则无法通过微信审核.   小程序的资质范围查询地址,数据同步微信官方 ht ...

  8. ios label 自动计算行高详解

    在OC当中自动计算行高主要调用系统的 p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo; color: #ffffff } span ...

  9. SQLServer如何添加try catch

    在.net中我们经常用到try catch.不过在sqlserver中我们也可以使用try catch捕捉错误,在这里把语法记录下来和大家分享一下, --构建存储过程CREATE PROCEDURE ...

  10. C++的性能C#的产能?! - .Net Native 系列五:.Net Native与反射

    此系列系小九的学堂原创翻译,翻译自微软官方开发向导,一共分为六个主题.本文是第五个主题:.Net Native与反射. 向导文链接:<C++的性能C#的产能?! - .Net Native 系列 ...