爬虫入门——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怎去抓取网页信息以及获取相应的图片标题等信 ...
随机推荐
- 你必须知道的EF知识和经验
注意:以下内容如果没有特别申明,默认使用的EF6.0版本,code first模式. 推荐MiniProfiler插件 工欲善其事,必先利其器. 我们使用EF和在很大程度提高了开发速度,不过随之带来的 ...
- 从0开始搭建SQL Server AlwaysOn 第三篇(配置AlwaysOn)
从0开始搭建SQL Server AlwaysOn 第三篇(配置AlwaysOn) 第一篇http://www.cnblogs.com/lyhabc/p/4678330.html第二篇http://w ...
- Python高手之路【四】python函数装饰器
def outer(func): def inner(): print('hello') print('hello') print('hello') r = func() print('end') p ...
- .Net 大型分布式基础服务架构横向演变概述
一. 业务背景 构建具备高可用,高扩展性,高性能,能承载高并发,大流量的分布式电子商务平台,支持用户,订单,采购,物流,配送,财务等多个项目的协作,便于后续运营报表,分析,便于运维及监控. 二. 基础 ...
- UWP开发之Mvvmlight实践七:如何查找设备(Mobile模拟器、实体手机、PC)中应用的Log等文件
在开发中或者后期测试乃至最后交付使用的时候,如果应用出问题了我们一般的做法就是查看Log文件.上章也提到了查看Log文件,这章重点讲解下如何查看Log文件?如何找到我们需要的Packages安装包目录 ...
- BPM配置故事之案例10-获取外部数据
老李:Hi,小明,我又来了 小明:--这次又怎么了. 老李:之前的物资管理方式太混乱了,这段时间我整理了采购物资清单,现在都录入到我们的ERP中了,以后申请物资改成从ERP数据选择吧.物资明细表我也做 ...
- Android中访问sdcard路径的几种方式
以前的Android(4.1之前的版本)中,SDcard路径通过"/sdcard"或者"/mnt/sdcard"来表示,而在JellyBean(安卓4.1)系统 ...
- C语言可以开发哪些项目?
C语言是我们大多数人的编程入门语言,对其也再熟悉不过了,不过很多初学者在学习的过程中难免会出现迷茫,比如:不知道C语言可以开发哪些项目,可以应用在哪些实际的开发中--,这些迷茫也导致了我们在学习的过程 ...
- DirectX Graphics Infrastructure(DXGI):最佳范例 学习笔记
今天要学习的这篇文章写的算是比较早的了,大概在DX11时代就写好了,当时龙书11版看得很潦草,并没有注意这篇文章,现在看12,觉得是跳不过去的一篇文章,地址如下: https://msdn.micro ...
- redis大幅性能提升之使用管道(PipeLine)和批量(Batch)操作
前段时间在做用户画像的时候,遇到了这样的一个问题,记录某一个商品的用户购买群,刚好这种需求就可以用到Redis中的Set,key作为productID,value 就是具体的customerid集合, ...