爬虫入门——01
1. 引言
从今天开始系统的学习网络爬虫。写这篇博客的目的在于,一来记录下自己的学习过程;二来希望可以给像我一样不懂爬虫但又对爬虫十分感兴趣的人带来一些帮助。
昨天去图书馆找有关爬虫书籍,居然寥寥无几,且都是泛泛而谈。之后上某宝淘来淘去,只找到一本相关书籍《自己动手写网络爬虫》,虽然在某瓣上看到此书的无数差评,但最终还是忍痛买下……
对我而言,学习爬虫不是学习如何使用API(学API看帮助文档就ok了),而是学习爬虫的算法和数据结构,即学习爬虫的爬取策略,任务调度,数据挖掘,数据存储以及整个系统的架构。因此我会花较多的篇幅去记录以上提到的点,而不会去过多地介绍API如何调用。
这篇文章作为自己第一篇学习爬虫的博文,只想记录一些最最基本的概念,并简单实现一个最最基本的爬虫:它能够根据种子节点以特定的策略来爬取页面,直到达到设定的条件,并将这些页面保存在磁盘中。 我们使用Java作为编程语言。
2. 分析
我们现在从需求中提取关键词来逐步分析问题。
首先是“种子节点”。它就是一个或多个在爬虫程序运行前手动给出的URL(网址),爬虫正是下载并解析这些种子URL指向的页面,从中提取出新的URL,然后重复以上的工作,直到达到设定的条件才停止。
然后是“特定的策略”。这里所说的策略就是以怎样的顺序去请求这些URL。如下图是一个简单的页面指向示意图(实际情况远比这个复杂),页面A是种子节点,当然最先请求。但是剩下的页面该以何种顺序请求呢?我们可以采用深度优先遍历策略,通俗讲就是一条路走到底,走完一条路才再走另一条路,在下图中就是按A,B,C,F,D,G,E,H的顺序访问。我们也可以采用宽度优先遍历策略,就是按深度顺序去遍历,在下图中就是按A,B,C,D,E,F,G,H的顺序请求各页面。还有许多其他的遍历策略,如Google经典的PageRank策略,OPIC策略策略,大站优先策略等,这里不一一介绍了。我们还需要注意的一个问题是,很有可能某个页面被多个页面同时指向,这样我们可能重复请求某一页面,因此我们还必须过滤掉已经请求过的页面。
最后是“设定的条件”,爬虫程序终止的条件可以根据实际情况灵活设置,比如设定爬取时间,爬取数量,爬行深度等。
到此,我们分析完了爬虫如何开始,怎么运作,如何结束(当然,要实现一个强大,完备的爬虫要考虑的远比这些复杂,这里只是入门分析),下面给出整个运作的流程图:
根据以上的分析,我们需要用一种数据结构来保存初始的种子URL和解析下载的页面得到的URL,并且我们希望先解析出的URL先执行请求,因此我们用队列来储存URL。因为我们要频繁的添加,取出URL,因此我们采用链式存储。下载的页面解析后直接原封不动的保存到磁盘。
所谓网络爬虫,我们当然要访问网络,我们这里使用jsoup,它对http请求和html解析都做了良好的封装,使用起来十分方便。根据数据结构分析,我们用LinkedList实现队列,用来保存未访问的URL,用HashSet来保存访问过的URL(因为我们要大量的判断该URL是否在该集合内,而HashSet用元素的Hash值作为“索引”,查找速度很快)。
3. 实现
以上分析,我们一共要实现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();
}
}
下面进行测试,我们来抓取园子里排行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的更多相关文章
- 【爬虫入门01】我第一只由Reuests和BeautifulSoup4供养的Spider
[爬虫入门01]我第一只由Reuests和BeautifulSoup4供养的Spider 广东职业技术学院 欧浩源 1.引言 网络爬虫可以完成传统搜索引擎不能做的事情,利用爬虫程序在网络上取得数据 ...
- 【网络爬虫入门01】应用Requests和BeautifulSoup联手打造的第一条网络爬虫
[网络爬虫入门01]应用Requests和BeautifulSoup联手打造的第一条网络爬虫 广东职业技术学院 欧浩源 2017-10-14 1.引言 在数据量爆发式增长的大数据时代,网络与用户的沟 ...
- python爬虫入门01:教你在 Chrome 浏览器轻松抓包
通过 python爬虫入门:什么是爬虫,怎么玩爬虫? 我们知道了什么是爬虫 也知道了爬虫的具体流程 那么在我们要对某个网站进行爬取的时候 要对其数据进行分析 就要知道应该怎么请求 就要知道获取的数据是 ...
- 【网络爬虫入门05】分布式文件存储数据库MongoDB的基本操作与爬虫应用
[网络爬虫入门05]分布式文件存储数据库MongoDB的基本操作与爬虫应用 广东职业技术学院 欧浩源 1.引言 网络爬虫往往需要将大量的数据存储到数据库中,常用的有MySQL.MongoDB和Red ...
- python爬虫入门02:教你通过 Fiddler 进行手机抓包
哟~哟~哟~ hi起来 everybody 今天要说说怎么在我们的手机抓包 通过 python爬虫入门01:教你在Chrome浏览器轻松抓包 我们知道了 HTTP 的请求方式 以及在 Chrome 中 ...
- 爬虫入门系列(二):优雅的HTTP库requests
在系列文章的第一篇中介绍了 HTTP 协议,Python 提供了很多模块来基于 HTTP 协议的网络编程,urllib.urllib2.urllib3.httplib.httplib2,都是和 HTT ...
- Python爬虫入门教程 37-100 云沃客项目外包网数据爬虫 scrapy
爬前叨叨 2019年开始了,今年计划写一整年的博客呢~,第一篇博客写一下 一个外包网站的爬虫,万一你从这个外包网站弄点外快呢,呵呵哒 数据分析 官方网址为 https://www.clouderwor ...
- 转 Python爬虫入门三之Urllib库的基本使用
静觅 » Python爬虫入门三之Urllib库的基本使用 1.分分钟扒一个网页下来 怎样扒网页呢?其实就是根据URL来获取它的网页信息,虽然我们在浏览器中看到的是一幅幅优美的画面,但是其实是由浏览器 ...
- Python简单爬虫入门三
我们继续研究BeautifulSoup分类打印输出 Python简单爬虫入门一 Python简单爬虫入门二 前两部主要讲述我们如何用BeautifulSoup怎去抓取网页信息以及获取相应的图片标题等信 ...
随机推荐
- CMS模板应用调研问卷
截止目前,已经有数十家网站与我们合作,进行了MIP化改造,在搜索结果页也能看到"闪电标"的出现.除了改造方面的问题,MIP项目组被问到最多的就是:我用了wordpress,我用了织 ...
- UITextView 输入字数限制
本文介绍了UITextView对中英文还有iOS自带表情输入的字数限制,由于中文输入会有联想导致字数限制不准确所以苦恼好久,所以参考一些大神的博客终于搞定,欢迎大家参考和指正. 对于限制UITextV ...
- Linux 开机时网络自动连接
简单版本: cd /etc/sysconfig/network-scripts/ vi ifcfg-enoXXX 输入:reboot重启 或者输入:service network restart ...
- Android业务组件化之URL Scheme使用
前言: 最近公司业务发展迅速,单一的项目工程不再适合公司发展需要,所以开始推进公司APP业务组件化,很荣幸自己能够牵头做这件事,经过研究实现组件化的通信方案通过URL Scheme,所以想着现在还是在 ...
- 操作系统篇-分段机制与GDT|LDT
|| 版权声明:本文为博主原创文章,未经博主允许不得转载. 一.前言 在<操作系统篇-浅谈实模式与保护模式>中提到了两种模式,我们说在操作系统中,其实大部分时间是待在保护模式中的. ...
- hibernate的基本xml文件配置
需要导入基本的包hibernate下的bin下的required和同bin下optional里的c3p0包下的所有jar文件,当然要导入mysql的驱动包了.下面需要注意的是hibernate的版本就 ...
- x01.os.23: 制作 linux LiveCD
1.首先运行如下命令 sudo apt-get install wget bc build-essential gawk genisoimage 2.下载如下资源,make all 即可 http: ...
- Storm介绍(一)
作者:Jack47 PS:如果喜欢我写的文章,欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 内容简介 本文是Storm系列之一,介绍了Storm的起源,Storm ...
- 学习笔记:delphi之TStringGrid
1.说明 最近加入了一个项目组,使用的开发工具是delphi6,想想又要开始搞这个工具有点小忧伤,但没办法谁让咱就是个打杂的尼... 的需求是显示一个类似于Word/excel的那种表格,可以合并列等 ...
- 酷酷的CSS3三角形运用
概述 在早期的前端Web设计开发年代,完成一些页面元素时,我们必须要有专业的PS美工爸爸,由PS美工爸爸来切图,做一些圆角.阴影.锯齿或者一些小图标. 在CSS3出现后,借助一些具有魔力的CSS3属性 ...