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

来源:互联网 发布:数据开发工程师 编辑:程序博客网 时间:2024/06/06 18:58

图的遍历分为宽度优先遍历和深度优先遍历两种方式,由于网络的无限性,爬虫采用深度优先遍历会导致陷入过深,故应采用宽度优先遍历,同时,还可以根据遍历网页的权重分配优先级,这就是带偏好的遍历。宽度优先遍历从一系列种子节点开始后,应将之后的子节点依次放入待访问队列,同时,应该保存一张已访问的表,遍历前应先查询是否访问过,从而避免重复访问。即可分为下列步骤:
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
原创粉丝点击