1. 程式人生 > >總結一下五種實現網路爬蟲的方法(一,基於socket通訊編寫爬蟲)

總結一下五種實現網路爬蟲的方法(一,基於socket通訊編寫爬蟲)

最近呢,由於實習需要呢,複習一遍爬蟲,前斷時間閉關刷題去了,也會把刷題心得總結成部落格分享給大家,比如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;

http 1.1中預設啟用Keep-Alive,如果加入"Connection: close ",才關閉。

嗯,1.1版本更持久一點- -,

構造完就呼叫flush扔出去了

然後輸入流就不多說了,基本套路。

這樣就實現了socket抓取網頁= =。