接上节
3 Java 宽度优先爬虫示例
本节使用java实现一个简易的爬虫。其中用到了HttpClient和HtmlParser两个开源工具包。HttpClient的内容之前已经做过详细的阐述。有关HtmlParser的用法,以后会给出详细的介绍。为了便于理解,下面给出示例程序的结构,如下图:
首先,需要定义图中所描述的“URL队列”,这里使用一个LinkedList来实现这个队列。
Queue类
/** *队列,保存将要访问的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(); } //判断队列是否包含t public boolean contians(Object t){ return queue.contains(t); } public boolean empty(){ return queue.isEmpty(); } }
除了URL队列之外,在爬虫过程中,还需要一个数据结构记录已经访问过得URL。每当要访问URL的时候,首先在这个数据结构中进行查找,如果当前的URL已经存在,则丢弃它。这个数据结构要有两个特点:
针对以上两点,我们选择HashSet作为存储结构。
LinkQueue类:
1 public class LinkQueue{ 2 37 }
下面的代码详细说明了网页下载并处理的过程。和第一节讲述的内容相比,它考虑了更多的方面。比如存储网页,设置请求超时策略等。
DownLoadFile类:
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; } //如application/pdf类型 else { 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(IOExceptione){ e.printStackTrace(); } } //下载URL指向的网页 public String downloadFile(String url){ String filePath=null; //1.生成HttpClinet对象并设置参数 HttpClienthttp Client=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.执行HTTPGET请求 try{ int statusCode=httpClient.executeMethod(getMethod); //判断访问的状态码 if(statusCode!=HttpStatus.SC_OK){ System.err.println("Methodfailed:"+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("Pleasecheckyourprovidedhttpaddress!"); e.printStackTrace(); }catch(IOException e){ //发生网络异常 e.printStackTrace(); }finally{ //释放连接 getMethod.releaseConnection(); } return filePath;
}
}
}
接下来,演示如何从获得的网页中提取URL,Java有一个非常实用的开源工具包HtmlParser,它专门针对Html页面进行处理,不仅能提取URL,还能提取文本以及你想要的任何内容。关于它的有关内容,会在后面详细介绍。以下是代码:
HtmlParserTool类:
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(inti=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属性的链接,如<framesrc="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; } }
最后,来看看宽度爬虫的主程序:
MyCrawler类:
public class MyCrawler{ /** *使用种子初始化URL队列 *@return *@paramseeds种子URL */ private void initCrawlerWithSeeds(String[]seeds) { for(int i=0;i<seeds.length;i++) LinkQueue.addUnvisitedUrl(seeds[i]); } /** *抓取过程 *@return *@paramseeds */ public void crawling(String[]seeds) { //定义过滤器,提取以http://www.lietu.com开头的链接 LinkFilter filter=new LinkFilter(){ public boolean accept(Stringurl){ if(url.startsWith("http://www.lietu.com")) return true; else return false; } }; //初始化URL队列 initCrawlerWithSeeds(seeds); //循环条件:待抓取的链接不空且抓取的网页不多于1000 while(!LinkQueue.unVisitedUrlsEmpty()&&LinkQueue.getVisitedUrlNum()<=1000) { //队头URL出队列 String visitUrl=(String)LinkQueue.unVisitedUrlDeQueue(); if(visitUrl==null) continue; DownLoadFile downLoader=new DownLoadFile(); //下载网页 downLoader.downloadFile(visitUrl); //该URL放入已访问的URL中 LinkQueue.addVisitedUrl(visitUrl); //提取出下载网页中的URL Set<String>links=HtmlParserTool.extracLinks(visitUrl,filter); //新的未访问的URL入队 for(Stringlink:links) { LinkQueue.addUnvisitedUrl(link); } } } //main方法入口 public static void main(String[]args) { MyCrawler crawler=new MyCrawler(); crawler.crawling(new String[]{"http://www.lietu.com"}); } }
上面的主程序使用了一个LinkFilter接口,并且实现了一个内部类。这个接口的目的是为了过滤提取出来的URL,它使得程序中提取出来的URL只会和猎图网站相关。而不会提取其他无关的网站,代码如下:
public interface LinkFilter{ public boolean accept(String url); }