通用文章采集器的设计与实现

2010-05-12, Posted in 人生百味 | 5 回复

最近头脑有点发热,想做一个采集器来采集文章,然后做一个类似垃圾站的东西,当然,使用的文章发布程序还是用wordpress,wordpress上的采集器不太多,他们大多都是通过rss采集,这也不太合适我,因为如果做为文章采集,采集的对象可能有文章内分页,所以最终还是自己写一个程序吧,自己最熟悉的还是java,那就用java写吧。

采集一堆文章,主要有如下步骤:
第一:采集规则定义
第二:按指定的规则进行文章采集

根据以上步骤,我们分开来实现。采集规则的定义实际就是你要做什么,怎么做的定义,我们这里很明确,就是指定一个列表的url地址,提取列表中所有文章的url,然后再一一采集各文章中的标题、内容,如果每篇文章有内分页,还要分别提取。所以,我们首先要做的,就是给定一个url地址,取得对应的html源码。下面是一个实现,它模拟浏览器,向url发出http请求,服务器返回的结果,就是我们需要得到的东西。

/**
	 * 给定一个列表url,得到本url对象的html,然后其它操作在这个基础上做
	 * @param url 
	 * @param encoding   返回html的编码方式,如GBK,UTF─8
	 * @return
	 * @throws Exception
	 */
	public String getHtml(String url, String encoding) throws Exception {
		String value = null;
		HttpClient httpclient = new DefaultHttpClient();        
        HttpGet httpget = new HttpGet(url);
 
        // 以下这条如果不加会发现无论你设置Accept-Charset为gbk还是utf-8,他都会默认返回gb2312(本例针对google.cn来说)
        httpget.setHeader("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.2)");
        // 用逗号分隔显示可以同时接受多种编码
        httpget.setHeader("Accept-Language", "zh-cn,zh;q=0.5");
        httpget.setHeader("Accept-Charset", "GB2312,utf-8;q=0.7,*;q=0.7");        
        HttpResponse response = httpclient.execute(httpget);     
 
        // 判断页面返回状态判断是否进行转向抓取新链接
        int statusCode = response.getStatusLine().getStatusCode();
        if ((statusCode == HttpStatus.SC_MOVED_PERMANENTLY) ||
                (statusCode == HttpStatus.SC_MOVED_TEMPORARILY) ||
                (statusCode == HttpStatus.SC_SEE_OTHER) ||
                (statusCode == HttpStatus.SC_TEMPORARY_REDIRECT)) {
            // 此处重定向处理  此处还未验证
            String newUri = response.getLastHeader("Location").getValue();
            httpclient = new DefaultHttpClient();
            httpget = new HttpGet(newUri);
            response = httpclient.execute(httpget);
        }
 
        // Get hold of the response entity
        HttpEntity entity = response.getEntity();        
        // If the response does not enclose an entity, there is no need
        // to bother about connection release
        if (entity != null) {
            // 将源码流保存在一个byte数组当中,因为可能需要两次用到该流,
            byte[] bytes = EntityUtils.toByteArray(entity);
            // 如果头部Content-Type中包含了编码信息,那么我们可以直接在此处获取
 
            if(encoding != null && !"".equals(encoding.trim())){
            	value = new String(bytes, encoding);
            }else{
            	String charSet = EntityUtils.getContentCharSet(entity);
            	// 如果头部中没有,那么我们需要 查看页面源码,这个方法虽然不能说完全正确,因为有些粗糙的网页编码者没有在页面中写头部编码信息
	            if (charSet == null || "".equals(charSet.trim())) {
	                String regEx="(?=<meta).*?(?<=charset=[\\'|\\\"]?)([[a-z]|[A-Z]|[0-9]|-]*)";
	                Pattern p=Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
	                Matcher m=p.matcher(new String(bytes));  // 默认编码转成字符串,因为我们的匹配中无中文,所以串中可能的乱码对我们没有影响
	                boolean result=m.find();
	                if (m.groupCount() == 1) {
	                    charSet = m.group(1);
	                }
	            }
	            value = new String(bytes, charSet);
            }       
        }
 
        httpclient.getConnectionManager().shutdown();   
        return value;
    }

注意,这里用到了Apache的HttpClient项目,由于空间有限,我这里不上传本函数所依赖的包,请大家自己到apache下载。

以淘宝的女人门户为例子,我给定http://info.taobao.com/list/lady/23/30/2330b6ed-16ce-4d0e-b7fa-58a411e1871a_1.php这个url地址,它会返回一个包含文章列表的html。有了这个html,我们接下来就是提取URL,当然,提取前先不着急,因为这个html里面的内容比较多,不能直接提取,那就把它简单化,先得到一个区域吧:

/**
	 * 列表区域识别规则
	 * @param html 原始的html
	 * @param areaRegex 所提取区域的正则表达式
	 * @param area 正则表达式中需要提取的区域,取值是:AREA_LIST,AREA_TITLE等等
	 * @return
	 */
	public String getArea(String html, String areaRegex ,String area) {
		String valueHtml=null;
		String regex = areaRegex.replaceFirst(area, "([\\\\s\\\\S]*?)");
//		System.out.println(regex);
		Pattern pattern = Pattern.compile(regex,Pattern.MULTILINE); 
		Matcher matcher = pattern.matcher(html); 
		while(matcher.find()) {
			valueHtml = matcher.group(1).trim();
			break;
		} 
		return valueHtml;
	}

有了这个域名,我就就可以提取URL列表了:

       //提取给定区域的文章列表地址,
       public List<String> getListUrl(String areaHtml,String regex,String withPrefix){
		return this.getListUrl(areaHtml, regex, withPrefix, false, null);
	}
 
	/**
	 * 提取给定区域的文章列表地址,主要用来提取文章内的分页用的
	 * @param areaHtml 
	 * @param regex 
	 * @param withPrefix 返回的地址是否需要加一个前缀,如21cn的新闻,提取到后是没有前缀的,得加一个
	 * @param hasOmit  提取文章内分页时,是否有分页是否有省略号,如淘宝的就有。
	 * @param pageNumRegex  提取文章内的分页正则表达式
	 * @return
	 */
	public List<String> getListUrl(String areaHtml,String regex,String withPrefix,boolean hasOmit,String pageNumRegex){
		List<String> list = new ArrayList<String>();
		regex = regex.replaceFirst(WebContent.AREA_URL, "(.*?)");
		Pattern pa = Pattern.compile(regex, Pattern.MULTILINE);
		Matcher ma = pa.matcher(areaHtml);
		while (ma.find()) {
			if(withPrefix !=null && !"".equals(withPrefix.trim())){
				list.add(withPrefix+ma.group(1).trim());
			}else{
				list.add(ma.group(1).trim());
			}
		}
 
		if(hasOmit && list.size()>1){
			String lasturl = list.get(list.size()-1);
			list.clear(); 
			regex = "(.*?)"+pageNumRegex.replaceFirst(WebContent.PAGE_URL_NUM, "(-?\\\\d+)");			
			pa = Pattern.compile(regex);
			ma = pa.matcher(lasturl);
			if(ma.find()){
				int num = Integer.parseInt(ma.group(2));
				String prefix = ma.group(1).trim();
				String baseurl = prefix+pageNumRegex;
 
				for(int i = 2; i <= num; i ++)
				list.add(baseurl.replaceFirst(WebContent.PAGE_URL_NUM, i+""));
			}			
		}
 
		return list;
	}

以上两个函数,就可以得到文章列表及文章内分页列表。实际上有这些组合,就可以提取大部分的网站了,这里给一个例子,我的http://www.extbi.com这个站点的文章就是用这个提取的。大家根据上面的讲解,好好看吧。WebContent.java

标签: ,

4 Comments for this entry

1 Trackback or Pingback for this entry

你也讲两句吧~