爬虫两种方式--宽度优先和带偏好爬虫
先复习下上次学了什么:
URL和URI的结构组成
根据指定网址爬取网站内容(get方式和post方式)
上一日记中学到了抓取单个页面内容的方法,但实际项目中则需要爬虫遍历互联网,把互联网中相关的页面都抓取回来。那么爬虫是怎样遍历互联网,把页面抓取下来的呢?首先互联网可以开成是一个"图",每个页面可以看作一个节点,链接可以看作是"有向边"。因此能够通过图的方式对互联网这超级大"图"进行遍历。图的遍历通常可分为宽度优先遍历和深度优先遍历这两种方式。
宽度优先遍历
图的宽度优先遍历需要一个队列作为保存当前节点的子节点的数据结构。算法如下:
1) 顶点V入队列
2)当队列非空时继续执行,否则算法为空
3)出队列,获得队头节点V,访问顶点V并标志V已经被访问
4)查找顶点V的第一个邻接顶点col
5)若V的邻接顶点col未被访问,则col进队列
6)继续查找V的其它邻接顶点col,转到步骤5判断,若V的所有邻接顶点都被访问过,则转到步骤2
执行过程如下:
整个宽度优先爬虫过程就是从一系列种子节点开始,把网页中的"子节点"(超链接)提取出来,然后放入队列中一次进行抓取。被处理过的链接需要放到一张表(通常叫Visited表)中。每次新处理一个链接之前,都会判断是否存在于Visited表中。如果存在,证明该链接已处理过,跳过不再处理,否则进入处理流程。过程如下图:
1)解析出来的链接和Visited表中的链接进行比较,若Visited表不存在此链接,表示未被访问过。
2)把链接放入TODO表中
3)处理完毕后,再次从TODO表中取出一条链接,直接放入Visited表中
4)针对这个链接所表示的网页,重复上述流程
接下来我们就看一下怎么用java代码实现整个流程抓取逻辑吧!
import java.util.LinkedList; /** *URL队列类 */ public class Queue { //使用链表实现队列 private LinkedList<Object> queue=new LinkedList<Object>(); //入队列 public void enQueue(Object t){ queue.addLast(t); } //出队列 public Object deQueue(){ return queue.removeFirst(); } //判断队列是否为空 public boolean isQueueEmpty(){ return queue.isEmpty(); } //判断队列是否包含t public boolean contains(Object t){ return queue.contains(t); } public boolean enpty(){ return queue.isEmpty(); } }
import java.util.HashSet; import java.util.Set; /** * 记录哪些URL访问过 * 哪些待访问 * */ 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 unVisitedUrlDeQueue(){ return unVisitedUrl.deQueue(); } /** * 保证每个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 unVisitedUrlIsEmpty(){ return unVisitedUrl.enpty(); } }
import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpMethodParams; /** * 下载并存储网页信息 * */ public class DownLoadFile { //生成HttpClient对象并设置参数 private static HttpClient httpClient = new HttpClient(); //下载url指定网页 public String downloadFile(String url) { //设置HTTP连接超市时间 httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000); //生成GetMethod对象并设置参数 GetMethod getMethod=new GetMethod(url); //设置get请求超时时间 getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000); //设置请求重试处理 getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler()); String filePath=null; //执行Http GET请求 try { int statusCode = httpClient.executeMethod(getMethod); //判断状态码 if(statusCode!=HttpStatus.SC_OK){ filePath=null; } //处理Http响应内容 byte[] responseBody=getMethod.getResponseBody();//读取字节数组 filePath="temp\\"+getFileNameByUrl(url,getMethod.getResponseHeader("Content-Type").getValue()); saveToLocal(responseBody,filePath); } catch (Exception e) { // TODO: handle exception }finally{ getMethod.releaseConnection(); } return filePath; } /** * 保存网页字节数组到本地文件,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 (Exception e) { // TODO: handle exception } } /** * 根据url和网页累生成需要保存的网页的文件名,去除url中非文件名符号 * */ private 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); } } }
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; /** * 提取页面内容 * */ 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(){ @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标签 String frame=tag.getText(); //提取frame里面src属性的链接,如 frame src='test.html' 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 (Exception e) { e.printStackTrace(); } return links; } }
import java.util.Set; /** * 宽度爬取页面主程序 * */ public class MyCrawler { /** * 抓取过程 * */ public void crawling(String [] seeds){ //定义过滤器,提取以http://www.baidu.com开头的链接 LinkFilter filter=new LinkFilter(){ @Override public boolean accept(String url) { if(url.startsWith("https://www.baidu.com")){ return true; }else{ return false; } } }; //初始化url队列 initCrawlerWithSeeds(seeds); //抓取的链接不为空并且数量不多于1000 while(!LinkQueue.unVisitedUrlIsEmpty()&&LinkQueue.getVisitedUrlNum()<=1000){ //队列头URL出队列 String visitUrl=(String)LinkQueue.unVisitedUrlDeQueue(); if(visitUrl==null){ continue; } DownLoadFile downLoadFile=new DownLoadFile(); //下载网页 downLoadFile.downloadFile(visitUrl); //该url放入已访问队列中 LinkQueue.addVisitedUrl(visitUrl); Set<String> links=HtmlParserTool.extracLinks(visitUrl,filter); for(String link:links){ LinkQueue.addUnvisitedUrl(link); } } } /** * 使用种子初始化URL队列 * @param seeds 种子url * @return * */ private void initCrawlerWithSeeds(String[] seeds) { for(int i=0;i<seeds.length;i++){ LinkQueue.addUnvisitedUrl(seeds[i]); } } }
/** * 过滤提取出来的url,使得您爬取出来的url只会与你所需要的页面相关 * 这里例子只爬取以https://www.baidu.com/开头的内容 * */ public interface LinkFilter { public boolean accept(String url); }
public class SpiderWidth { public static void main(String[] args) { MyCrawler myCrawler=new MyCrawler(); //网页列表 myCrawler.crawling(new String[]{"https://www.baidu.com"}); } }
以上程序本人以验证过,需要验证的可以将百度换成自己的网站,有什么以为可以多留言交流
深度优先方式敬请期待下节。。。