1. 引言html
从今天开始系统的学习网络爬虫。写这篇博客的目的在于,一来记录下本身的学习过程;二来但愿能够给像我同样不懂爬虫但又对爬虫十分感兴趣的人带来一些帮助。java
昨天去图书馆找有关爬虫书籍,竟然寥寥无几,且都是泛泛而谈。以后上某宝淘来淘去,只找到一本相关书籍《本身动手写网络爬虫》,虽然在某瓣上看到此书的无数差评,但最终仍是忍痛买下……node
对我而言,学习爬虫不是学习如何使用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数据的持久化。