最近呢,由于实习需要呢,复习一遍爬虫,前断时间闭关刷题去了,也会把刷题心得总结成博客分享给大家,比如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的浏览器。无头,即无界面。
(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;
嗯,1.1版本更持久一点- -,
构造完就调用flush扔出去了
然后输入流就不多说了,基本套路。
这样就实现了socket抓取网页= =。