POST获取网易博客数据(网页抓取,模拟登陆资料学习备份)

         下面这个日志网站(http://www.crifan.com/的类别“Category Archives: Crawl_emulatelogin”:

       http://www.crifan.com/category/work_and_job/web/crawl_emulatelogin/

       里有很多网页解析和抓取以及模拟登陆的学习资料,并给出了个博客搬家的工具:BlogsToWordPress,功能很强大,但也因为过于强大,需要很多时间去折腾,我当时主要用到下载网易博客数据的功能。想详细了解可以去根据标题找相关信息。

       因为网易博客(http://blog.163.com)博主日志目录的数据是动态加载的,例如清华大学肖鹰的博客日志目录:

       http://xying1962.blog.163.com/blog/  (通常显示后面还有"#m=0":http://xying1962.blog.163.com/blog/#m=0)

       如图所示:

        直接通过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代码,可以看这篇日志:

记录】用Python解析网易163博客的心情随笔FeelingCard返回的DWR-REPLY数据

其实这篇说得还繁琐的,想看更简洁的,可以看下面这篇:

【记录】给BlogsToWordPress添加支持导出网易的心情随笔

我列出的这三篇日志基本把解析网易博客日志数据如何设置并请求POST说清楚了,里面用的是Python写的。下面呢,是我参考后用Java实现的请求用户博客数据的完整代码。


首先说下,网易博客的目录数据是动态加载的,需要POST请求.dwr,但博客内容是静态的,可以通过GET请求网址就可获取,例如肖鹰的一篇博客:

肖鹰:晚明文人为何发狂?

地址是:

http://xying1962.blog.163.com/blog/static/138445490201310207320529/

我的目的是获得“肖鹰:晚明文人为何发狂”这篇日志的内容,只需要通过一次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请求

http://xying1962.blog.163.com/blog/的返回结果里看到“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软件包:

htmlcleaner

httpclient

可能还需要下面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;
	}
	
}







      

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值