最近呢,由于实习需要呢,复习一遍爬虫,前断时间闭关刷题去了,也会把刷题心得总结成博客分享给大家,比如java集合类特性及源码解析,操作系统数据结构的一些算法,设计模式等,放心,肯定不会鸽的,虽然可能会晚一点写。
言归正传,java实现网络爬虫一般有五种方法(据我所知,要是有其他方法的同学欢迎分享)
1.基于socket通信编写爬虫:最底层的方式,同时也是执行最高效的,不过开发效率最低。
2.基于HttpURLConnection类编写爬虫:java se的net包的核心类,主要用于http的相关操作。
3.基于apache的HttpClient包编写爬虫:由net包拓展而来,专为java网络通信编程而服务。
4.基于phantomjs之类的无头(无界面)浏览器:
(1)它是浏览器的核心,并非浏览器。换言之,它是没有UI的浏览器。
(2)它提供的js api,故它可以方便直接的被各种程序语言调用。换言之,似乎是js写的。
5.基于Selenium或者是WebDriver之类的有头(有界面)浏览器
(1)它是浏览器核心,并非浏览器。换言之,它是没有界面UI的浏览器。无头,即无界面。
![](/qrcode.jpg)
(2)它提供的js api,故它可以方便直接的被各种程序语言调用。
其实第五个呢,我也不是很熟悉,到时候写第五篇的时候呢,会一边学一边写,可能会有比较幼稚的错误,欢迎大家指点哈。
这部分呢。借鉴了周光亮老师的视频上的部分讲解。
首先,这篇介绍的是socket编程编写爬虫,当然,一般在程序开发的时候我们一般不会用这种方式,毕竟httpclient几行代码的事情,但是基于这种方式使其他的方式更易于理解,了解一下还是比较必要的。
socket并不是什么通信协义,只是为了方便tcp/ip层的上层访问tcp/ip层而做一层封装。即应用层以下负责ip地址间的传输,而应用层应用socket实现端对端的传输,即精确到 IP:端口。
首先呢,因为之后要写出很多的类,所以提前规划一下包结构是比较好的,如下:
com.lzx.simple.control这个包是用来给用户控制行为的包
com.lzx.simple.enumeration 这个包集合了一些枚举类型,如TaskLevel用来控制优先级。
package com.lzx.simple.enumeration; /** * 抓取任务的优先级等级 * @author Administrator * */ public enum TaskLevel { HIGH,MIDDLES,LOW }
com.lzx.simple.iface.crawl这个包集合了一些接口,毕竟我们需要面向接口来编程,什么是面向接口编程?去拿本设计模式的书出去罚站
package com.lzx.simple.iface.crawl; import com.lzx.simple.pojos.CrawlResultPojo; import com.lzx.simple.pojos.UrlPojo; public interface ICrawler { public CrawlResultPojo crawl(UrlPojo urlPojo); }
com.lzx.simple.imple.crawl这个包集合了一些接口的实现类,比如这次的SocktCrawlerImpl类实现了ICrawler接口,这个类的代码不放了,待会儿单独解释。
com.lzx.simple.manager这个包集合了一些管理方法
com.lzx.simple.pojos这个包集合了一些简单对象,例如UrlPojo类
package com.lzx.simple.pojos; import java.net.MalformedURLException; import java.net.URL; import com.lzx.simple.enumeration.TaskLevel; /** * 简单的Java对象(Plain Ordinary Java Objects) * @author Administrator * */ public class UrlPojo { private String url; private TaskLevel taskLevel; public String getHost(){ try { URL url = new URL(this.url); return url.getHost(); } catch (MalformedURLException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } return null; } public UrlPojo(String url) { this.url = url; } public UrlPojo(String url, TaskLevel taskLevel) { this.url = url; this.taskLevel = taskLevel; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public TaskLevel getTaskLevel() { return taskLevel; } public void setTaskLevel(TaskLevel taskLevel) { this.taskLevel = taskLevel; } }
我们可以看到UrlPojo中有url属性(网址)和taskLevel(优先级),除了getset还有gethost函数,这个类封装 了一个需抓取的网页,还有另一个类如下:
package com.lzx.simple.pojos; public class CrawlResultPojo { private boolean isSuccess; private String pageContent; private int httpStatuCode; public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean isSuccess) { this.isSuccess = isSuccess; } public String getPageContent() { return pageContent; } public void setPageContent(String pageContent) { this.pageContent = pageContent; } public int getHttpStatuCode() { return httpStatuCode; } public void setHttpStatuCode(int httpStatuCode) { this.httpStatuCode = httpStatuCode; } }我们用CrawlResultPojo这个类来表示抓取结果,三个属性都很显而易见,抓取是否成功,网页内容,状态码。还有相应getset。
有些package还没有文件,之后用到会添加并说明。
然后开始我们的socket通信的类:
package com.lzx.simple.imple.crawl; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; import javax.swing.text.AbstractDocument.BranchElement; import com.lzx.simple.iface.crawl.ICrawler; import com.lzx.simple.pojos.CrawlResultPojo; import com.lzx.simple.pojos.UrlPojo; public class SocketCrawlerImpl implements ICrawler{ public CrawlResultPojo crawl(UrlPojo urlPojo) { CrawlResultPojo crawlResultPojo=new CrawlResultPojo(); if (urlPojo==null||urlPojo.getUrl()==null ){ crawlResultPojo.setSuccess(false); crawlResultPojo.setPageContent(null); return crawlResultPojo; } String host=urlPojo.getHost(); if (host==null) { crawlResultPojo.setSuccess(false); crawlResultPojo.setPageContent(null); return crawlResultPojo; } BufferedWriter bufferedWriter=null; BufferedReader bufferedReader=null; try { Socket socket=new Socket(host, 80); //socket.setKeepAlive(false); bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("GET "+urlPojo.getUrl()+" HTTP/1.1\r\n"); bufferedWriter.write("HOST:"+host+"\r\n"); bufferedWriter.write("Connection:close"+"\r\n"); bufferedWriter.write("\r\n");//提示http header结束 bufferedWriter.flush();//flush()表示强制将缓冲区中的数据发送出去,不必等到缓冲区满. bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream())); String line; StringBuilder stringBuilder=new StringBuilder(); while((line=bufferedReader.readLine())!=null){ stringBuilder.append(line+"\n"); } crawlResultPojo.setSuccess(true); crawlResultPojo.setPageContent(stringBuilder.toString()); return crawlResultPojo; } catch (UnknownHostException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); }finally { try { if (bufferedReader!=null) { bufferedReader.close(); } if (bufferedWriter!=null) { bufferedWriter.close(); } } catch (Exception e2) { // TODO: handle exception e2.printStackTrace(); } } return null; } public static void main(String[] args) { SocketCrawlerImpl socketCrawlerImpl=new SocketCrawlerImpl(); UrlPojo urlPojo=new UrlPojo("http://www.baidu.com"); CrawlResultPojo crawlResultPojo=socketCrawlerImpl.crawl(urlPojo); System.out.println(crawlResultPojo.getPageContent()); } }
这个是整个类的源码
主要的抓取工作就在crawl这个函数中,因为我们要返回CrawlResultPojo对象,所以一开始new了一个CrawlResultPojo;
然后判断待抓取的网页对象urlPojo是不是空,以及它的属性url是不是空。如果是就构造个返回失败的CrawlResultPojo返回回去。
然后gethost获得待抓取对象的主机名,用于待会儿构造http header。当然也要判断一下首部是否为空了。
然后构造缓冲输入流和输出流bufferedWriter和bufferedReader;
然后构造Socket,构造函数的参数这里是主机名+端口,端口默认是80,如果有特殊需要再改动;当然构造函数的参数也可以是IP地址+端口,端对端通信的本质。
然后在bufferedWriter构造http首部以便发送。
这里就有一个比较有趣事情了,周光亮老师在视频的讲解中发现了一个bug,他发现使用http/1.1的协议会导致抓取有一段真空期,即程序输出完这个页面的字符串后,会停顿一段时间然后再结束程序,而改用http/1.0的协议就不会,抓取完直接结束程序,周老师当时调试了很久都没有解决。
其实主要原因呢,是他在构造http首部的时候没有加
bufferedWriter.write("Connection:close"+"\r\n");
这段代码,为什么加了这段代码后就可以去除真空期呢?
因为,
http 1.0中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;
http 1.1中默认启用Keep-Alive,如果加入"Connection: close ",才关闭。
嗯,1.1版本更持久一点- -,
构造完就调用flush扔出去了
然后输入流就不多说了,基本套路。
这样就实现了socket抓取网页= =。