java多线程爬虫实现

先上做的结果吧:

[java] view plain copy

print?

  1. 开始爬虫.........................................
  2. 当前有1个线程在等待
  3. 当前有2个线程在等待
  4. 当前有3个线程在等待
  5. 当前有4个线程在等待
  6. 当前有5个线程在等待
  7. .....................
开始爬虫.........................................
当前有1个线程在等待
当前有2个线程在等待
当前有3个线程在等待
当前有4个线程在等待
当前有5个线程在等待
.....................

[java] view plain copy

print?

  1. 爬网页http://dev.yesky.com成功,深度为2 是由线程thread-9来爬
  2. 当前有7个线程在等待
  3. 爬网页http://www.cnblogs.com/rexyoung/archive/2012/05/01/2477960.html成功,深度为2 是由线程thread-2来爬
  4. 当前有8个线程在等待
  5. 爬网页http://www.hjenglish.com 成功,深度为2 是由线程thread-0来爬
  6. 当前有9个线程在等待
  7. 爬网页http://www.cnblogs.com/snandy/archive/2012/05/01/2476675.html成功,深度为2 是由线程thread-5来爬
  8. 当前有10个线程在等待
  9. 总共爬了159个网页
  10. 总共耗时53秒
爬网页http://dev.yesky.com成功,深度为2 是由线程thread-9来爬
当前有7个线程在等待
爬网页http://www.cnblogs.com/rexyoung/archive/2012/05/01/2477960.html成功,深度为2 是由线程thread-2来爬
当前有8个线程在等待
爬网页http://www.hjenglish.com 成功,深度为2 是由线程thread-0来爬
当前有9个线程在等待
爬网页http://www.cnblogs.com/snandy/archive/2012/05/01/2476675.html成功,深度为2 是由线程thread-5来爬
当前有10个线程在等待
总共爬了159个网页
总共耗时53秒

上面是爬博客园的主页,只爬了两级深度,10个线程,总共耗时53秒,应该速度还算不错的,下面是所有的代码:

[java] view plain copy

print?

  1. public class WebCrawler {
  2. ArrayList<String> allurlSet = new ArrayList<String>();//所有的网页url,需要更高效的去重可以考虑HashSet
  3. ArrayList<String> notCrawlurlSet = new ArrayList<String>();//未爬过的网页url
  4. HashMap<String, Integer> depth = new HashMap<String, Integer>();//所有网页的url深度
  5. int crawDepth  = 2; //爬虫深度
  6. int threadCount = 10; //线程数量
  7. int count = 0; //表示有多少个线程处于wait状态
  8. public static final Object signal = new Object();   //线程间通信变量
  9. public static void main(String[] args) {
  10. final WebCrawler wc = new WebCrawler();
  11. //      wc.addUrl("http://www.126.com", 1);
  12. wc.addUrl("http://www.cnblogs.com", 1);
  13. long start= System.currentTimeMillis();
  14. System.out.println("开始爬虫.........................................");
  15. wc.begin();
  16. while(true){
  17. if(wc.notCrawlurlSet.isEmpty()&& Thread.activeCount() == 1||wc.count==wc.threadCount){
  18. long end = System.currentTimeMillis();
  19. System.out.println("总共爬了"+wc.allurlSet.size()+"个网页");
  20. System.out.println("总共耗时"+(end-start)/1000+"秒");
  21. System.exit(1);
  22. //              break;
  23. }
  24. }
  25. }
  26. private void begin() {
  27. for(int i=0;i<threadCount;i++){
  28. new Thread(new Runnable(){
  29. public void run() {
  30. //                  System.out.println("当前进入"+Thread.currentThread().getName());
  31. //                  while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
  32. //                      String tmp = getAUrl();
  33. //                      crawler(tmp);
  34. //                  }
  35. while (true) {
  36. //                      System.out.println("当前进入"+Thread.currentThread().getName());
  37. String tmp = getAUrl();
  38. if(tmp!=null){
  39. crawler(tmp);
  40. }else{
  41. synchronized(signal) {  //------------------(2)
  42. try {
  43. count++;
  44. System.out.println("当前有"+count+"个线程在等待");
  45. signal.wait();
  46. } catch (InterruptedException e) {
  47. // TODO Auto-generated catch block
  48. e.printStackTrace();
  49. }
  50. }
  51. }
  52. }
  53. }
  54. },"thread-"+i).start();
  55. }
  56. }
  57. public synchronized  String getAUrl() {
  58. if(notCrawlurlSet.isEmpty())
  59. return null;
  60. String tmpAUrl;
  61. //      synchronized(notCrawlurlSet){
  62. tmpAUrl= notCrawlurlSet.get(0);
  63. notCrawlurlSet.remove(0);
  64. //      }
  65. return tmpAUrl;
  66. }
  67. //  public synchronized  boolean isEmpty() {
  68. //      boolean f = notCrawlurlSet.isEmpty();
  69. //      return f;
  70. //  }
  71. public synchronized void  addUrl(String url,int d){
  72. notCrawlurlSet.add(url);
  73. allurlSet.add(url);
  74. depth.put(url, d);
  75. }
  76. //爬网页sUrl
  77. public  void crawler(String sUrl){
  78. URL url;
  79. try {
  80. url = new URL(sUrl);
  81. //              HttpURLConnection urlconnection = (HttpURLConnection)url.openConnection();
  82. URLConnection urlconnection = url.openConnection();
  83. urlconnection.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
  84. InputStream is = url.openStream();
  85. BufferedReader bReader = new BufferedReader(new InputStreamReader(is));
  86. StringBuffer sb = new StringBuffer();//sb为爬到的网页内容
  87. String rLine = null;
  88. while((rLine=bReader.readLine())!=null){
  89. sb.append(rLine);
  90. sb.append("/r/n");
  91. }
  92. int d = depth.get(sUrl);
  93. System.out.println("爬网页"+sUrl+"成功,深度为"+d+" 是由线程"+Thread.currentThread().getName()+"来爬");
  94. if(d<crawDepth){
  95. //解析网页内容,从中提取链接
  96. parseContext(sb.toString(),d+1);
  97. }
  98. //              System.out.println(sb.toString());
  99. } catch (IOException e) {
  100. //          crawlurlSet.add(sUrl);
  101. //          notCrawlurlSet.remove(sUrl);
  102. e.printStackTrace();
  103. }
  104. }
  105. //从context提取url地址
  106. public  void parseContext(String context,int dep) {
  107. String regex = "<a href.*?/a>";
  108. //      String regex = "<title>.*?</title>";
  109. String s = "fdfd<title>我 是</title><a href=\"http://www.iteye.com/blogs/tag/Google\">Google</a>fdfd<>";
  110. // String regex ="http://.*?>";
  111. Pattern pt = Pattern.compile(regex);
  112. Matcher mt = pt.matcher(context);
  113. while (mt.find()) {
  114. //          System.out.println(mt.group());
  115. Matcher myurl = Pattern.compile("href=\".*?\"").matcher(
  116. mt.group());
  117. while(myurl.find()){
  118. String str = myurl.group().replaceAll("href=\"|\"", "");
  119. //              System.out.println("网址是:"+ str);
  120. if(str.contains("http:")){ //取出一些不是url的地址
  121. if(!allurlSet.contains(str)){
  122. addUrl(str, dep);//加入一个新的url
  123. if(count>0){ //如果有等待的线程,则唤醒
  124. synchronized(signal) {  //---------------------(2)
  125. count--;
  126. signal.notify();
  127. }
  128. }
  129. }
  130. }
  131. }
  132. }
  133. }
  134. }
public class WebCrawler {
	ArrayList<String> allurlSet = new ArrayList<String>();//所有的网页url,需要更高效的去重可以考虑HashSet
	ArrayList<String> notCrawlurlSet = new ArrayList<String>();//未爬过的网页url
	HashMap<String, Integer> depth = new HashMap<String, Integer>();//所有网页的url深度
	int crawDepth  = 2; //爬虫深度
	int threadCount = 10; //线程数量
	int count = 0; //表示有多少个线程处于wait状态
	public static final Object signal = new Object();   //线程间通信变量

	public static void main(String[] args) {
		final WebCrawler wc = new WebCrawler();
//		wc.addUrl("http://www.126.com", 1);
		wc.addUrl("http://www.cnblogs.com", 1);
		long start= System.currentTimeMillis();
		System.out.println("开始爬虫.........................................");
		wc.begin();

		while(true){
			if(wc.notCrawlurlSet.isEmpty()&& Thread.activeCount() == 1||wc.count==wc.threadCount){
				long end = System.currentTimeMillis();
				System.out.println("总共爬了"+wc.allurlSet.size()+"个网页");
				System.out.println("总共耗时"+(end-start)/1000+"秒");
				System.exit(1);
//				break;
			}

		}
	}
	private void begin() {
		for(int i=0;i<threadCount;i++){
			new Thread(new Runnable(){
				public void run() {
//					System.out.println("当前进入"+Thread.currentThread().getName());
//					while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
//						String tmp = getAUrl();
//						crawler(tmp);
//					}
					while (true) {
//						System.out.println("当前进入"+Thread.currentThread().getName());
						String tmp = getAUrl();
						if(tmp!=null){
							crawler(tmp);
						}else{
							synchronized(signal) {  //------------------(2)
								try {
									count++;
									System.out.println("当前有"+count+"个线程在等待");
									signal.wait();
								} catch (InterruptedException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}

						}
					}
				}
			},"thread-"+i).start();
		}
	}
	public synchronized  String getAUrl() {
		if(notCrawlurlSet.isEmpty())
			return null;
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}
//	public synchronized  boolean isEmpty() {
//		boolean f = notCrawlurlSet.isEmpty();
//		return f;
//	}

	public synchronized void  addUrl(String url,int d){
			notCrawlurlSet.add(url);
			allurlSet.add(url);
			depth.put(url, d);
	}

	//爬网页sUrl
	public  void crawler(String sUrl){
		URL url;
		try {
				url = new URL(sUrl);
//				HttpURLConnection urlconnection = (HttpURLConnection)url.openConnection();
				URLConnection urlconnection = url.openConnection();
				urlconnection.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
				InputStream is = url.openStream();
				BufferedReader bReader = new BufferedReader(new InputStreamReader(is));
				StringBuffer sb = new StringBuffer();//sb为爬到的网页内容
				String rLine = null;
				while((rLine=bReader.readLine())!=null){
					sb.append(rLine);
					sb.append("/r/n");
				}

				int d = depth.get(sUrl);
				System.out.println("爬网页"+sUrl+"成功,深度为"+d+" 是由线程"+Thread.currentThread().getName()+"来爬");
				if(d<crawDepth){
					//解析网页内容,从中提取链接
					parseContext(sb.toString(),d+1);
				}
//				System.out.println(sb.toString());

		} catch (IOException e) {
//			crawlurlSet.add(sUrl);
//			notCrawlurlSet.remove(sUrl);
			e.printStackTrace();
		}
	}

	//从context提取url地址
	public  void parseContext(String context,int dep) {
	    String regex = "<a href.*?/a>";
//		String regex = "<title>.*?</title>";
		String s = "fdfd<title>我 是</title><a href=\"http://www.iteye.com/blogs/tag/Google\">Google</a>fdfd<>";
		// String regex ="http://.*?>";
		Pattern pt = Pattern.compile(regex);
		Matcher mt = pt.matcher(context);
		while (mt.find()) {
//			System.out.println(mt.group());
			Matcher myurl = Pattern.compile("href=\".*?\"").matcher(
					mt.group());
			while(myurl.find()){
				String str = myurl.group().replaceAll("href=\"|\"", "");
//				System.out.println("网址是:"+ str);
				if(str.contains("http:")){ //取出一些不是url的地址
					if(!allurlSet.contains(str)){
						addUrl(str, dep);//加入一个新的url
						if(count>0){ //如果有等待的线程,则唤醒
							synchronized(signal) {  //---------------------(2)
								count--;
								signal.notify();
							}
						}

					}
				}
			}
		}
	}
}	

在上面(1)(2)两个地方卡了很久,两个地方其实是一个知识点,都是多线程的知识:

一开始用了

[java] view plain copy

print?

  1. //                  while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
  2. //                      String tmp = getAUrl();
  3. //                      crawler(tmp);
  4. //                  }
//					while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
//						String tmp = getAUrl();
//						crawler(tmp);
//					}

一进入线程就判断notCrawlurlSet为不为空,但是是多线程的,一开始notCrawlurlSet不为空,所以所有的线程都进入了循环,尽管getAul()方法我设置了synchronized,但是一旦一个线程从getAurl()方法出来,另外一个线程就会进去,看一开始的getAurl方法的代码:

[java] view plain copy

print?

  1. public synchronized  String getAUrl() {
  2. String tmpAUrl;
  3. //      synchronized(notCrawlurlSet){
  4. tmpAUrl= notCrawlurlSet.get(0);
  5. notCrawlurlSet.remove(0);
  6. //      }
  7. return tmpAUrl;
  8. }
	public synchronized  String getAUrl() {
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}

每一次都会删除一个notCrawlurlSet数组里面的元素,导致第一个线程执行完getAUrl方法时,且notCrawlurlSet恰好为空的时候,另外一个线程进入就会报错,因为notCrawlUrlSet没有元素,get(0)会报错。后来把getAUrl函数改成:

[java] view plain copy

print?

  1. public synchronized  String getAUrl() {
  2. if(notCrawlurlSet.isEmpty())
  3. return null;
  4. String tmpAUrl;
  5. //      synchronized(notCrawlurlSet){
  6. tmpAUrl= notCrawlurlSet.get(0);
  7. notCrawlurlSet.remove(0);
  8. //      }
  9. return tmpAUrl;
  10. }
	public synchronized  String getAUrl() {
		if(notCrawlurlSet.isEmpty())
			return null;
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}

在线程的run函数改成:

[java] view plain copy

print?

  1. while (true) {
  2. System.out.println("当前进入"+Thread.currentThread().getName());
  3. String tmp = getAUrl();
  4. if(tmp!=null){
  5. crawler(tmp);
  6. }else{
  7. synchronized(signal) {
  8. try {
  9. count++;
  10. System.out.println("当前有"+count+"个线程在等待");
  11. signal.wait();
  12. } catch (InterruptedException e) {
  13. // TODO Auto-generated catch block
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
					while (true) {
//						System.out.println("当前进入"+Thread.currentThread().getName());
						String tmp = getAUrl();
						if(tmp!=null){
							crawler(tmp);
						}else{
							synchronized(signal) {
								try {
									count++;
									System.out.println("当前有"+count+"个线程在等待");
									signal.wait();
								} catch (InterruptedException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}

						}
					}

即线程进入后就调用getAUrl函数,从notCrawlurlSet数组取url,如果没有取到,则用signal来让此线程等待,但是在哪里唤醒呢?肯定在notCrawlurlSet有元素的时候唤醒,即notCrawlurlSet不能空的时候,这其中有个很重要的变量count,它表示正在等待的线程个数,只有count大于0才会唤醒线程,即只有有线程在等待的时候才会调用signal.notify(); 此段实现在parseContext函数里面:

[java] view plain copy

print?

  1. if(str.contains("http:")){ //取出一些不是url的地址
  2. if(!allurlSet.contains(str)){
  3. addUrl(str, dep);//加入一个新的url
  4. if(count>0){ //如果有等待的线程,则唤醒
  5. synchronized(signal) {
  6. count--;
  7. signal.notify();
  8. }
  9. }
  10. }
  11. }
				if(str.contains("http:")){ //取出一些不是url的地址
					if(!allurlSet.contains(str)){
						addUrl(str, dep);//加入一个新的url
						if(count>0){ //如果有等待的线程,则唤醒
							synchronized(signal) {
								count--;
								signal.notify();
							}
						}
					}
				}

这个count变量还解决了我一个问题,当所有的线程启动后,也正确的爬取网页了,但是不知道怎么结束这些线程,因为线程都是永久循环的,有了count变量,就知道有多少线程在等待,当等待的线程等于threadCount的时候,就表示已经爬完了,因为所有线程都在等待了,不会往notCrawlurlSet添加新的url了,此时已经爬完了指定深度的所有网页。

写下自己的一点感悟,明白原理是一回事,有时候实现起来也挺费神的。

代码几度修改,还有待完善的地方及我的思路:

1:爬取的网页要存起来,该怎么存放也是一个问题,目录怎么生成?网页自动分类?等等,分类可以用考虑贝叶斯分类器,分好类之后安装类别来存储。

2:网页去重问题,如果url太多,内存装不下去怎么办?考虑先压缩,比如MD5压缩,同时MD5又能得到hash值,最简单的是hash去重,或者可以考虑用bloom filter去重,还有一种方法是考虑用key-value数据库来实现去重,不过我对key-value数据库不是很了解,应该类似hash,但是效率的问题数据库已经帮你解决了。

3:url不同的网页也可能内容一样,怎么判断网页相似度问题。网页相似度可以先提取网页正文,方法有行块函数法,提取正文后再可以用向量余弦法来计算相似度。

4:增量抓取的问题,一个网页抓取之后,什么时候再重新来抓?可以针对具体的网页的更新频率来解决这个问题,如新浪首页的新闻可能更新快一些,重新来爬的频率会更快一点。

很早就知道爬虫的原理,但是一直没有去实现过,今天写起来还真遇到很多困难,尤其是多线程同步的问题。还是自己对多线程不熟,没有大量实践过的原因。

先上我做的结果吧:

[java] view plain copy

print?

  1. 开始爬虫.........................................
  2. 当前有1个线程在等待
  3. 当前有2个线程在等待
  4. 当前有3个线程在等待
  5. 当前有4个线程在等待
  6. 当前有5个线程在等待
  7. .....................
开始爬虫.........................................
当前有1个线程在等待
当前有2个线程在等待
当前有3个线程在等待
当前有4个线程在等待
当前有5个线程在等待
.....................

[java] view plain copy

print?

  1. 爬网页http://dev.yesky.com成功,深度为2 是由线程thread-9来爬
  2. 当前有7个线程在等待
  3. 爬网页http://www.cnblogs.com/rexyoung/archive/2012/05/01/2477960.html成功,深度为2 是由线程thread-2来爬
  4. 当前有8个线程在等待
  5. 爬网页http://www.hjenglish.com 成功,深度为2 是由线程thread-0来爬
  6. 当前有9个线程在等待
  7. 爬网页http://www.cnblogs.com/snandy/archive/2012/05/01/2476675.html成功,深度为2 是由线程thread-5来爬
  8. 当前有10个线程在等待
  9. 总共爬了159个网页
  10. 总共耗时53秒
爬网页http://dev.yesky.com成功,深度为2 是由线程thread-9来爬
当前有7个线程在等待
爬网页http://www.cnblogs.com/rexyoung/archive/2012/05/01/2477960.html成功,深度为2 是由线程thread-2来爬
当前有8个线程在等待
爬网页http://www.hjenglish.com 成功,深度为2 是由线程thread-0来爬
当前有9个线程在等待
爬网页http://www.cnblogs.com/snandy/archive/2012/05/01/2476675.html成功,深度为2 是由线程thread-5来爬
当前有10个线程在等待
总共爬了159个网页
总共耗时53秒

上面是爬博客园的主页,只爬了两级深度,10个线程,总共耗时53秒,应该速度还算不错的,下面是所有的代码:

[java] view plain copy

print?

  1. public class WebCrawler {
  2. ArrayList<String> allurlSet = new ArrayList<String>();//所有的网页url,需要更高效的去重可以考虑HashSet
  3. ArrayList<String> notCrawlurlSet = new ArrayList<String>();//未爬过的网页url
  4. HashMap<String, Integer> depth = new HashMap<String, Integer>();//所有网页的url深度
  5. int crawDepth  = 2; //爬虫深度
  6. int threadCount = 10; //线程数量
  7. int count = 0; //表示有多少个线程处于wait状态
  8. public static final Object signal = new Object();   //线程间通信变量
  9. public static void main(String[] args) {
  10. final WebCrawler wc = new WebCrawler();
  11. //      wc.addUrl("http://www.126.com", 1);
  12. wc.addUrl("http://www.cnblogs.com", 1);
  13. long start= System.currentTimeMillis();
  14. System.out.println("开始爬虫.........................................");
  15. wc.begin();
  16. while(true){
  17. if(wc.notCrawlurlSet.isEmpty()&& Thread.activeCount() == 1||wc.count==wc.threadCount){
  18. long end = System.currentTimeMillis();
  19. System.out.println("总共爬了"+wc.allurlSet.size()+"个网页");
  20. System.out.println("总共耗时"+(end-start)/1000+"秒");
  21. System.exit(1);
  22. //              break;
  23. }
  24. }
  25. }
  26. private void begin() {
  27. for(int i=0;i<threadCount;i++){
  28. new Thread(new Runnable(){
  29. public void run() {
  30. //                  System.out.println("当前进入"+Thread.currentThread().getName());
  31. //                  while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
  32. //                      String tmp = getAUrl();
  33. //                      crawler(tmp);
  34. //                  }
  35. while (true) {
  36. //                      System.out.println("当前进入"+Thread.currentThread().getName());
  37. String tmp = getAUrl();
  38. if(tmp!=null){
  39. crawler(tmp);
  40. }else{
  41. synchronized(signal) {  //------------------(2)
  42. try {
  43. count++;
  44. System.out.println("当前有"+count+"个线程在等待");
  45. signal.wait();
  46. } catch (InterruptedException e) {
  47. // TODO Auto-generated catch block
  48. e.printStackTrace();
  49. }
  50. }
  51. }
  52. }
  53. }
  54. },"thread-"+i).start();
  55. }
  56. }
  57. public synchronized  String getAUrl() {
  58. if(notCrawlurlSet.isEmpty())
  59. return null;
  60. String tmpAUrl;
  61. //      synchronized(notCrawlurlSet){
  62. tmpAUrl= notCrawlurlSet.get(0);
  63. notCrawlurlSet.remove(0);
  64. //      }
  65. return tmpAUrl;
  66. }
  67. //  public synchronized  boolean isEmpty() {
  68. //      boolean f = notCrawlurlSet.isEmpty();
  69. //      return f;
  70. //  }
  71. public synchronized void  addUrl(String url,int d){
  72. notCrawlurlSet.add(url);
  73. allurlSet.add(url);
  74. depth.put(url, d);
  75. }
  76. //爬网页sUrl
  77. public  void crawler(String sUrl){
  78. URL url;
  79. try {
  80. url = new URL(sUrl);
  81. //              HttpURLConnection urlconnection = (HttpURLConnection)url.openConnection();
  82. URLConnection urlconnection = url.openConnection();
  83. urlconnection.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
  84. InputStream is = url.openStream();
  85. BufferedReader bReader = new BufferedReader(new InputStreamReader(is));
  86. StringBuffer sb = new StringBuffer();//sb为爬到的网页内容
  87. String rLine = null;
  88. while((rLine=bReader.readLine())!=null){
  89. sb.append(rLine);
  90. sb.append("/r/n");
  91. }
  92. int d = depth.get(sUrl);
  93. System.out.println("爬网页"+sUrl+"成功,深度为"+d+" 是由线程"+Thread.currentThread().getName()+"来爬");
  94. if(d<crawDepth){
  95. //解析网页内容,从中提取链接
  96. parseContext(sb.toString(),d+1);
  97. }
  98. //              System.out.println(sb.toString());
  99. } catch (IOException e) {
  100. //          crawlurlSet.add(sUrl);
  101. //          notCrawlurlSet.remove(sUrl);
  102. e.printStackTrace();
  103. }
  104. }
  105. //从context提取url地址
  106. public  void parseContext(String context,int dep) {
  107. String regex = "<a href.*?/a>";
  108. //      String regex = "<title>.*?</title>";
  109. String s = "fdfd<title>我 是</title><a href=\"http://www.iteye.com/blogs/tag/Google\">Google</a>fdfd<>";
  110. // String regex ="http://.*?>";
  111. Pattern pt = Pattern.compile(regex);
  112. Matcher mt = pt.matcher(context);
  113. while (mt.find()) {
  114. //          System.out.println(mt.group());
  115. Matcher myurl = Pattern.compile("href=\".*?\"").matcher(
  116. mt.group());
  117. while(myurl.find()){
  118. String str = myurl.group().replaceAll("href=\"|\"", "");
  119. //              System.out.println("网址是:"+ str);
  120. if(str.contains("http:")){ //取出一些不是url的地址
  121. if(!allurlSet.contains(str)){
  122. addUrl(str, dep);//加入一个新的url
  123. if(count>0){ //如果有等待的线程,则唤醒
  124. synchronized(signal) {  //---------------------(2)
  125. count--;
  126. signal.notify();
  127. }
  128. }
  129. }
  130. }
  131. }
  132. }
  133. }
  134. }
public class WebCrawler {
	ArrayList<String> allurlSet = new ArrayList<String>();//所有的网页url,需要更高效的去重可以考虑HashSet
	ArrayList<String> notCrawlurlSet = new ArrayList<String>();//未爬过的网页url
	HashMap<String, Integer> depth = new HashMap<String, Integer>();//所有网页的url深度
	int crawDepth  = 2; //爬虫深度
	int threadCount = 10; //线程数量
	int count = 0; //表示有多少个线程处于wait状态
	public static final Object signal = new Object();   //线程间通信变量

	public static void main(String[] args) {
		final WebCrawler wc = new WebCrawler();
//		wc.addUrl("http://www.126.com", 1);
		wc.addUrl("http://www.cnblogs.com", 1);
		long start= System.currentTimeMillis();
		System.out.println("开始爬虫.........................................");
		wc.begin();

		while(true){
			if(wc.notCrawlurlSet.isEmpty()&& Thread.activeCount() == 1||wc.count==wc.threadCount){
				long end = System.currentTimeMillis();
				System.out.println("总共爬了"+wc.allurlSet.size()+"个网页");
				System.out.println("总共耗时"+(end-start)/1000+"秒");
				System.exit(1);
//				break;
			}

		}
	}
	private void begin() {
		for(int i=0;i<threadCount;i++){
			new Thread(new Runnable(){
				public void run() {
//					System.out.println("当前进入"+Thread.currentThread().getName());
//					while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
//						String tmp = getAUrl();
//						crawler(tmp);
//					}
					while (true) {
//						System.out.println("当前进入"+Thread.currentThread().getName());
						String tmp = getAUrl();
						if(tmp!=null){
							crawler(tmp);
						}else{
							synchronized(signal) {  //------------------(2)
								try {
									count++;
									System.out.println("当前有"+count+"个线程在等待");
									signal.wait();
								} catch (InterruptedException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}

						}
					}
				}
			},"thread-"+i).start();
		}
	}
	public synchronized  String getAUrl() {
		if(notCrawlurlSet.isEmpty())
			return null;
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}
//	public synchronized  boolean isEmpty() {
//		boolean f = notCrawlurlSet.isEmpty();
//		return f;
//	}

	public synchronized void  addUrl(String url,int d){
			notCrawlurlSet.add(url);
			allurlSet.add(url);
			depth.put(url, d);
	}

	//爬网页sUrl
	public  void crawler(String sUrl){
		URL url;
		try {
				url = new URL(sUrl);
//				HttpURLConnection urlconnection = (HttpURLConnection)url.openConnection();
				URLConnection urlconnection = url.openConnection();
				urlconnection.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
				InputStream is = url.openStream();
				BufferedReader bReader = new BufferedReader(new InputStreamReader(is));
				StringBuffer sb = new StringBuffer();//sb为爬到的网页内容
				String rLine = null;
				while((rLine=bReader.readLine())!=null){
					sb.append(rLine);
					sb.append("/r/n");
				}

				int d = depth.get(sUrl);
				System.out.println("爬网页"+sUrl+"成功,深度为"+d+" 是由线程"+Thread.currentThread().getName()+"来爬");
				if(d<crawDepth){
					//解析网页内容,从中提取链接
					parseContext(sb.toString(),d+1);
				}
//				System.out.println(sb.toString());

		} catch (IOException e) {
//			crawlurlSet.add(sUrl);
//			notCrawlurlSet.remove(sUrl);
			e.printStackTrace();
		}
	}

	//从context提取url地址
	public  void parseContext(String context,int dep) {
	    String regex = "<a href.*?/a>";
//		String regex = "<title>.*?</title>";
		String s = "fdfd<title>我 是</title><a href=\"http://www.iteye.com/blogs/tag/Google\">Google</a>fdfd<>";
		// String regex ="http://.*?>";
		Pattern pt = Pattern.compile(regex);
		Matcher mt = pt.matcher(context);
		while (mt.find()) {
//			System.out.println(mt.group());
			Matcher myurl = Pattern.compile("href=\".*?\"").matcher(
					mt.group());
			while(myurl.find()){
				String str = myurl.group().replaceAll("href=\"|\"", "");
//				System.out.println("网址是:"+ str);
				if(str.contains("http:")){ //取出一些不是url的地址
					if(!allurlSet.contains(str)){
						addUrl(str, dep);//加入一个新的url
						if(count>0){ //如果有等待的线程,则唤醒
							synchronized(signal) {  //---------------------(2)
								count--;
								signal.notify();
							}
						}

					}
				}
			}
		}
	}
}	

在上面(1)(2)两个地方卡了很久,两个地方其实是一个知识点,都是多线程的知识:

一开始用了

[java] view plain copy

print?

  1. //                  while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
  2. //                      String tmp = getAUrl();
  3. //                      crawler(tmp);
  4. //                  }
//					while(!notCrawlurlSet.isEmpty()){ ----------------------------------(1)
//						String tmp = getAUrl();
//						crawler(tmp);
//					}

一进入线程就判断notCrawlurlSet为不为空,但是是多线程的,一开始notCrawlurlSet不为空,所以所有的线程都进入了循环,尽管getAul()方法我设置了synchronized,但是一旦一个线程从getAurl()方法出来,另外一个线程就会进去,看一开始的getAurl方法的代码:

[java] view plain copy

print?

  1. public synchronized  String getAUrl() {
  2. String tmpAUrl;
  3. //      synchronized(notCrawlurlSet){
  4. tmpAUrl= notCrawlurlSet.get(0);
  5. notCrawlurlSet.remove(0);
  6. //      }
  7. return tmpAUrl;
  8. }
	public synchronized  String getAUrl() {
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}

每一次都会删除一个notCrawlurlSet数组里面的元素,导致第一个线程执行完getAUrl方法时,且notCrawlurlSet恰好为空的时候,另外一个线程进入就会报错,因为notCrawlUrlSet没有元素,get(0)会报错。后来把getAUrl函数改成:

[java] view plain copy

print?

  1. public synchronized  String getAUrl() {
  2. if(notCrawlurlSet.isEmpty())
  3. return null;
  4. String tmpAUrl;
  5. //      synchronized(notCrawlurlSet){
  6. tmpAUrl= notCrawlurlSet.get(0);
  7. notCrawlurlSet.remove(0);
  8. //      }
  9. return tmpAUrl;
  10. }
	public synchronized  String getAUrl() {
		if(notCrawlurlSet.isEmpty())
			return null;
		String tmpAUrl;
//		synchronized(notCrawlurlSet){
			tmpAUrl= notCrawlurlSet.get(0);
			notCrawlurlSet.remove(0);
//		}
		return tmpAUrl;
	}

在线程的run函数改成:

[java] view plain copy

print?

  1. while (true) {
  2. System.out.println("当前进入"+Thread.currentThread().getName());
  3. String tmp = getAUrl();
  4. if(tmp!=null){
  5. crawler(tmp);
  6. }else{
  7. synchronized(signal) {
  8. try {
  9. count++;
  10. System.out.println("当前有"+count+"个线程在等待");
  11. signal.wait();
  12. } catch (InterruptedException e) {
  13. // TODO Auto-generated catch block
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
					while (true) {
//						System.out.println("当前进入"+Thread.currentThread().getName());
						String tmp = getAUrl();
						if(tmp!=null){
							crawler(tmp);
						}else{
							synchronized(signal) {
								try {
									count++;
									System.out.println("当前有"+count+"个线程在等待");
									signal.wait();
								} catch (InterruptedException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}

						}
					}

即线程进入后就调用getAUrl函数,从notCrawlurlSet数组取url,如果没有取到,则用signal来让此线程等待,但是在哪里唤醒呢?肯定在notCrawlurlSet有元素的时候唤醒,即notCrawlurlSet不能空的时候,这其中有个很重要的变量count,它表示正在等待的线程个数,只有count大于0才会唤醒线程,即只有有线程在等待的时候才会调用signal.notify(); 此段实现在parseContext函数里面:

[java] view plain copy

print?

  1. if(str.contains("http:")){ //取出一些不是url的地址
  2. if(!allurlSet.contains(str)){
  3. addUrl(str, dep);//加入一个新的url
  4. if(count>0){ //如果有等待的线程,则唤醒
  5. synchronized(signal) {
  6. count--;
  7. signal.notify();
  8. }
  9. }
  10. }
  11. }
				if(str.contains("http:")){ //取出一些不是url的地址
					if(!allurlSet.contains(str)){
						addUrl(str, dep);//加入一个新的url
						if(count>0){ //如果有等待的线程,则唤醒
							synchronized(signal) {
								count--;
								signal.notify();
							}
						}
					}
				}

这个count变量还解决了我一个问题,当所有的线程启动后,也正确的爬取网页了,但是不知道怎么结束这些线程,因为线程都是永久循环的,有了count变量,就知道有多少线程在等待,当等待的线程等于threadCount的时候,就表示已经爬完了,因为所有线程都在等待了,不会往notCrawlurlSet添加新的url了,此时已经爬完了指定深度的所有网页。

写下自己的一点感悟,明白原理是一回事,有时候实现起来也挺费神的。

代码几度修改,还有待完善的地方及我的思路:

1:爬取的网页要存起来,该怎么存放也是一个问题,目录怎么生成?网页自动分类?等等,分类可以用考虑贝叶斯分类器,分好类之后安装类别来存储。

2:网页去重问题,如果url太多,内存装不下去怎么办?考虑先压缩,比如MD5压缩,同时MD5又能得到hash值,最简单的是hash去重,或者可以考虑用bloom filter去重,还有一种方法是考虑用key-value数据库来实现去重,不过我对key-value数据库不是很了解,应该类似hash,但是效率的问题数据库已经帮你解决了。

3:url不同的网页也可能内容一样,怎么判断网页相似度问题。网页相似度可以先提取网页正文,方法有行块函数法,提取正文后再可以用向量余弦法来计算相似度。

4:增量抓取的问题,一个网页抓取之后,什么时候再重新来抓?可以针对具体的网页的更新频率来解决这个问题,如新浪首页的新闻可能更新快一些,重新来爬的频率会更快一点。

时间: 2024-08-07 21:31:58

java多线程爬虫实现的相关文章

原创Java多线程详解(一)

只看书不实践是不行的.来实践一下~~~~~~(引用请指明来源) 先看看百科对多线程的介绍 http://baike.baidu.com/view/65706.htm?fr=aladdin Java对多线程的支持 Java创建多线程的3种常用方法: 1)继承Thread类 重写Thread类的run方法,创建Thread子类实例,启动线程. 例如: /* * @author [email protected] wangxu */ public class TreadOfextends extend

开源的49款Java 网络爬虫软件

参考地址 搜索引擎 Nutch Nutch 是一个开源Java 实现的搜索引擎.它提供了我们运行自己的搜索引擎所需的全部工具.包括全文搜索和Web爬虫. Nutch的创始人是Doug Cutting,他同时也是Lucene.Hadoop和Avro开源项目的创始人. Nutch诞生于2002年8月,是Apache旗下的一个用Java实现... JAVA爬虫 WebCollector 爬虫简介: WebCollector是一个无须配置.便于二次开发的JAVA爬虫框架(内核),它提供精简的的API,只

【转】44款Java 网络爬虫开源软件

原帖地址 http://www.oschina.net/project/lang/19?tag=64&sort=time 极简网络爬虫组件 WebFetch WebFetch 是无依赖极简网页爬取组件,能在移动设备上运行的微型爬虫. WebFetch 要达到的目标: 没有第三方依赖jar包 减少内存使用 提高CPU利用率 加快网络爬取速度 简洁明了的api接口 能在Android设备上稳定运行 小巧灵活可以方便集成的网页抓取组件 使用...更多WebFetch信息 开源爬虫框架 Guozhong

爬虫.多线程爬虫与多进程爬虫

多线程爬虫 多线程的复杂性 1.资源.数据的安全性:锁保护 2.原子性:数据操作是天然互斥的 3.同步等待:wait().notify().notifyAll() 4.死锁:多个线程对资源互锁,造成死锁 5.容灾:任何线程出现错误,整个进程都会停止 多线程的优势 1.内存空间共享,信息数据交换效率高 2.提高CPU的使用效率 3.开发便捷 4.轻,创建.销毁的开销小 Python线程 支持多线程(JavaScript PHP 不支持多线程) Python线程直接映射到native线程(Java1

爬虫学习之第四章爬虫进阶之多线程爬虫

多线程爬虫 有些时候,比如下载图片,因为下载图片是一个耗时的操作.如果采用之前那种同步的方式下载.那效率肯会特别慢.这时候我们就可以考虑使用多线程的方式来下载图片. 多线程介绍: 多线程是为了同步完成多项任务,通过提高资源使用效率来提高系统的效率.线程是在同一时间需要完成多项任务的时候实现的.最简单的比喻多线程就像火车的每一节车厢,而进程则是火车.车厢离开火车是无法跑动的,同理火车也可以有多节车厢.多线程的出现就是为了提高效率.同时它的出现也带来了一些问题.更多介绍请参考:https://bai

学 Java 网络爬虫,需要哪些基础知识?

说起网络爬虫,大家想起的估计都是 Python ,诚然爬虫已经是 Python 的代名词之一,相比 Java 来说就要逊色不少.有不少人都不知道 Java 可以做网络爬虫,其实 Java 也能做网络爬虫而且还能做的非常好,在开源社区中有不少优秀的 Java 网络爬虫框架,例如 webmagic .我的第一份正式工作就是使用 webmagic 编写数据采集程序,当时参与了一个舆情分析系统的开发,这里面涉及到了大量网站的新闻采集,我们就使用了 webmagic 进行采集程序的编写,由于当时不知道其设

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3534050.html Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可

从JAVA多线程理解到集群分布式和网络设计的浅析

对于JAVA多线程的应用非常广泛,现在的系统没有多线程几乎什么也做不了,很多时候我们在何种场合如何应用多线程成为一种首先需要选择的问题,另外关于java多线程的知识也是非常的多,本文中先介绍和说明一些常用的,在后续文章中如果有必要再说明更加复杂的吧,本文主要说明多线程的一下几个内容: 1.在应用开发中什么时候选择多线程? 2.多线程应该注意些什么? 3.状态转换控制,如何解决死锁? 4.如何设计一个具有可扩展性的多线程处理器? 5.多线程联想:在多主机下的扩展-集群? 6.WEB应用的多线程以及

java多线程心得

多并发的时候,在什么情况下必须加锁?如果不加锁会产生什么样的后果. 加锁的场景跟java的new thread和Runnable的关系是什么? 看看java的concurrentMap源码. 还有spring 的web.xml启动执行源码 spring aop http://www.cnblogs.com/FDROSE1001/p/3661895.html activemq的本质是什么? java的jms hibernate由配置文件映射到实体类的本质是什么? java反射 spring aop