宽度优先爬虫和带偏好的爬虫的简单实现
来源:互联网 发布:数据开发工程师 编辑:程序博客网 时间: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(); }}
- 宽度优先爬虫和带偏好的爬虫的简单实现
- 宽度优先爬虫和带偏好的爬虫
- 宽度优先爬虫和带偏好的爬虫
- 一个简单的宽度优先网络爬虫
- 网络爬虫(三)------宽度优先爬虫(一个的基础,简单但是很重要哦)
- c#宽度优先的网络爬虫
- 《自己动手写网络爬虫》笔记4-带偏好的网络爬虫
- 爬虫宽度优先遍历
- 基于宽度优先爬虫
- 宽度优先爬虫
- 宽度优先爬虫
- java爬虫学习日记2-宽度优先爬虫代码实现
- 网络爬虫中用到的宽度优先遍历算法
- [爬虫] 爬虫的实现
- 简单爬虫的实现原理
- java实现的简单爬虫
- 网络爬虫的简单实现
- Python实现简单的爬虫
- 后台将map放入jsonobject中,前台从map中取值
- 欢迎使用CSDN-markdown编辑器
- Java后端WebSocket的Tomcat实现
- CENTOS7使用rpm方式安装mysql5.7.18
- JavaScript函数笔记by-Lying
- 宽度优先爬虫和带偏好的爬虫的简单实现
- windows下使用批处理脚本实现多个版本的JDK切换
- CPP_Basic_Code_P7.1-PP7.13.10
- css li 不换行 没用到,留着
- 启动tomcat时 错误: 代理抛出异常 : java.rmi.server.ExportException: Port already in use: 1099的解决办法
- delphi 下枚举可用串口
- 从图像处理到图像识别
- window.location.Reload()和window.location.href 区别
- 值得推荐的C/C++框架和库