[Java] 知乎下巴第5集:使用HttpClient工具包和宽度爬虫

来源:互联网 发布:jpg软件官方下载 编辑:程序博客网 时间:2024/04/29 18:14

下载地址:https://code.csdn.net/wxg694175346/zhihudown


说到爬虫,使用Java本身自带的URLConnection可以实现一些基本的抓取页面的功能,但是对于一些比较高级的功能,比如重定向的处理,HTML标记的去除,仅仅使用URLConnection还是不够的。

在这里我们可以使用HttpClient这个第三方jar包,下载地址点击这里。

接下来我们使用HttpClient简单的写一个爬去百度的Demo:

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. import java.io.FileOutputStream;  
  2. import java.io.InputStream;  
  3. import java.io.OutputStream;  
  4. import org.apache.commons.httpclient.HttpClient;  
  5. import org.apache.commons.httpclient.HttpStatus;  
  6. import org.apache.commons.httpclient.methods.GetMethod;  
  7.   
  8. /** 
  9.  *  
  10.  * @author CallMeWhy 
  11.  *  
  12.  */  
  13. public class Spider {  
  14.     private static HttpClient httpClient = new HttpClient();  
  15.   
  16.     /** 
  17.      * @param path 
  18.      *            目标网页的链接 
  19.      * @return 返回布尔值,表示是否正常下载目标页面 
  20.      * @throws Exception 
  21.      *             读取网页流或写入本地文件流的IO异常 
  22.      */  
  23.     public static boolean downloadPage(String path) throws Exception {  
  24.         // 定义输入输出流  
  25.         InputStream input = null;  
  26.         OutputStream output = null;  
  27.         // 得到 post 方法  
  28.         GetMethod getMethod = new GetMethod(path);  
  29.         // 执行,返回状态码  
  30.         int statusCode = httpClient.executeMethod(getMethod);  
  31.         // 针对状态码进行处理  
  32.         // 简单起见,只处理返回值为 200 的状态码  
  33.         if (statusCode == HttpStatus.SC_OK) {  
  34.             input = getMethod.getResponseBodyAsStream();  
  35.             // 通过对URL的得到文件名  
  36.             String filename = path.substring(path.lastIndexOf('/') + 1)  
  37.                     + ".html";  
  38.             // 获得文件输出流  
  39.             output = new FileOutputStream(filename);  
  40.             // 输出到文件  
  41.             int tempByte = -1;  
  42.             while ((tempByte = input.read()) > 0) {  
  43.                 output.write(tempByte);  
  44.             }  
  45.             // 关闭输入流  
  46.             if (input != null) {  
  47.                 input.close();  
  48.             }  
  49.             // 关闭输出流  
  50.             if (output != null) {  
  51.                 output.close();  
  52.             }  
  53.             return true;  
  54.         }  
  55.         return false;  
  56.     }  
  57.   
  58.     public static void main(String[] args) {  
  59.         try {  
  60.             // 抓取百度首页,输出  
  61.             Spider.downloadPage("http://www.baidu.com");  
  62.         } catch (Exception e) {  
  63.             e.printStackTrace();  
  64.         }  
  65.     }  
  66. }  

但是这样基本的爬虫是不能满足各色各样的爬虫需求的。


先来介绍宽度优先爬虫。

宽度优先相信大家都不陌生,简单说来可以这样理解宽度优先爬虫。

我们把互联网看作一张超级大的有向图,每一个网页上的链接都是一个有向边,每一个文件或没有链接的纯页面则是图中的终点:



宽度优先爬虫就是这样一个爬虫,爬走在这个有向图上,从根节点开始一层一层往外爬取新的节点的数据。

宽度遍历算法如下所示:

(1) 顶点 V 入队列。
(2) 当队列非空时继续执行,否则算法为空。
(3) 出队列,获得队头节点 V,访问顶点 V 并标记 V 已经被访问。
(4) 查找顶点 V 的第一个邻接顶点 col。
(5) 若 V 的邻接顶点 col 未被访问过,则 col 进队列。
(6) 继续查找 V 的其他邻接顶点 col,转到步骤(5),若 V 的所有邻接顶点都已经被访问过,则转到步骤(2)。


按照宽度遍历算法,上图的遍历顺序为:A->B->C->D->E->F->H->G->I,这样一层一层的遍历下去。

而宽度优先爬虫其实爬取的是一系列的种子节点,和图的遍历基本相同。

我们可以把需要爬取页面的URL都放在一个TODO表中,将已经访问的页面放在一个Visited表中:


则宽度优先爬虫的基本流程如下:

(1) 把解析出的链接和 Visited 表中的链接进行比较,若 Visited 表中不存在此链接, 表示其未被访问过。
(2) 把链接放入 TODO 表中。
(3) 处理完毕后,从 TODO 表中取得一条链接,直接放入 Visited 表中。
(4) 针对这个链接所表示的网页,继续上述过程。如此循环往复。


下面我们就来一步一步制作一个宽度优先的爬虫。

首先,对于先设计一个数据结构用来存储TODO表, 考虑到需要先进先出所以采用队列,自定义一个Quere类:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. import java.util.LinkedList;  
  2.   
  3. /** 
  4.  * 自定义队列类 保存TODO表 
  5.  */  
  6. public class Queue {  
  7.   
  8.     /** 
  9.      * 定义一个队列,使用LinkedList实现 
  10.      */  
  11.     private LinkedList<Object> queue = new LinkedList<Object>(); // 入队列  
  12.   
  13.     /** 
  14.      * 将t加入到队列中 
  15.      */  
  16.     public void enQueue(Object t) {  
  17.         queue.addLast(t);  
  18.     }  
  19.   
  20.     /** 
  21.      * 移除队列中的第一项并将其返回 
  22.      */  
  23.     public Object deQueue() {  
  24.         return queue.removeFirst();  
  25.     }  
  26.   
  27.     /** 
  28.      * 返回队列是否为空 
  29.      */  
  30.     public boolean isQueueEmpty() {  
  31.         return queue.isEmpty();  
  32.     }  
  33.   
  34.     /** 
  35.      * 判断并返回队列是否包含t 
  36.      */  
  37.     public boolean contians(Object t) {  
  38.         return queue.contains(t);  
  39.     }  
  40.   
  41.     /** 
  42.      * 判断并返回队列是否为空 
  43.      */  
  44.     public boolean empty() {  
  45.         return queue.isEmpty();  
  46.     }  
  47.   
  48. }  



还需要一个数据结构来记录已经访问过的 URL,即Visited表。

考虑到这个表的作用,每当要访问一个 URL 的时候,首先在这个数据结构中进行查找,如果当前的 URL 已经存在,则丢弃这个URL任务。

这个数据结构需要不重复并且能快速查找,所以选择HashSet来存储。

综上,我们另建一个SpiderQueue类来保存Visited表和TODO表:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. import java.util.HashSet;  
  2. import java.util.Set;  
  3.   
  4. /** 
  5.  * 自定义类 保存Visited表和unVisited表 
  6.  */  
  7. public class SpiderQueue {  
  8.   
  9.     /** 
  10.      * 已访问的url集合,即Visited表 
  11.      */  
  12.     private static Set<Object> visitedUrl = new HashSet<>();  
  13.   
  14.     /** 
  15.      * 添加到访问过的 URL 队列中 
  16.      */  
  17.     public static void addVisitedUrl(String url) {  
  18.         visitedUrl.add(url);  
  19.     }  
  20.   
  21.     /** 
  22.      * 移除访问过的 URL 
  23.      */  
  24.     public static void removeVisitedUrl(String url) {  
  25.         visitedUrl.remove(url);  
  26.     }  
  27.   
  28.     /** 
  29.      * 获得已经访问的 URL 数目 
  30.      */  
  31.     public static int getVisitedUrlNum() {  
  32.         return visitedUrl.size();  
  33.     }  
  34.   
  35.     /** 
  36.      * 待访问的url集合,即unVisited表 
  37.      */  
  38.     private static Queue unVisitedUrl = new Queue();  
  39.   
  40.     /** 
  41.      * 获得UnVisited队列 
  42.      */  
  43.     public static Queue getUnVisitedUrl() {  
  44.         return unVisitedUrl;  
  45.     }  
  46.   
  47.     /** 
  48.      * 未访问的unVisitedUrl出队列 
  49.      */  
  50.     public static Object unVisitedUrlDeQueue() {  
  51.         return unVisitedUrl.deQueue();  
  52.     }  
  53.   
  54.     /** 
  55.      * 保证添加url到unVisitedUrl的时候每个 URL只被访问一次 
  56.      */  
  57.     public static void addUnvisitedUrl(String url) {  
  58.         if (url != null && !url.trim().equals("") && !visitedUrl.contains(url)  
  59.                 && !unVisitedUrl.contians(url))  
  60.             unVisitedUrl.enQueue(url);  
  61.     }  
  62.   
  63.     /** 
  64.      * 判断未访问的 URL队列中是否为空 
  65.      */  
  66.     public static boolean unVisitedUrlsEmpty() {  
  67.         return unVisitedUrl.empty();  
  68.     }  
  69. }  


上面是一些自定义类的封装,接下来就是一个定义一个用来下载网页的工具类,我们将其定义为DownTool类:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package controller;  
  2.   
  3. import java.io.*;  
  4. import org.apache.commons.httpclient.*;  
  5. import org.apache.commons.httpclient.methods.*;  
  6. import org.apache.commons.httpclient.params.*;  
  7.   
  8. public class DownTool {  
  9.     /** 
  10.      * 根据 URL 和网页类型生成需要保存的网页的文件名,去除 URL 中的非文件名字符 
  11.      */  
  12.     private String getFileNameByUrl(String url, String contentType) {  
  13.         // 移除 "http://" 这七个字符  
  14.         url = url.substring(7);  
  15.         // 确认抓取到的页面为 text/html 类型  
  16.         if (contentType.indexOf("html") != -1) {  
  17.             // 把所有的url中的特殊符号转化成下划线  
  18.             url = url.replaceAll("[\\?/:*|<>\"]""_") + ".html";  
  19.         } else {  
  20.             url = url.replaceAll("[\\?/:*|<>\"]""_") + "."  
  21.                     + contentType.substring(contentType.lastIndexOf("/") + 1);  
  22.         }  
  23.         return url;  
  24.     }  
  25.   
  26.     /** 
  27.      * 保存网页字节数组到本地文件,filePath 为要保存的文件的相对地址 
  28.      */  
  29.     private void saveToLocal(byte[] data, String filePath) {  
  30.         try {  
  31.             DataOutputStream out = new DataOutputStream(new FileOutputStream(  
  32.                     new File(filePath)));  
  33.             for (int i = 0; i < data.length; i++)  
  34.                 out.write(data[i]);  
  35.             out.flush();  
  36.             out.close();  
  37.         } catch (IOException e) {  
  38.             e.printStackTrace();  
  39.         }  
  40.     }  
  41.   
  42.     // 下载 URL 指向的网页  
  43.     public String downloadFile(String url) {  
  44.         String filePath = null;  
  45.         // 1.生成 HttpClinet对象并设置参数  
  46.         HttpClient httpClient = new HttpClient();  
  47.         // 设置 HTTP连接超时 5s  
  48.         httpClient.getHttpConnectionManager().getParams()  
  49.                 .setConnectionTimeout(5000);  
  50.         // 2.生成 GetMethod对象并设置参数  
  51.         GetMethod getMethod = new GetMethod(url);  
  52.         // 设置 get请求超时 5s  
  53.         getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);  
  54.         // 设置请求重试处理  
  55.         getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,  
  56.                 new DefaultHttpMethodRetryHandler());  
  57.         // 3.执行GET请求  
  58.         try {  
  59.             int statusCode = httpClient.executeMethod(getMethod);  
  60.             // 判断访问的状态码  
  61.             if (statusCode != HttpStatus.SC_OK) {  
  62.                 System.err.println("Method failed: "  
  63.                         + getMethod.getStatusLine());  
  64.                 filePath = null;  
  65.             }  
  66.             // 4.处理 HTTP 响应内容  
  67.             byte[] responseBody = getMethod.getResponseBody();// 读取为字节数组  
  68.             // 根据网页 url 生成保存时的文件名  
  69.             filePath = "temp\\"  
  70.                     + getFileNameByUrl(url,  
  71.                             getMethod.getResponseHeader("Content-Type")  
  72.                                     .getValue());  
  73.             saveToLocal(responseBody, filePath);  
  74.         } catch (HttpException e) {  
  75.             // 发生致命的异常,可能是协议不对或者返回的内容有问题  
  76.             System.out.println("请检查你的http地址是否正确");  
  77.             e.printStackTrace();  
  78.         } catch (IOException e) {  
  79.             // 发生网络异常  
  80.             e.printStackTrace();  
  81.         } finally {  
  82.             // 释放连接  
  83.             getMethod.releaseConnection();  
  84.         }  
  85.         return filePath;  
  86.     }  
  87. }  

在这里我们需要一个HtmlParserTool类来处理Html标记:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package controller;  
  2.   
  3. import java.util.HashSet;  
  4. import java.util.Set;  
  5. import org.htmlparser.Node;  
  6. import org.htmlparser.NodeFilter;  
  7. import org.htmlparser.Parser;  
  8. import org.htmlparser.filters.NodeClassFilter;  
  9. import org.htmlparser.filters.OrFilter;  
  10. import org.htmlparser.tags.LinkTag;  
  11. import org.htmlparser.util.NodeList;  
  12. import org.htmlparser.util.ParserException;  
  13.   
  14. import model.LinkFilter;  
  15.   
  16. public class HtmlParserTool {  
  17.     // 获取一个网站上的链接,filter 用来过滤链接  
  18.     public static Set<String> extracLinks(String url, LinkFilter filter) {  
  19.         Set<String> links = new HashSet<String>();  
  20.         try {  
  21.             Parser parser = new Parser(url);  
  22.             parser.setEncoding("gb2312");  
  23.   
  24.             // 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性  
  25.             NodeFilter frameFilter = new NodeFilter() {  
  26.                 private static final long serialVersionUID = 1L;  
  27.   
  28.                 @Override  
  29.                 public boolean accept(Node node) {  
  30.                     if (node.getText().startsWith("frame src=")) {  
  31.                         return true;  
  32.                     } else {  
  33.                         return false;  
  34.                     }  
  35.                 }  
  36.             };  
  37.             // OrFilter 来设置过滤 <a> 标签和 <frame> 标签  
  38.             OrFilter linkFilter = new OrFilter(new NodeClassFilter(  
  39.                     LinkTag.class), frameFilter);  
  40.             // 得到所有经过过滤的标签  
  41.             NodeList list = parser.extractAllNodesThatMatch(linkFilter);  
  42.             for (int i = 0; i < list.size(); i++) {  
  43.                 Node tag = list.elementAt(i);  
  44.                 if (tag instanceof LinkTag)// <a> 标签  
  45.                 {  
  46.                     LinkTag link = (LinkTag) tag;  
  47.                     String linkUrl = link.getLink();// URL  
  48.                     if (filter.accept(linkUrl))  
  49.                         links.add(linkUrl);  
  50.                 } else// <frame> 标签  
  51.                 {  
  52.                     // 提取 frame 里 src 属性的链接, 如 <frame src="test.html"/>  
  53.                     String frame = tag.getText();  
  54.                     int start = frame.indexOf("src=");  
  55.                     frame = frame.substring(start);  
  56.                     int end = frame.indexOf(" ");  
  57.                     if (end == -1)  
  58.                         end = frame.indexOf(">");  
  59.                     String frameUrl = frame.substring(5, end - 1);  
  60.                     if (filter.accept(frameUrl))  
  61.                         links.add(frameUrl);  
  62.                 }  
  63.             }  
  64.         } catch (ParserException e) {  
  65.             e.printStackTrace();  
  66.         }  
  67.         return links;  
  68.     }  
  69. }  

最后我们来写个爬虫类调用前面的封装类和函数:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package controller;  
  2. import java.util.Set;  
  3.   
  4. import model.LinkFilter;  
  5. import model.SpiderQueue;  
  6.   
  7. public class BfsSpider {  
  8.     /** 
  9.      * 使用种子初始化URL队列 
  10.      */  
  11.     private void initCrawlerWithSeeds(String[] seeds) {  
  12.         for (int i = 0; i < seeds.length; i++)  
  13.             SpiderQueue.addUnvisitedUrl(seeds[i]);  
  14.     }  
  15.   
  16.     // 定义过滤器,提取以 http://www.xxxx.com开头的链接  
  17.     public void crawling(String[] seeds) {  
  18.         LinkFilter filter = new LinkFilter() {  
  19.             public boolean accept(String url) {  
  20.                 if (url.startsWith("http://www.baidu.com"))  
  21.                     return true;  
  22.                 else  
  23.                     return false;  
  24.             }  
  25.         };  
  26.         // 初始化 URL 队列  
  27.         initCrawlerWithSeeds(seeds);  
  28.         // 循环条件:待抓取的链接不空且抓取的网页不多于 1000  
  29.         while (!SpiderQueue.unVisitedUrlsEmpty()  
  30.                 && SpiderQueue.getVisitedUrlNum() <= 1000) {  
  31.             // 队头 URL 出队列  
  32.             String visitUrl = (String) SpiderQueue.unVisitedUrlDeQueue();  
  33.             if (visitUrl == null)  
  34.                 continue;  
  35.             DownTool downLoader = new DownTool();  
  36.             // 下载网页  
  37.             downLoader.downloadFile(visitUrl);  
  38.             // 该 URL 放入已访问的 URL 中  
  39.             SpiderQueue.addVisitedUrl(visitUrl);  
  40.             // 提取出下载网页中的 URL  
  41.             Set<String> links = HtmlParserTool.extracLinks(visitUrl, filter);  
  42.             // 新的未访问的 URL 入队  
  43.             for (String link : links) {  
  44.                 SpiderQueue.addUnvisitedUrl(link);  
  45.             }  
  46.         }  
  47.     }  
  48.   
  49.     // main 方法入口  
  50.     public static void main(String[] args) {  
  51.         BfsSpider crawler = new BfsSpider();  
  52.         crawler.crawling(new String[] { "http://www.baidu.com" });  
  53.     }  
  54. }  

运行可以看到,爬虫已经把百度网页下所有的页面都抓取出来了:


0 0