前几天写一个爬虫,看到网上有使用jsoup直接去访问并抓取目标url,但是个人感觉jsoup解析html还行,其直接连接目标网页的能力还是相较HttpClient弱一些,所以使用了HttpClient来连接并下载目标网页,而只单纯的使用jsoup来解析网页。

   jsoup解析网页有几种方法:包括从输入流,从本地File对象,从url,从html字符串中。为了避免使用jsoup自己的网络接口,同时减小磁盘的io,所以使用从输入流中来进行Parse。

   

        InputStream htmlStream = entity.getContent();
	 reStream = saveToLocal(htmlStream, filePath);

    将getContent函数返回的输入流传入下面的saveLocal中,实现将网页文件存储到本地磁盘上。然后再次把同样的输入流对象再交给jsoup来解析。

	Document doc = Jsoup.parse(htmlIn, "UTF-8", "");

     然后执行程序,发现从目标输入流中解析出来的待抓取set永远是空的,断点一看。才发现因为doc就已经是空的了。原来,输入流在一次read以后,其内部的指针已经移动到了尾部,所以对于同一个输入流来说,一般是不能重复使用的。

     但是还有一个办法,就是使用inputStream接口中定义的mark方法和reset方法来实现一个类似于标记,返回的功能。在众多输入流中,貌似只有BufferInputStream这个输入流已经实现并重写的合适的mark和reset函数。也就是会说,其他的大部分输入流不具有这个二次使用的功能。除非自己再重写其源码。所以,直接将网络流包装成BufferInputSteam来使用其标记-回溯功能。

public BufferedInputStream saveToLocal(InputStream content, String filePath)
			throws IOException {
		FileOutputStream out = new FileOutputStream(filePath);
		BufferedInputStream reader = new BufferedInputStream(content,8192000);
		byte[] containter = new byte[1024];
		reader.mark(reader.available() + 1);
		int count = 0;
		while ((count = reader.read(containter)) > 0) {
			out.write(containter);
		}
		out.flush();
		out.close();
		reader.reset();
		return reader;
	}

     这里的意思是,先将网络流包装成BufferInputStream,然后再读取输入流之前,先标记一下,然后再读取完毕,且输出到输出中以后,调用reset方法,将该输入流的指针回溯到初始位置,然后这才将其交给jsoup来用。这样就解决了二次利用的问题。

     但是,在实际的运行过程中,又发现了一个问题。就是,在抓取百度网页时无压力,但是在抓取新浪时出现一个“reset失败”的exception。经过一段时间的查询求解,原来这个mark和reset还有一些使用的限制的。他只能配合我们的BufferInputStream的缓冲区来使用,也就是说:

     假设缓冲区大小为10,我们要读取的文件大小为5,那么我们读入前标记文件的起始位置,然后执行读取。这时候,整个文件从首到尾都放入了缓冲区,所以我们执行reset可以找到标记,同时重定位到标记所指定的位置。这种情况下还没有问题,但是如果文件并缓冲区大,我们的缓冲区需要更新,比如一个100大小的文件,那么在读取到最后时,缓冲区里只存储了90-100的这一段内容,我们再reset,就已经找不到那个标记了,所以reset函数就会报错。

    所以使用mark-reset时,要注意对缓冲区大小进行合理的设置,如果要读取的文件总是很大,我们也不可能把缓冲区调的那么的大,则此时,我们可能要做好,对于这样大的文件的输入流可能真的不能多次使用的心理准备咯!