宽度优先爬虫和带偏好的爬虫的简单实现

图的遍历分为宽度优先遍历和深度优先遍历两种方式,由于网络的无限性,爬虫采用深度优先遍历会导致陷入过深,故应采用宽度优先遍历,同时,还可以根据遍历网页的权重分配优先级,这就是带偏好的遍历。宽度优先遍历从一系列种子节点开始后,应将之后的子节点依次放入待访问队列,同时,应该保存一张已访问的表,遍历前应先查询是否访问过,从而避免重复访问。即可分为下列步骤:
1. 把解析出来的链接和已访问表中的链接进行比较,若不存在此链接,则表示其未被访问过。
2. 把链接放入TODO表,即待处理表。
3. 处理完毕后,再次从TODO表中取出一条链接进行处理,并放入以访问表。
4. 针对该连接所示网页,再次抓取和解析新链接,重复上述过程。

采用宽度优先搜索策略有以下原因:
1. 重要的网页往往离种子较近。
2. 万维网的深度最多能达到17层。总存在一条权重最短的路径能快速到达指定网页。
3. 宽度优先遍历有利于多爬虫合作抓取,多爬虫合作通常先抓取站内链接,封闭性很强。
4. 链接优化:能避开抓取链接的死循环以及该抓取的资源没有抓取到。

在这里,我们使用HttpClient和Html Parser两个工具包实现抓取,首先是自定义一个待访问队列。

package me.zzx.crawler;

import java.util.LinkedList;

/**
* 队列,保存将要访问的URL
* @author zzx
*
*/
public class Queue {
    //使用链表实现队列
    private LinkedList<Object> queue = new LinkedList<Object>();

    //入队
    public void enQueue(Object o) {
        queue.addLast(o);
    }

    //出队
    public Object deQueue() {
        return queue.removeFirst();
    }

    //判断队列是否为空
    public boolean isQueueEmpty() {
        return queue.isEmpty();
    }

    //判断队列是否包含o
    public boolean contains(Object o) {
        return queue.contains(o);
    }

}

然后用一个哈希表存放已访问链接,并和待访问队列一起封装成LinkQueue

package me.zzx.crawler;

import java.util.HashSet;
import java.util.Set;

/**
* 保存已访问过的URL
* @author zzx
*
*/
public class LinkQueue {
    //已访问的URL集合
    private static Set<Object> visitedUrls = new HashSet<Object>();

    //待访问的URL集合
    private static Queue unVisitedUrls = new Queue();

    //获得URL队列
    public static Queue getUnVisitedUrl() {
        return unVisitedUrls;
    }

    //添加到访问过的URL队列中
    public static void addVisitedUrl(String url) {
        visitedUrls.add(url);
    }

    //移除访问过的URL
    public static void removeVisitedUrl(String url) {
        visitedUrls.remove(url);
    }

    //未访问的URL出队
    public static Object unVisitedUrlDequeue() {
        return unVisitedUrls.deQueue();
    }

    //添加到待访问的URL队列中,保证每个URL只被访问一次
    public static void addUnVisitedUrl(String url) {
        if(url != null && !url.trim().equals("")
                && !visitedUrls.contains(url)
                && !unVisitedUrls.contains(url)) {
            unVisitedUrls.enQueue(url);
        }
    }

    //获得已访问的URL数目
    public static int getVisitedUrlNum() {
        return visitedUrls.size();
    }

    //判断待访问的URL队列是否为空
    public static boolean unVisitedUrlIsEmpty() {
        return unVisitedUrls.isQueueEmpty();
    }
}

再创建一个文件下载工具类,用于抓取的下载工作

package me.zzx.crawler;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

/**
* 下载网页工具类
* @author zzx
*
*/
public class DownloadFileUtil {
    /**
     * 根据URL和网页类型生成需要保存的网页的文件名,去除URL中的非文件名字符
     */
    public static String getFilenameByUrl(String url, String contentType) {
        //移除http://或https://
        url = url.charAt(4) == ':' ? url.substring(7) : url.substring(8);
        //text/html类型
        if(contentType.indexOf("html") != -1) {
            url = url.replaceAll("[\\?/:*|<>\"]", "_");
            return url.contains(".html")? url : url + ".html";
        } else {
            //application/pdf等其他类型
            url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."
                    + contentType.substring(contentType.lastIndexOf("/") + 1);
            return url;
        }
    }

    /**
     * 保存网页字节数组到本地文件,filePath为要保存文件的相对地址
     */
    private static void saveToLocal(InputStream data, String filePath) {
        DataOutputStream  dos;
        try {
            dos = new DataOutputStream(new FileOutputStream(new File(filePath)));
            int tempByte = -1;
            while(((tempByte = data.read()) >= 0)) {
                dos.write(tempByte);
            }
            dos.flush();
            dos.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            data = null;
            dos = null;
        }
    }

    /**
     * 下载URL指向的网页
     */
    public static String downloadFile(String url) {
        String filePath = null;
        //生成HttpClient对象
        HttpClient httpClient = new HttpClient();
        //设置HTTP连接超时5秒
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
        //生成GetMethod对象
        GetMethod get = new GetMethod(url);
        //设置get请求超时5秒
        get.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
        //设置请求重试处理
        get.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());

        //执行HTTP GET请求
        try {
            int statusCode = httpClient.executeMethod(get);
            //判断访问状态码
            if(statusCode != HttpStatus.SC_OK) {
                System.err.println("Method Failed: " + get.getStatusLine());
            }
            //处理HTTP响应内容
            InputStream responseBody = get.getResponseBodyAsStream();
            //根据网页URL生成保存时的文件名
            filePath = "temp\\" + getFilenameByUrl(url, get.getResponseHeader("Content-Type").getValue());
            saveToLocal(responseBody, filePath);
        } catch (HttpException e) {
            //发生致命的异常,可能是协议不对或者返回的内容有问题
            System.out.println("Please check your provided http address!");
            e.printStackTrace();
        } catch (IOException e) {
            //发生IO异常
            e.printStackTrace();
        } finally {
            //释放连接,重要
            get.releaseConnection();
        }
        return filePath;
    }
}

另外提供下载文件工具类的基于HttpClient4的写法供参考

package me.zzx.crawler;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.UnknownHostException;

import javax.net.ssl.SSLException;

import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

/**
* 下载网页工具类
* @author zzx
*
*/
public class DownloadFileUtil {
    private static final int DEFAULT_RETRY_TIME = 5;

    /**
     * 根据URL和网页类型生成需要保存的网页的文件名,去除URL中的非文件名字符
     */
    public static String getFilenameByUrl(String url, String contentType) {
        //移除http://或https://
        url = url.charAt(4) == ':' ? url.substring(7) : url.substring(8);
        //text/html类型
        if(contentType.indexOf("html") != -1) {
            url = url.replaceAll("[\\?/:*|<>\"]", "_");
            return url.contains(".html")? url : url + ".html";
        } else {
            //application/pdf等其他类型
            url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."
                    + contentType.substring(contentType.lastIndexOf("/") + 1);
            return url;
        }
    }

    /**
     * 保存网页字节数组到本地文件,filePath为要保存文件的相对地址
     */
    private static void saveToLocal(InputStream data, String filePath) {
        DataOutputStream  dos;
        try {
            dos = new DataOutputStream(new FileOutputStream(new File(filePath)));
            int tempByte = -1;
            while(((tempByte = data.read()) >= 0)) {
                dos.write(tempByte);
            }
            dos.flush();
            dos.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            data = null;
            dos = null;
        }
    }

    /**
     * 下载URL指向的网页
     */
    public static String downloadFile(String url) {
        String filePath = null;
        //生成HttpClient对象
        HttpClient httpClient = new DefaultHttpClient();
        HttpParams params = httpClient.getParams();
        //设置HTTP连接超时5秒
        HttpConnectionParams.setConnectionTimeout(params, 5000);
        //生成GetMethod对象
        HttpGet get = new HttpGet(url);
        //设置get请求超时5秒
        get.getParams().setParameter(HttpConnectionParams.SO_TIMEOUT, 5000);
        //设置请求重试处理
        ((AbstractHttpClient)httpClient).setHttpRequestRetryHandler(new HttpRequestRetryHandler() {
            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                if (executionCount >= DEFAULT_RETRY_TIME) {   
                    return false; 
                } else if (exception instanceof InterruptedIOException) { 
                    // Timeout 
                    return false; 
                } else if (exception instanceof UnknownHostException) { 
                    // Unknown host 
                    return false; 
                } else if (exception instanceof ConnectException) { 
                    return false; 
                } else if (exception instanceof SSLException) { 
                    // SSL handshake exception 
                    return false; 
                } 
                HttpRequest request = (HttpRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST); 
                boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                // Retry if the request is considered idempotent
                if (idempotent)
                    return true; 
                return false; 
            }
        });

        //执行HTTP GET请求
        try {
            HttpResponse response = httpClient.execute(get);
            int statusCode = response.getStatusLine().getStatusCode();
            //判断访问状态码
            if(statusCode != HttpStatus.SC_OK) {
                System.err.println("Method Failed: " + statusCode);
            }
            //处理HTTP响应内容
            InputStream responseBody = response.getEntity().getContent();
            //根据网页URL生成保存时的文件名
            filePath = "temp\\" + getFilenameByUrl(url, response.getEntity().getContentType().getValue());
            saveToLocal(responseBody, filePath);
        } catch (IOException e) {
            //发生IO异常
            e.printStackTrace();
        } finally {
            //释放连接,重要
            get.abort();
        }
        return filePath;
    }
}

再使用引入的Html Parser,构建一个网页链接解析类

package me.zzx.crawler;

import java.util.HashSet;
import java.util.Set;

import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.tags.ImageTag;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

public class HtmlParserUtil {
    //获取一个网站上的链接,filter用来过滤链接
    public static Set<String> extracLinks(String url, LinkFilter filter) {
        Set<String> links = new HashSet<String>();
        try {
            Parser parser  = new Parser(url);
            parser.setEncoding("utf-8");
            //过滤<frame>标签的filter,用来提取frame标签里的src属性
            NodeFilter frameFilter = new NodeFilter() {
                private static final long serialVersionUID = 1L;
                @Override
                public boolean accept(Node node) {
                    if(node.getText().startsWith("frame src="))
                        return true;
                    return false;   
                }
            };

            //OrFilter来设置过滤<a><frame><img>标签
            NodeFilter[] predicates = new NodeFilter[]{new NodeClassFilter(ImageTag.class), new NodeClassFilter(LinkTag.class), frameFilter};
            OrFilter linkFilter = new OrFilter(predicates);
            //得到所有经过过滤的标签
            NodeList list = parser.extractAllNodesThatMatch(linkFilter);
            for(int i = 0; i < list.size(); i++) {
                Node tag = list.elementAt(i);
                //<a>标签
                if(tag instanceof LinkTag) {
                    LinkTag link = (LinkTag) tag;
                    String linkUrl = link.getLink();
                    if(filter.accept(linkUrl)) links.add(linkUrl);
                //<image>标签
                } else if(tag instanceof ImageTag) {
                    ImageTag image = (ImageTag) tag;
                    String imageUrl = image.getImageURL();
                    if(filter.accept(imageUrl)) links.add(imageUrl);
                //<frame>标签
                } else {
                    //提取frame里的src属性的链接
                    String frame = tag.getText();
                    //System.out.println(frame);
                    int start = frame.indexOf("src=");
                    frame = frame.substring(start);
                    int end = frame.indexOf("/");
                    if(end == -1) end = frame.indexOf(">");
                    String frameUrl = frame.substring(5, end - 1);
                    if(filter.accept(frameUrl)) links.add(frameUrl);
                }
            }
        } catch (ParserException e) {
            e.printStackTrace();
        }
        return links;
    }
}

创建一个监听器接口,用于监听抓取特定链接

package me.zzx.crawler;

public interface LinkFilter {
    public boolean accept(String url);
}

最后是爬虫的主程序

package me.zzx.crawler;

import java.util.Set;

public class TestCrawler {
    /**
     * 使用种子初始化URL队列
     * @param seeds 种子URL
     */
    private void initCrawlerWithSeeds(String[] seeds) {
        for(String seed : seeds)
            LinkQueue.addUnVisitedUrl(seed);
    }

    /**
     * 抓取过程
     * @param seeds
     */
    public void crawling(String[] seeds) {
        //定义过滤器,提取以http(s)://www.alibaba.com开头的链接
        LinkFilter filter = new LinkFilter() {
            @Override
            public boolean accept(String url) {
                if(url.startsWith("http://www.alibaba.com") || url.startsWith("https://www.alibaba.com"))
                    return true;
                return false;
            }
        };

        //初始化URL队列
        initCrawlerWithSeeds(seeds);

        //循环条件:待抓取队列不为空且已抓取的网页不多于1000
        while(!LinkQueue.unVisitedUrlIsEmpty() && LinkQueue.getVisitedUrlNum() <= 1000) {
            String visitingUrl = (String) LinkQueue.unVisitedUrlDequeue();
            if(visitingUrl == null) continue;
            //下载网页
            DownloadFileUtil.downloadFile(visitingUrl);
            //该URL放入已访问的URL中
            LinkQueue.addVisitedUrl(visitingUrl);
            //提取出新的URL
            Set<String> links = HtmlParserUtil.extracLinks(visitingUrl, filter);
            //新的未访问URL入队
            for(String link : links) {
                LinkQueue.addUnVisitedUrl(link);
            }
        }
    }

    //main方法入口
    public static void main(String[] args) {
        TestCrawler crawler = new TestCrawler();
        crawler.crawling(new String[] {"https://www.alibaba.com"});
    }
}

在将抓取的URL链接入队后,不一定严格按照先进先出的策略去访问,而是可以有选择地将权重值较高地链接先访问,影响权重的因素很多,包括链接地欢迎度IB(P),链接地重要度IL(P)以及平均链接深度等。在这里,我们使用Java内置地支持优先级的队列,来替换掉原有LinkQueue的实现,代码如下

package me.zzx.crawler;

import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;

/**
* 保存已访问过的URL
* @author zzx
*
*/
public class PreferenceLinkQueue {
    //已访问的URL集合
    private static Set<Object> visitedUrls = new HashSet<Object>();

    //待访问的URL集合
    private static PriorityQueue<Object> unVisitedUrls = new PriorityQueue<Object>();

    //获得URL队列
    public static PriorityQueue<Object> getUnVisitedUrl() {
        return unVisitedUrls;
    }

    //添加到访问过的URL队列中
    public static void addVisitedUrl(String url) {
        visitedUrls.add(url);
    }

    //移除访问过的URL
    public static void removeVisitedUrl(String url) {
        visitedUrls.remove(url);
    }

    //未访问的URL出队
    public static Object unVisitedUrlDequeue() {
        return unVisitedUrls.poll();
    }

    //添加到待访问的URL队列中,保证每个URL只被访问一次
    public static void addUnVisitedUrl(String url) {
        if(url != null && !url.trim().equals("")
                && !visitedUrls.contains(url)
                && !unVisitedUrls.contains(url)) {
            unVisitedUrls.add(url);
        }
    }

    //获得已访问的URL数目
    public static int getVisitedUrlNum() {
        return visitedUrls.size();
    }

    //判断待访问的URL队列是否为空
    public static boolean unVisitedUrlIsEmpty() {
        return unVisitedUrls.isEmpty();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值