黄磊的编程日记

一个善于用Java,Python解决各种问题的伪全栈工程师

首页 GitHub 科学上网 关于

2016-03-08 23:29:47
从零开始写爬虫:01.最简单爬虫的实现

源码地址:spider4hl.

之前有研究过larbin爬虫源码,但是当时偷懒没有记录下来,结果最近发现全忘光了,所以决定自己用java从零开始实现一个爬虫.我觉得这比单纯的看代码要有效很多.

为什么用java?因为开源的东西比较多,能借鉴的东西比较多,写它的原因也就是通过重造轮子来学技术嘛,等有实力时再去想技术创新.当然也是因为工作上用的是java,不然好想用python,这个写爬虫有天然优势.

我主要借鉴的是crawler4j爬虫,这里是它的源码.不过后面我有可能会看一些nutch的,现在还看不大懂,有点过于复杂了.


1.简单的爬虫架构

不多说了,要想实现一个爬虫,那么最首先要做的事就是对爬虫的架构有个大致的了解,最简单爬虫就是基于这个最简单的架构实现的.

img

简单叙述一下流程,首先我们提供一个Url,然后先将其保存至已访问队列中,再根据这个Url页面我们可以提取出一大堆Url,然后将这一大堆的Url保留在待访问的Url队列中,存入之前要判断其是否已经被访问.

这个是最最简单的爬虫架构,还有很多外加条件,这些都是以后要干的,今天我们只实现这些.不过我还添加了是否是垂直爬虫的条件,其实就是在<是否被访问>这里添加了一个条件,就是是否Url含有初始Url,这些大家都可以从代码中看.


2.代码结构

下面是我代码的类结构

img

  • org.leihuang.spider4hl.crawler 负责整个爬虫爬取的过程
  • org.leihuang.spider4hl.fetcher 负责页面内容抓取逻辑
  • org.leihuang.spider4hl.frontier 等待的Url队列和已访问的Url队列
  • org.leihuang.spider4hl.parser 对fetcher中抓取下来的内容进行解析

首先我们先写fetcher包中的内容,先实现根据url抓取内容下来,然后需要用到什么我们就补充什么.

public class PageFetchResult {
o/* 保存抓取到的内容
o * 抓取的页面内容,url,content
o */
o
oprivate String url = null ;
oprivate String content = null ;
o
opublic String getUrl() {
oreturn url;
o}
opublic void setUrl(String url) {
othis.url = url;
o}
opublic String getContent() {
oreturn content;
o}
opublic void setContent(String content) {
othis.content = content;
o}
}
//传进一个Url,然后对该页面抓取到的内容存入到一个pageFetchResult对象中.
public class PageFetcher {
o/*
o * 通过Url,爬去该Url页面的内容
o * 
o * @return content
o */
oprivate CloseableHttpClient client = null;

opublic PageFetchResult fetchPage(String url) {
o// 将页面的结果保存到pageFetchResult中
oPageFetchResult pageFetchResult = new PageFetchResult();
o// 创建一个httpclient客户端
oclient = HttpClients.createDefault();
oHttpGet getHttp = new HttpGet(url);
oHttpResponse response;
otry {
o// 获得信息
oresponse = client.execute(getHttp);
oHttpEntity entity = response.getEntity();
o
o//将该url添加到已访问队列中
oOutUrlQueues.add(url);
o
o//将抓取到的页面信息存入到pageFetchResult中
oif (url != null && entity != null) {
opageFetchResult.setUrl(url);
opageFetchResult.setContent(EntityUtils.toString(entity));
o}
o} catch (ClientProtocolException e) {
oe.printStackTrace();
o} catch (IOException e) {
oe.printStackTrace();
o}finally{
otry {
oclient.close();
o} catch (IOException e) {
oe.printStackTrace();
o}
o}

oreturn pageFetchResult;
o}
}

抓取的过程中我们需要用到两个队列,下面是它们的代码.

//已访问的url队列
public class OutUrlQueues {
o//此处存储已经访问过的Url
oprivate static HashSet<String> outQueues = new HashSet<String>() ;
o
o//添加一个已经访问过的url
opublic static void add(String url){
ooutQueues.add(url) ;
o}
o
o//删除一个已经访问过的url
opublic static boolean remove(String url){
oreturn outQueues.remove(url) ;
o}
o
o//已访问队列中是否已经存在该url
opublic static boolean contains(String url){
oreturn outQueues.contains(url) ;
o}
o
o//得到该队列url的数量
opublic static int size(){
oreturn outQueues.size() ;
o}
o
}
//等待访问的url队列
public class WaitingUrlQueues {
o// 保存等待接受抓取的url,因为主要涉及删除添加操作,所以用LinkedList
oprivate static LinkedList<String> waitingUrlQueues = new LinkedList<String>();

o//提取出一个url
opublic static String poll(){
oreturn waitingUrlQueues.pollFirst() ;
o}
o
o// 添加一个新的url到等待队列中
opublic static void add(String url) {
owaitingUrlQueues.add(url);
o}

o// 删除等待队列中的某个url
opublic static boolean remove(String url) {
oreturn waitingUrlQueues.remove(url);
o}

o// 判断是否存在已经存在该url
opublic static boolean contains(String url) {
oreturn waitingUrlQueues.contains(url);
o}

o// 得到该队列url的数量
opublic static int size() {
oreturn waitingUrlQueues.size();
o}
o
opublic static boolean isEmpty(){
oreturn waitingUrlQueues.isEmpty() ;
o}

}

然后是对抓取下来的内容进行解析,此处我们只从中提取,url.

//从html页面内容中提取url
public class UrlParser {

opublic void getUrlFromPage(String content, String url,
oboolean isVerticalcrawler) {

oDocument doc = null;
odoc = Jsoup.parse(content, url);
oElements links = doc.getElementsByTag("a");

o// 垂直爬虫--只获取特定目录下的网页
oif (!links.isEmpty() && isVerticalcrawler) {
ofor (Element link : links) {
o// linkHref完整链接
oString linkHref = link.absUrl("href");
o// 判断等待队列和已访问队列中是否存在,且该链接必须以主页url开头
oif (!OutUrlQueues.contains(linkHref)
o&& linkHref.startsWith(url)
o&& !WaitingUrlQueues.contains(linkHref)) {
o// 添加链接到等待抓取的队列中
oWaitingUrlQueues.add(linkHref);
o}
o}
o}
o// 通用爬虫--爬取一切链接
oelse if (!links.isEmpty() && !isVerticalcrawler) {
ofor (Element link : links) {
o// linkHref完整链接
oString linkHref = link.absUrl("href");
o// 判断等待队列和已访问队列中是否存在,且该链接必须以http开头
oif (!OutUrlQueues.contains(linkHref)
o&& linkHref.startsWith("http")
o&& !WaitingUrlQueues.contains(linkHref)) {
o// 添加链接到等待抓取的队列中
oWaitingUrlQueues.add(linkHref);
o}
o}
o}

o}
}

然后我们再把整个前面的逻辑综合起来,写一个webcrawler类.

//整个爬虫的过程.
public class WebCrawler implements Runnable {

o//传入的参数
oprivate boolean isVerticalcrawler = false;
oprivate String url = null;
oprivate String storageFolder = null ;
o
o//需要用到的
oprivate UrlParser urlParser = null;
oprivate PageFetcher pageFetcher = null;
oprivate PageFetchResult pageFetchResult = null;

opublic WebCrawler(String url, String storageFolder,boolean isVerticalcrawler) {
othis.url = url;
othis.storageFolder = storageFolder ;
oWaitingUrlQueues.add(url);
othis.isVerticalcrawler = isVerticalcrawler;

o// 初始化页面url提取器
ourlParser = new UrlParser();
o// 初始化抓取器
opageFetcher = new PageFetcher();
o}

opublic void run() {
owhile (!WaitingUrlQueues.isEmpty()) {

o//从等待队列中提取一个Url
oString urlCurrent = WaitingUrlQueues.poll() ;
o
o// 页面内容
opageFetchResult = pageFetcher.fetchPage(urlCurrent);
oString contentCurrent = pageFetchResult.getContent() ;
o
o// 对页面中的url进行提取
ourlParser.getUrlFromPage(contentCurrent, urlCurrent,
oisVerticalcrawler);
o
o//下载页面到本地
odownloadPage(urlCurrent,contentCurrent) ;
o}
o}
o//下载页面的html文件
oprivate void downloadPage(String url,String content){
oBufferedWriter bw = null ;
oString names[] = url.split("/") ;
o
oFile folder = new File(storageFolder) ;
oif(!folder.exists()){
ofolder.mkdirs() ;
o}
oFile file = new File(storageFolder+"/"+names[names.length-1]+".html") ;
oif(!file.exists()){
otry {
ofile.createNewFile() ;
o} catch (IOException e) {
oe.printStackTrace();
o}
o}
otry {
o bw = new BufferedWriter(new FileWriter(file)) ;
o bw.write(content);
o bw.flush();
o} catch (IOException e) {
oe.printStackTrace();
o}
o}
}

最后我们对我们写的代码进行测试运行.

public class HtmlPageCrawler {
opublic static void main(String args[]){
o//爬去主页
oString url = "http://leihuang.org" ;
o//是否是垂直爬虫
oboolean isVerticalcrawler = true ;
o//html页面存储地址
oString storageFolder = "/home/lei/data/" ;
o
oThread t = new Thread(new WebCrawler(url,storageFolder,isVerticalcrawler)) ;
ot.start();
o}
}

大家可以直接用上面的代码建立在一个包里面,然后运行一下试试,不过要自己下载许多包.下面是我用到的包. img

为了方便,我用maven建的这个项目,这样可以省去大家自己去下载这些包了.这里是源代码地址:https://github.com/SpeedMe/spider4hl.

这是第一个版本,以后会陆续更新,还要继续补补知识,不然好多地方都不会.主要看得三本书,推荐给大家.

  1. Effective Java, 2nd Edition.pdf
  2. Java Concurrency In Practice.pdf
  3. Head First Pattern Design.pdf

第一本书是为了写出更好的程序,第二本是为了了解并发爬虫编程,第三本则是为了完善代码的架构.总之很好的三本书.

大家可以从这里面下,里面还有我收藏的许多java经典书籍.

http://yunpan.cn/cAWSDgSayIFMu (提取码:5dbc)