《自己动手写网络爬虫》笔记3-宽度优先遍历互联网

之前写的是获取单个网页的内容,但是在实际项目中是需要遍历整个网络的相关网页。图论中有深度优先遍历和宽度优先遍历,深度优先可能会因为过”深“或者进入黑洞;同时,也不能完全按照宽度优先进行遍历,需要进行优先级排序。


1.图的宽度优先遍历

先回顾一下图论中的有向图的BFS宽度优先遍历算法。
例题:如图,根据BFS写出各个节点的遍历顺序
这里写图片描述
首先任选一点A作为开始节点(种子节点)。

操作队列中的元素
初始
A入队A
A出队
BCDEF入队BCDEF
B出队CDEF
C出队DEF
D出队EF
E出队F
H入队FH
F出队H
G入队HG
H出队G
I入队GI
G出队I
I出队

所以图的优先遍历顺序为ABCDEFHGI
算法总结:
1.任选顶点V入队
2.当队列非空时继续执行否则停止算法
3.队列头部元素M出队列,访问并且标记M已经被访问过
4.查找M的邻接顶点X
5.若X已经被访问则继续寻找邻接顶点,若没有,则X入队
6.循环第五步,直到M的所有邻接顶点均已入栈,若M的所有邻接顶点均已被访问过(即没有一个X入栈)则跳转步骤2

2.宽度优先遍历互联网

在网页中所有的节点都是html网页,对于非HTML文档可以看成是终端节点。并且网络的宽度优先遍历不是从单个的链接开始的,而是从一系列链接开始的,把这些网页中的“子节点”就是超链接提取出来,放入TODO队列重依次进行抓取。被处理过的链接需要放入一张Visited表中,每次处理一个链接之前,需要判断这个链接是否已经在Visited表中,若是,则已经处理过,则跳过这个链接,若不是,则继续处理。
上面拿到例题,用TODO表和Visited表来表示就是:

TODO表Visited表
A
BCDEFA
CDEFAB
DEFABC
EFABCD
FHABCDE
HGABCDEF
GIABCDEFH
IABCDEFHG
ABCDEFHGI

为什么使用宽度优先遍历的爬虫策略

他是爬虫中使用最广泛的一种策略
1.重要的网页往往离最初选择的种子节点比较近,随着宽度的深入,网页的重要性就会降低
2.万维网的实际深度最多能够达到17层,但是到达某一个具体的网页总是存在一条很短路径,宽度优先遍历总是能以最快的速度到达这个网页
3.宽度有限有利于多爬虫的合作抓取,多爬虫通常先抓取站内的链接,抓取的封闭性很强

3.宽度优先遍历的Java实现

这里写图片描述

实现一个存储URL的队列

public class Queue {
    //使用链表实现队列
    private LinkedList queue=new LinkedList();
    //入队列
    public void enQueue(Object t){
        queue.addLast(t);
    }
    //出队列
    public Object deQueue(){
        return queue.removeFirst();
    }
    //判断队列是否为空
    public boolean isQueueEmpty(){
        return queue.isEmpty();
    }
    //判断队列是否包含某一个元素
    public boolean contains(Object t){
        return queue.contains(t);
    }
}

这里书上多了一个函数,可能是作者忘记删除的
这里写图片描述
这两个函数的作用是一模一样的,所以只需要其中一个就行了,删除另外一个。

实现一个存储已经访问过的URL的队列

每当从URL队列中取得一个url进行查询之前,需要先在
visitedUrl队列中查询是否已经访问过该节点,然后才能对该节点进行处理

public class LinkQueue {
    //已经访问的URL集合
    private static Set visitedUrl = new HashSet();
    //带访问的url集合
    private static Queue unVisitedUrl = new Queue();

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

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

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

    //未访问的URL出队列
    public static Object unVisitedUrl(String url) {
        return unVisitedUrl.deQueue();
    }
    //保证每个URL只能被访问一次

    /**
     * 每一个url不是null并且字符串有效,不包含在已经访问过的节点集合中
     * 也不包含在没有访问过的节点集合中
     *
     * @param url
     */
    public static void addUnvisitedUrl(String url) {
        if (url != null && !url.trim().equals("") &&
                !visitedUrl.contains(url) &&
                !unVisitedUrl.contains(url))
            unVisitedUrl.enQueue(url);
    }

    //获得已经访问过的URL的数量
    public static int getVisitedUrlNum() {
        return visitedUrl.size();
    }

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

实现一个网页信息下载类

public class DownLoadFile {
    //根据URL和网页类型生成需要保存的网页的文件名,取出URL中的非文件名字符
    public String getFileNameByUrl(String url, String contentType) {
        //移除http
        url = url.substring(7);
        //text或者html类型
        if (contentType.indexOf("html") != -1) {
            url = url.replaceAll("[\\?/:*|<>\"]", "_") + ".html";
            return url;
        } else {//如application或者pdf类型
            return url.replaceAll("[\\?/:*|<>\"]", "_") + "."
                    + contentType.substring(contentType.lastIndexOf("/") + 1);
        }
    }

    //保存网页字节数组到本地文件,filepath为要保存的文件的相对地址
    private void saveToLocal(byte[] data, String filepath) {
        try {
            //System.out.println(filepath);
                DataOutputStream out = new DataOutputStream(
                        new FileOutputStream(new File(filepath)));
            for (int i = 0; i < data.length; i++) {
                out.write(data[i]);
            }
            out.flush();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //下载URL指向的网页
    public String downloadFile(String url) {
        String filepath = null;
        //1.生成HttpClient对象并设置参数
        HttpClient httpClient = new HttpClient();
        //设置HTTP链接超时5秒
        httpClient.getHttpConnectionManager().getParams()
                .setConnectionTimeout(5000);
        //2.生成GetMethod对象并设置参数
        GetMethod getMethod = new GetMethod(url);
        //设置GetMethod请求超时5秒
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
        //设置请求重试处理
        getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler());
        //3.执行HttpGet请求
        try {
            int statusCode = httpClient.executeMethod(getMethod);
            //判断访问状态码
            if (statusCode != HttpStatus.SC_OK) {
                System.err.println("Method failed:" + getMethod.getStatusLine());
                filepath = null;
            }
            //4.处理HTTP响应内容
            byte[] responseBody = getMethod.getResponseBody();//读取为字节数组
            //根据网页url生成保存时的文件名
            filepath ="temp//" +  getFileNameByUrl(url,
                    getMethod.getResponseHeader("Content-Type").getValue());
            //System.out.println(filepath);
            saveToLocal(responseBody, filepath);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            getMethod.releaseConnection();
        }
        return filepath;
    }
}

这里需要注意的是,书上的地址保存是temp/网页名,这里的temp文件夹需要自己新建,建在所在工程文件的文件夹下,之后所有爬到的网页都会在这个temp文件夹里面这是相对路径,你也可以在某一个盘上建一个绝对路径的文件夹,如D://temp//

页面解析工具类

这个类需要用到外部包HTMLParser 2.0的包,一定要下载最新的包,不然里面缺少某些类如org.htmlParser.Parser。

public class HtmlParserTool {
    //获取一个网页上的链接,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() {
                @Override
                public boolean accept(Node node) {
                    if (node.getText().startsWith("frame src=")) {
                        return true;
                    } else return false;
                }
            };
            //OrFilter来设置过滤<a>标签和<frame>标签
            OrFilter linkFilter = new OrFilter(new NodeClassFilter(LinkTag.class), frameFilter);
            //得到所有经过过滤的标签
            NodeList list = parser.extractAllNodesThatMatch(linkFilter);
            for (int i = 0; i < list.size(); i++) {
                Node tag = list.elementAt(i);
                if (tag instanceof LinkTag)//<a>标签
                {
                    LinkTag link = (LinkTag) tag;
                    String linkUrl = link.getLink();
                    if (filter.accept(linkUrl)) links.add(linkUrl);
                } else //<frame>标签
                {
                    //提取frame里src属性的链接如<frame src="test.html">
                    String frame = tag.getText();
                    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;
    }

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

1.书上也说了LinkFilter是一个接口,并且实现为内部类
2.编码形式设置为UTF-8 parser.setEncoding("UTF-8");不然可能会出现中文字或者其他字符乱码的情况

主程序

public class Main {

    /**
     * 使用种子初始化URL队列
     *
     * @param seeds
     */
    private void initCrawlerWithSeeds(String[] seeds) {
        for (int i = 0; i < seeds.length; i++) {
            LinkQueue.addUnvisitedUrl(seeds[i]);
        }
    }

    /**
     * 抓取过程
     *
     * @param seeds
     */
    public void crawling(String[] seeds) {
        //定义过滤器,提取以http://www.lietu.com开头的链接
        HtmlParserTool.LinkFilter filter = new HtmlParserTool.LinkFilter() {
            @Override
            public boolean accept(String url) {
                if (url.startsWith("http://www.lietu.com")) return true;
                else return false;
            }
        };
        //抓取过程
        initCrawlerWithSeeds(seeds);
        //循环条件:待抓取的链接不空并且抓取的网页数量不多于1000
        while (!LinkQueue.unVisitedUrlEmpty() && LinkQueue.getVisitedUrlNum() <= 1000) {
            //队头出队
            String visitUrl = (String) LinkQueue.unVisitedDeUrl();
            if (visitUrl == null) continue;
            DownLoadFile downLoad = new DownLoadFile();
            //下载网页
            downLoad.downloadFile(visitUrl);
            //将该URL放入已访问的URL队列中
            LinkQueue.addVisitedUrl(visitUrl);
            //提取出下载网页中的URL
            Set<String> links = HtmlParserTool.extracLinks(visitUrl, filter);
            //新的未访问的url入队
            for (String link : links) {
                LinkQueue.addUnvisitedUrl(link);
            }
        }
    }

    public static void main(String[] args) {
        Main crawler = new Main();
        crawler.crawling(new String[]{"http://lietu.com"});
    }
}

得到的结果
这里写图片描述

换一个百度搜索www.baidu.com,搜以www.baidu.com开头的网页结果
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值