简易爬虫的实现
HttpClient 提供了便利的 HTTP 协议访问,使得我们可以很容易的得到某个网页的源码并保存在本地;HtmlParser 提供了如此简便灵巧的类库,可以从网页中便捷的提取出指向其他网页的超链接。笔者结合这两个开源包,构建了一个简易的网络爬虫。
爬虫 (Crawler) 原理
学过数据结构的读者都知道有向图这种数据结构。如下图所示,如果将网页看成是图中的某一个节点,而将网页中指向其他网页的链接看成是这个节点指向其他节点的边,那么我们很容易将整个 Internet 上的网页建模成一个有向图。理论上,通过遍历算法遍历该图,可以访问到Internet 上的几乎所有的网页。最简单的遍历就是宽度优先以及深度优先。以下笔者实现的简易爬虫就是使用了宽度优先的爬行策略。
图 2. 网页关系的建模图
简易爬虫实现流程
在看简易爬虫的实现代码之前,先介绍一下简易爬虫爬取网页的流程。
图 3. 爬虫流程图
各个类的源码以及说明
对应上面的流程图,简易爬虫由下面几个类组成,各个类职责如下:
Crawler.java:爬虫的主方法入口所在的类,实现爬取的主要流程。
LinkDb.java:用来保存已经访问的 url 和待爬取的 url 的类,提供url出对入队操作。
Queue.java: 实现了一个简单的队列,在 LinkDb.java 中使用了此类。
FileDownloader.java:用来下载 url 所指向的网页。
HtmlParserTool.java: 用来抽取出网页中的链接。
LinkFilter.java:一个接口,实现其 accept() 方法用来对抽取的链接进行过滤。
下面是各个类的源码,代码中的注释有比较详细的说明。
清单6 Crawler.java
package com.ie; import java.util.Set; public class Crawler { /* 使用种子 url 初始化 URL 队列*/ private void initCrawlerWithSeeds(String[] seeds) { for(int i=0;i<seeds.length;i++) LinkDB.addUnvisitedUrl(seeds[i]); } /* 爬取方法*/ public void crawling(String[] seeds) { LinkFilter filter = new LinkFilter(){ //提取以 http://www.twt.edu.cn 开头的链接 public boolean accept(String url) { if(url.startsWith("http://www.twt.edu.cn")) return true; else return false; } }; //初始化 URL 队列 initCrawlerWithSeeds(seeds); //循环条件:待抓取的链接不空且抓取的网页不多于 1000 while(!LinkDB.unVisitedUrlsEmpty()&&LinkDB.getVisitedUrlNum()<=1000) { //队头 URL 出对 String visitUrl=LinkDB.unVisitedUrlDeQueue(); if(visitUrl==null) continue; FileDownLoader downLoader=new FileDownLoader(); //下载网页 downLoader.downloadFile(visitUrl); //该 url 放入到已访问的 URL 中 LinkDB.addVisitedUrl(visitUrl); //提取出下载网页中的 URL Set<String> links=HtmlParserTool.extracLinks(visitUrl,filter); //新的未访问的 URL 入队 for(String link:links) { LinkDB.addUnvisitedUrl(link); } } } //main 方法入口 public static void main(String[]args) { Crawler crawler = new Crawler(); crawler.crawling(new String[]{"http://www.twt.edu.cn"}); } }
清单7 LinkDb.java
package com.ie; import java.util.HashSet; import java.util.Set; /** * 用来保存已经访问过 Url 和待访问的 Url 的类 */ public class LinkDB { //已访问的 url 集合 private static Set<String> visitedUrl = new HashSet<String>(); //待访问的 url 集合 private static Queue<String> unVisitedUrl = new Queue<String>(); public static Queue<String> getUnVisitedUrl() { return unVisitedUrl; } public static void addVisitedUrl(String url) { visitedUrl.add(url); } public static void removeVisitedUrl(String url) { visitedUrl.remove(url); } public static String unVisitedUrlDeQueue() { return unVisitedUrl.deQueue(); } // 保证每个 url 只被访问一次 public static void addUnvisitedUrl(String url) { if (url != null && !url.trim().equals("") && !visitedUrl.contains(url) && !unVisitedUrl.contians(url)) unVisitedUrl.enQueue(url); } public static int getVisitedUrlNum() { return visitedUrl.size(); } public static boolean unVisitedUrlsEmpty() { return unVisitedUrl.empty(); } }
清单8 Queue.java
package com.ie; import java.util.LinkedList; /** * 数据结构队列 */ public class Queue<T> { private LinkedList<T> queue=new LinkedList<T>(); public void enQueue(T t) { queue.addLast(t); } public T deQueue() { return queue.removeFirst(); } public boolean isQueueEmpty() { return queue.isEmpty(); } public boolean contians(T t) { return queue.contains(t); } public boolean empty() { return queue.isEmpty(); } }
清单 9 FileDownLoader.java
package com.ie; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; 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; public class FileDownLoader { /**根据 url 和网页类型生成需要保存的网页的文件名 *去除掉 url 中非文件名字符 */ public String getFileNameByUrl(String url,String contentType) { url=url.substring(7);//remove http:// if(contentType.indexOf("html")!=-1)//text/html { 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 { 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 (IOException e) { e.printStackTrace(); } } /*下载 url 指向的网页*/ public String downloadFile(String url) { String filePath=null; /* 1.生成 HttpClinet 对象并设置参数*/ HttpClient httpClient=new HttpClient(); //设置 Http 连接超时 5s httpClient.getHttpConnectionManager().getParams(). setConnectionTimeout(5000); /*2.生成 GetMethod 对象并设置参数*/ GetMethod getMethod=new GetMethod(url); //设置 get 请求超时 5s getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT,5000); //设置请求重试处理 getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler()); /*3.执行 HTTP GET 请求*/ 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()); saveToLocal(responseBody,filePath); } catch (HttpException e) { // 发生致命的异常,可能是协议不对或者返回的内容有问题 System.out.println("Please check your provided http address!"); e.printStackTrace(); } catch (IOException e) { // 发生网络异常 e.printStackTrace(); } finally { // 释放连接 getMethod.releaseConnection(); } return filePath; } //测试的 main 方法 public static void main(String[]args) { FileDownLoader downLoader = new FileDownLoader(); downLoader.downloadFile("http://www.twt.edu.cn"); } }
清单 10 HtmlParserTool.java
package com.ie; 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.LinkTag; import org.htmlparser.util.NodeList; import org.htmlparser.util.ParserException; 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("gb2312"); // 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接 NodeFilter frameFilter = new NodeFilter() { 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();// url 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; } //测试的 main 方法 public static void main(String[]args) { Set<String> links = HtmlParserTool.extracLinks( "http://www.twt.edu.cn",new LinkFilter() { //提取以 http://www.twt.edu.cn 开头的链接 public boolean accept(String url) { if(url.startsWith("http://www.twt.edu.cn")) return true; else return false; } }); for(String link : links) System.out.println(link); } } 清单11 LinkFilter.java package com.ie; public interface LinkFilter { public boolean accept(String url); }
这些代码中关键的部分都在 HttpClient 和 HtmlParser 介绍中说明过了。
来源:https://www.cnblogs.com/mywy/p/5065136.html