1. 程式人生 > >POST獲取網易部落格資料(網頁抓取,模擬登陸資料學習備份)

POST獲取網易部落格資料(網頁抓取,模擬登陸資料學習備份)

         下面這個日誌網站(http://www.crifan.com/的類別“Category Archives: Crawl_emulatelogin”:

       裡有很多網頁解析和抓取以及模擬登陸的學習資料,並給出了個部落格搬家的工具:BlogsToWordPress,功能很強大,但也因為過於強大,需要很多時間去折騰,我當時主要用到下載網易部落格資料的功能。想詳細瞭解可以去根據標題找相關資訊。

       因為網易部落格(http://blog.163.com)博主日誌目錄的資料是動態載入的,例如清華大學肖鷹的部落格日誌目錄:

       如圖所示:

       直接通過HttpClient一次請求“

http://xying1962.blog.163.com/blog/”是得不到部落格的資料的(如圖紅色方框所示),而是需要另外一次POST請求

"http://api.blog.163.com/xying1962/dwr/call/plaincall/

BlogBeanNew.getBlogs.dwr",下面這篇日誌就是分析如何去POST請求網易的".dwr"資料:

           該日誌是分析抓取網易部落格讀者資訊的,請求的是:VisitBeanNew.getBlogReaders.dwr,抓取部落格內容則請求:BlogBeanNew.getBlogs.dwr,都是通過POST請求,原理是類似,設定基本一樣。

看完了分析,就該看程式碼了,有興趣的可以去看整個BlogsToWordPress工具的Python程式碼,如果想只看POST程式碼,可以看這篇日誌:

其實這篇說得還繁瑣的,想看更簡潔的,可以看下面這篇:

我列出的這三篇日誌基本把解析網易部落格日誌資料如何設定並請求POST說清楚了,裡面用的是Python寫的。下面呢,是我參考後用Java實現的請求使用者部落格資料的完整程式碼。

首先說下,網易部落格的目錄資料是動態載入的,需要POST請求.dwr,但部落格內容是靜態的,可以通過GET請求網址就可獲取,例如肖鷹的一篇部落格:

地址是:

我的目的是獲得“肖鷹:晚明文人為何發狂”這篇日誌的內容,只需要通過一次GET請求它的地址就可以獲取,然後這個地址又是比較格式化的,例如只要解析出了最後這串數字“

138445490201310207320529”就可以拼接出完整地址,整個地址格式是:

http://[userName].blog.163.com/blog/static/[blogId]

肖鷹部落格的username:“xying1962”是可以通過入口地址“http://xying1962.blog.163.com/blog/”獲取的,後面的blogId就需要解析目錄資料才能獲取了,所以才需要POST請求.dwr。

另外,說明下網易部落格地址,地址格式有兩種(具體到部落格目錄地址):

1. http://[username].blog.163.com/blog/

2. http://blog.163.com/[username]/blog/

在給出Java程式碼前,我得說下,Google的Chrome瀏覽器真是好產品,連請求監測也做得那麼好,是網頁分析的好幫手,個人覺得比Wireshark好用,詳細使用如下:

1、右鍵單擊網頁某處,選擇最末項的“Inspect Element”,好像中文叫“審查元素”,如圖:


出來了“Inspect element”審查元素框後,點選“Network”,中文版應該是“網路”,並重新整理網頁,就可以看到網頁監測情況,如下圖所示:

可以檢視HTTP請求的名字(name),請求的方式(Method),請求的狀態(Status)和請求的返回結果型別(Type)。單擊最左側的Name,就可以檢視詳細的資訊,例如單擊“blog/”,圖示如下:

可以檢視Headers資訊,返回的結果“Response”以及Cookies,有時候模擬登陸進行網頁請求需要用到Cookies,但很多時候Headers和Response就夠用了,如果想清楚當前的資訊,重新檢視,點選底部的“Clear”按鈕(如圖,紅色方框圈出)就可以了。具體怎麼使用,如果學過計算機網路,做過抓包分析,自己檢視一下就都明白了。如果沒有,還真需要花點時間瞭解下。

下面就說明如何在Java裡設定POST請求,先按照類似原文Python格式上Java程式碼

public Set<String> post163Blog(String username, String userId, int startIndex, int returnNumber){
		/**
		* entityBody用於儲存字串格式的返回結果
		*/
		String entityBody = null;
		/**
		* 例項化一個HttpPost,並設定請求dwr地址,username表示博主的使用者名稱,例如肖鷹的username是“xying1962”
		*/
		HttpPost httppost = new HttpPost("http://api.blog.163.com/" + username + "/dwr/call/plaincall/BlogBeanNew.getBlogs.dwr");
		
		/*
		* 設定引數,除了c0-param0、c0-param1和c0-param2外都一樣。
		* c0-param0 :博主的userId,例如肖鷹的userId是“138445490”
		* c0-param1 :返回部落格資料的起始項,從0開始
		* c0-param2 :一次返回部落格的數量,最大值好像是500,具體多少我沒有完全去試,600肯定不行,我一般設定500,600以上就不返回資料了。
		* 如果一個博主寫了超過500篇部落格,那就可以分多次請求,只要合理設定c0-param1和c0-param2就可以。
		*/
		List<NameValuePair> nvp = new ArrayList<NameValuePair>();
		nvp.add(new BasicNameValuePair("callCount", "1"));
		nvp.add(new BasicNameValuePair("scriptSessionId", "${scriptSessionId}187"));
		nvp.add(new BasicNameValuePair("c0-scriptName", "BlogBeanNew"));
		nvp.add(new BasicNameValuePair("c0-methodName", "getBlogs"));
		nvp.add(new BasicNameValuePair("c0-id", "0"));
		nvp.add(new BasicNameValuePair("c0-param0", "number:" + userId));
		nvp.add(new BasicNameValuePair("c0-param1", "number:" + startIndex));
		nvp.add(new BasicNameValuePair("c0-param2", "number:" + (returnNumber <= 500 ? returnNumber : 500)));
		nvp.add(new BasicNameValuePair("batchId", "1"));
		
		try{
			httppost.setEntity(new UrlEncodedFormEntity(nvp, "UTF8"));
			httppost.addHeader("Referer", "http://api.blog.163.com/crossdomain.html?t=20100205");
			httppost.addHeader("Content-Type", "text/plain");
			//httppost.addHeader("User-Agent", "Mozilla/5.0 Firefox/3.5.9 Chrome/26.0.1410.64");
			
			HttpResponse response = httpclient.execute(httppost);
			
			HttpEntity entity = response.getEntity();
			if(entity != null){
				/**
				* 把返回結果轉換成字串的形式,這裡編碼設定其實無所謂,因為我只需要解析出blogId,而且POST請求返回的是unicode,還需要轉碼,我嫌麻煩就沒有去弄,也沒必要去弄。
				*/
				entityBody = EntityUtils.toString(entity, "UTF8");
			}
		} catch (Exception e){
			e.printStackTrace();
		} finally {
			/**
			* 請求結束,關閉httppost,釋放空間,注意,一定要在獲取返回結果(response.getEntity())之後再釋放,因為一旦關閉了httppost,
			* response也就關閉了,把返回結果也釋放了。
			*/
			httppost.abort();
		}		
		
		/**
		* blogIdSet用來儲存blogId,POST請求返回結果裡,blogId以三種形式出現:
		* 1. permalink="blo/static/[blogId]"
		* 2. trackbackUrl="blog/[blogId].track"
		* 3. permaSerial="[blogId]"
		* 其中第三種的permaSerial=後面肯定是緊跟blogId的,用這種方式可以解析得到純淨的blogId,而且進一步提取blogId也比較簡單,其他兩種具體我沒有去試,
		* 但應該也是可以得到純淨的blogId,有興趣的可以把entityBody值打印出來自己去看看,下面是解析POST請求返回結果提取blogId,使用HashSet的一個好處是
		* 可以不用每次都判斷blogId是否已經出現,可以少些幾行程式碼,不要用ArrayList,因為每個blogId的permaSerial="[blogId]"形式會出現兩次,如果需要提取
		* 其他資訊諸如標題可以考慮用HashMap<String, InfoStruct>(HashMap<blogId, 資料資訊>)
		*/
		Set<String> blogIdSet = new HashSet<String>();
		
		/**
		* 設定匹配的正則表示式,其中\"[0-9]+?\"中的問號"?"是最小匹配的意思,如果不用?,就可能得不到純淨的blogId。
		*/
		Pattern pattern = Pattern.compile("permaSerial=\"[0-9]+?\"");
		
		/**
		* 先對返回結果進行分句,再對每一句進行匹配,其實也可以不用分句,直接匹配,只是個人習慣先分句而已,防止跨句。
		*/
		String[] sents = entityBody.split("(\n|\r\n)+");
		for(int i = 0; i < sents.length; i++){
			Matcher matcher = pattern.matcher(sents[i]);
			while(matcher.find()){
				blogIdSet.add(matcher.group().replaceAll("permaSerial=|\"", ""));
			}
		}
		return blogIdSet;
	}

獲取了blogId後就可以拼接部落格地址並請求部落格內容資料了。【哎,我得感慨下,為了寫這篇日誌,還把英文註釋改成了中文註釋,並添加了很多新的註釋】

post163Blog(String username, String userId, int startIndex, int returnNumber)中的引數裡,startIndex和returnNumber可以根據需要設定,而username,userId是傳進去的,但給定一個部落格入口地址,我們只能從入口地址獲取username,userId是沒有的,這就需要另外去解析提取userId了。

userId可以在一次GET請求部落格入口地址的返回結果裡找到。例如在肖鷹例子裡,GET請求

的返回結果裡看到“userId:138445490”,如下圖所示(可以用上面的網頁分析神器Chrome檢視,在Response裡):


這個userId資訊是儲存在<script>...</script>裡的,可以使用HtmlCleaner進行解析或者直接用字串正則匹配就可以提取出來,例如上述post163Blog函式裡提取blogId用到的正則匹配。正則表示式模板是:

Pattern pattern = Pattern.compile("userId:[0-9]+");
      我這裡也給出根據GET請求部落格目錄地址並解析返回結果獲取userId的程式碼,以供參考。
/**
	 * Get the html text through a GET request, the default encoding is "UTF8"
	 * */
	public String getText(String inputUrl){
		return getText(inputUrl, "UTF8");
	}
	public String getText(String inputUrl, String encoding){
		/**
		* 例項化一個新的HttpGet,並新增Header
		*/
		HttpGet httpget = new HttpGet();
		httpget.addHeader("User-Agent", "Mozilla/5.0 Firefox/3.5.9 Chrome/26.0.1410.64");
		String entityBody = null;
		try{
			/**
			* 設定要請求的頁面地址
			*/
			httpget.setURI(new URI(inputUrl));
			HttpResponse response = httpclient.execute(httpget);
			/**
			* 獲取返回結果並轉換成字串形式
			*/	
			HttpEntity entity = response.getEntity();
			if(entity != null){
				entityBody = EntityUtils.toString(entity, encoding);
			}
			/**
			* 關閉httpget,釋放資源,及時釋放資源是個好習慣。
			*/
			httpget.abort();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {}
		/**
		* 返回請求的返回結果,entityBody一般是個html頁面的原始碼,也可能不是,看對方網站伺服器以什麼形式返回結果。
		*/
		return entityBody;		
	}
/**
	 * 解析GET請求部落格目錄返回結果,獲取博主的userId,userId是博主的唯一標識。
	 * userId隱藏在script程式碼裡。這裡會用到工具包HtmlCleaner。
	 * 這個程式碼做的檢查是過於小心了,因為我沒有詳細去分析返回結果是否包含其他人的userId,
	 * 但我的檢查可以保證提取出來的是博主正確的userId
	 * */
	public String parseReturnHtml(String htmlText){
		if(htmlText == null)
			return null;
		TagNode rootNode = htmlcleaner.clean(htmlText);
		try {
			/**
			* 提取<script>...</script>內容,從後往前是因為看userId藏在較低端的script程式碼裡。
			*/
			Object[] scriptNodes = rootNode.evaluateXPath("//script");
			for(int i = scriptNodes.length - 1; i >= 0; i--){
				TagNode scriptNode = (TagNode) scriptNodes[i];
				String text = scriptNode.getText().toString().trim();
				
				if(! text.startsWith("window.N"))
					continue;
				if(! text.contains("userId"))
					continue;
				/**
				* 分句
				*/
				String[] sents = text.split("\n|\r\n");
				for(int j = sents.length - 1; j >= 0; j--){
					if(! sents[j].contains("userId"))
						continue;
					sents[j] = sents[j].trim();
					
					String[] items = sents[j].split(":");
					if(items.length != 2)
						return null;
					String userId = items[1];
					/**
					 * userId是一個數字串
					 * */
					return userId;
				}
				break;
			}
		} catch (XPatherException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {}
		return null;
	}
其中,getText函式是對網頁進行GET請求,獲得返回結果,這個函式是通用的。parseReturnHtml只是解析GET請求網易部落格目錄的返回結果而已。

這就是獲取網易部落格資料的關鍵程式碼了。

下面給出完整可執行程式碼,需要去下載兩個jar軟體包:

可能還需要下面httpcore這個jar軟體包,如果用上面兩個還不夠,就把這個也加上。【注,貌似httpclient和httpcore是一塊放在httpcomponents的,我記不得了,自己看看就清楚了】

import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.XPatherException;

public class WangyiBlogCrawler {
	
	/**
	 * For http request and html cleaning and parsing
	 * */
	private HttpClient httpclient;
	private HtmlCleaner htmlcleaner;
	
	private int STARTINDEX;
	private int RETURNNUMBER;
	
	public WangyiBlogCrawler(){
		httpclient = new DefaultHttpClient();
		htmlcleaner = new HtmlCleaner();
		
		STARTINDEX = 0;
		RETURNNUMBER = 100;
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		String contentUrl = "http://xying1962.blog.163.com/blog/";
		WangyiBlogCrawler wyBlogCrawler = new WangyiBlogCrawler();
		
		wyBlogCrawler.run(contentUrl);

	}
	
	public void run(String contentUrl){
		
		String username = contentUrl.replaceAll("http://|.?blog.163.com/?|/?blog/|#m=0", "");;
		String returnEntity = getText(contentUrl);
		String userId = parseReturnHtml(returnEntity);
		
		int startIndex = STARTINDEX;
		int returnNumber = RETURNNUMBER;
		
		Set<String> blogIdSet = new HashSet<String>();
		
		Set<String> temIdSet = null;
		do{
			startIndex += returnNumber;
			returnNumber = RETURNNUMBER;
			temIdSet = post163Blog(username, userId, startIndex, returnNumber);
			blogIdSet.addAll(temIdSet);
		}while(temIdSet.size() == returnNumber);
		
		processBlogIdSet(contentUrl, blogIdSet);
		
	}
	
	public void processBlogIdSet(String contentUrl, Set<String> blogIdSet){
		contentUrl = contentUrl.replaceAll("#m=0", "");
		
		for(Iterator<String> iter = blogIdSet.iterator(); iter.hasNext(); ){
			String blogId = iter.next();
			
			
			/**
			* 拼接產生部落格內容的地址
			*/
			String blogUrl = contentUrl + "static/" + blogId + "/";
			
			/**
			 * output the blog url
			 * */
			System.out.println(blogUrl);
			
			/**
			 * output the blog entity
			 * */
			 /**
			 * 下面兩行程式碼請求每一篇部落格內容並打印出完整的html文字
			 *
			//String blogEntity = getText(blogUrl, "gbk");
			//System.out.println(blogEntity);
		}
		
	}
	
	/**
	 * Parsing the entry html in order to extract the unique userId.
	 * The unique userId is hidden in the script codes.
	 * */
	public String parseReturnHtml(String htmlText){
		if(htmlText == null)
			return null;
		TagNode rootNode = htmlcleaner.clean(htmlText);
		try {
			Object[] scriptNodes = rootNode.evaluateXPath("//script");
			for(int i = scriptNodes.length - 1; i >= 0; i--){
				TagNode scriptNode = (TagNode) scriptNodes[i];
				String text = scriptNode.getText().toString().trim();
				
				if(! text.startsWith("window.N"))
					continue;
				if(! text.contains("userId"))
					continue;
				
				String[] sents = text.split("\n|\r\n");
				for(int j = sents.length - 1; j >= 0; j--){
					if(! sents[j].contains("userId"))
						continue;
					sents[j] = sents[j].trim();
					
					String[] items = sents[j].split(":");
					if(items.length != 2)
						return null;
					String userId = items[1];
					/**
					 * the userId is a sequence numbers.
					 * */
					return userId;
				}
				break;
			}
		} catch (XPatherException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {}
		return null;
	}
	
	public Set<String> post163Blog(String username, String userId, int startIndex, int returnNumber){
		
		String entityBody = null;
		
		HttpPost httppost = new HttpPost("http://api.blog.163.com/" + username + "/dwr/call/plaincall/BlogBeanNew.getBlogs.dwr");
		
		List<NameValuePair> nvp = new ArrayList<NameValuePair>();
		nvp.add(new BasicNameValuePair("callCount", "1"));
		nvp.add(new BasicNameValuePair("scriptSessionId", "${scriptSessionId}187"));
		nvp.add(new BasicNameValuePair("c0-scriptName", "BlogBeanNew"));
		nvp.add(new BasicNameValuePair("c0-methodName", "getBlogs"));
		nvp.add(new BasicNameValuePair("c0-id", "0"));
		nvp.add(new BasicNameValuePair("c0-param0", "number:" + userId));
		nvp.add(new BasicNameValuePair("c0-param1", "number:" + startIndex));
		nvp.add(new BasicNameValuePair("c0-param2", "number:" + (returnNumber <= 500 ? returnNumber : 500)));
		nvp.add(new BasicNameValuePair("batchId", "1"));
		
		try{
			httppost.setEntity(new UrlEncodedFormEntity(nvp, "UTF8"));
			httppost.addHeader("Referer", "http://api.blog.163.com/crossdomain.html?t=20100205");
			httppost.addHeader("Content-Type", "text/plain");
			httppost.addHeader("User-Agent", "Mozilla/5.0 Firefox/3.5.9 Chrome/26.0.1410.64");
			
			HttpResponse response = httpclient.execute(httppost);
			
			HttpEntity entity = response.getEntity();
			if(entity != null){
				entityBody = EntityUtils.toString(entity, "UTF8");
			}
		} catch (Exception e){
			e.printStackTrace();
		} finally {
			httppost.abort();
		}		
		
		Set<String> blogIdSet = new HashSet<String>();
		
		Pattern pattern = Pattern.compile("permaSerial=\"[0-9]+?\"");
		String[] sents = entityBody.split("(\n|\r\n)+");
		for(int i = 0; i < sents.length; i++){
			Matcher matcher = pattern.matcher(sents[i]);
			while(matcher.find()){
				blogIdSet.add(matcher.group().replaceAll("permaSerial=|\"", ""));
			}
		}
		return blogIdSet;
	}
	
	/**
	 * Get the html text through a GET request
	 * */
	public String getText(String inputUrl){
		return getText(inputUrl, "UTF8");
	}
	public String getText(String inputUrl, String encoding){
		
		HttpGet httpget = new HttpGet();
		httpget.addHeader("User-Agent", "Mozilla/5.0 Firefox/3.5.9 Chrome/26.0.1410.64");
		String entityBody = null;
		try{
			httpget.setURI(new URI(inputUrl));
			HttpResponse response = httpclient.execute(httpget);
			HttpEntity entity = response.getEntity();
			if(entity != null){
				/**
				 * If you want extract the charset automatically, unannotated the following
				 * the statements
				 * getMeta函式和getCharset函式是用於自動獲取編碼的,在getText裡呼叫,在抓取具體部落格內容時可能或產生亂碼,
				 * 即EntityUtils.toString(entity, encoding)這條語句執行過程中可能會出現亂碼,因此在不知道編碼方式的時候
				 * 可以使用下面的語句自動獲取,屬於兩次解析,第一次是用getCharset獲取,使用html的標籤結果來提取,即一般
				 * 的網頁都有<head>裡都有這條語句,
				 * <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
				 * 但用解析器解析有時候得不到charset,或者有些網頁就不是這種形式,而是很簡單的
				 * <meta charset="utf-8">
				 * 這就需要用自己用字串處理的方式去提取,這樣一般都能解析到,但是先把response返回的結果轉換成字串,
				 * 而response貌似只能儲存一次,因而用字串提取charset又需要一次GET請求,代價比較高,因此我才想這種笨重
				 * 的多次解析多次請求,為的是解決亂碼問題。如果是抓同一個網站的東西,可以直接設好編碼方式。
				 */
				/**
				String charset = getCharset(entity);
				if(charset == null){
					entityBody = EntityUtils.toString(entity);
					charset = getMeta(entityBody);
					 
					response = httpclient.execute(httpget);
					entity = response.getEntity();
				}
				if(charset != null)
					encoding = charset;
				*/
				entityBody = EntityUtils.toString(entity, encoding);
			}
			httpget.abort();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {}
		return entityBody;		
	}
	
	public String getMeta(String htmlEntity){
		String charset = null;
		if(htmlEntity == null)
			return charset;
		Pattern pattern = Pattern.compile("charset=\"?.*?\"");
		String[] lines = htmlEntity.split("(\n|\r\n)+");
		for(int i = 0; i < lines.length; i++){
			Matcher matcher = pattern.matcher(lines[i]);
			if(matcher.find()){
				String[] items = matcher.group().split("=");
				charset = items[1].replaceAll("\"", "");
				break;
			}
		}
		return charset;
	}
	
	public String getCharset(HttpEntity entity){
		String charset = null;
		if(entity == null)
			return charset;
		if(entity.getContentType() != null){
			HeaderElement[] values = entity.getContentType().getElements();
			if(values != null && values.length > 0){
				for(HeaderElement value : values){
					NameValuePair param = value.getParameterByName("charset");
					if(param != null){
						charset = param.getValue();
						break;
					}
				}
			}
		}
		return charset;
	}
	
}