|
摘要:本文讨论了如何使用C#2.0实现抓取网络资源的网络蜘蛛。使用这个程序,可以通过一个入口网址(如http://www.17aspx.com)来扫描整个互联网的网址,并将这些扫描到的网址所指向的网络资源下载到本地。然后可以利用其他的分析工具对这些网络资源做进一步地分析,如提取关键词、分类索引等。也可以将这些网络资源作为数据源来实现象Google一样的搜索引擎。 一、引言 ![]() 图1 最后一个双环的状态是最终态。下面让我们来看看获得<a>的实现代码。 getA方法的实现 // 获得html中的<a> private void getA() { char[] buffer = new char[1024]; int state = 0; String a = ""; while (!sr.EndOfStream) { int n = sr.Read(buffer, 0, buffer.Length); for (int i = 0; i < n; i++) { switch (state) { case 0: // 状态0 if (buffer[i] == '<') // 读入的是'<' { a += buffer[i]; state = 1; // 切换到状态1 } break; case 1: // 状态1 if (buffer[i] == 'a' || buffer[i] == 'A') // 读入是'a'或'A' { a += buffer[i]; state = 2; // 切换到状态2 } else { a = ""; state = 0; // 切换到状态0 } break; case 2: // 状态2 if (buffer[i] == ' ' || buffer[i] == '\t') // 读入的是空格或'\t' { a += buffer[i]; state = 3; } else { a = ""; state = 0; // 切换到状态0 } break; case 3: // 状态3 if (buffer[i] == '>') // 读入的是'>',已经成功获得一个<a> { a += buffer[i]; try { string url = getUrl(getHref(a)); // 获得<a>中的href属性的值 if (url != null) { if (findUrl != null) findUrl(url); // 引发发现url的事件 } } catch (Exception e) { } state = 0; // 在获得一个<a>后,重新切换到状态0 } else a += buffer[i]; break; } } } } 在getA方法中除了切换到状态0外,其他的状态切换都将已经读入的字符赋给String变量a,如果最后发现变量a中的字符串不可能是<a>后,就将a清空,并切换到状态0后重新读入字符。 在getA方法中使用了一个重要的方法getHref来从<a>中获得href部分。getHref方法的实现如下: getHref方法的实现 // 从<a>中获得Href private String getHref(string a) { try { string p = @"href\s*=\s*('[^']*'|""[^""]*""|\S+\s+)"; // 获得Href的正则表达式 MatchCollection matches = Regex.Matches(a, p, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); foreach (Match nextMatch in matches) { return nextMatch.Value; // 返回href } return null; } catch (Exception e) { throw e; } } 在getHref方法中使用了正则表达式从<a>中获得href。在<a>中正确的href属性格式有三种情况,这三种情况的主要区别是url两边的符号,如单引号、双引号或没有符号。这三种情况如下所示: 情况1: <a href = "http://www.17aspx.com" > comprg</a> 情况2: <a href = 'http://www.17aspx.com' > comprg</a> 情况3: <a href = http://www.17aspx.com > comprg</a> getHref方法中的p存储了用于过滤这三种情况的href,也就是说,使用正则表达式可以获得上述三种情况的href如下: 从情况1获得得的href:href = "http://www.17aspx.com" 从情况2获得得的href:href = 'http://www.17aspx.com' 从情况3获得得的href:href = http://www.17aspx.com 在获得上述的href后,需要将url提出来。这个功能由getUrl完成,这个方法的实现代码如下: getUrl方法的实现 // 从href中提取url private String getUrl(string href) { try { if (href == null) return href; int n = href.IndexOf('='); // 查找'='位置 String s = href.Substring(n + 1); int begin = 0, end = 0; string sign = ""; if (s.Contains("\"")) // 第一种情况 sign = "\""; else if (s.Contains("'")) // 第二种情况 sign = "'"; else // 第三种情况 return getFullUrl(s.Trim()); begin = s.IndexOf(sign); end = s.LastIndexOf(sign); return getFullUrl(s.Substring(begin + 1, end - begin - 1).Trim()); } catch (Exception e) { throw e; } } 在获得url时有一点应该注意。有的url使用的是相对路径,也就是没有“http://host”部分,但将url保存时需要保存它们的完整路径。这就需要根据相对路径获得它们的完整路径。这个功能由getFullUrl方法完成。这个方法的实现代码如下: getFullUrl方法的实现代码 // 将相对路径变为绝对路径 private String getFullUrl(string url) { try { if (url == null) return url; if (processPattern(url)) return null; // 过滤不想下载的url // 如果url前有http://或https://,为绝对路径,按原样返回 if (url.ToLower().StartsWith("http://") || url.ToLower().StartsWith("https://")) return url; Uri parentUri = new Uri(parentUrl); string port = ""; if (!parentUri.IsDefaultPort) port = ":" + parentUri.Port.ToString(); if (url.StartsWith("/")) // url以"/"开头,直接放在host后面 return parentUri.Scheme + "://" + parentUri.Host + port + url; else // url不以"/"开头,放在url的路径后面 { string s = ""; s = parentUri.LocalPath.Substring(0, parentUri.LocalPath.LastIndexOf("/")); return parentUri.Scheme + "://" + parentUri.Host + port + s + "/" + url; } } catch (Exception e) { throw e; } } 在ParseResource中还提供了一个功能就是通过正则表达式过滤不想下载的url,这个功能将通过processPattern方法完成。实现代码如下: processPattern方法的实现代码 // 如果返回true,表示url符合pattern,否则,不符合模式 private bool processPattern(string url) { foreach (string p in patterns) { if (Regex.IsMatch(url, p, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture) && !p.Equals("")) return true; } return false; } ParseResource类在分析html代码之前,先将html下载到本地的线程目录中,再通过FileStream打开并读取待分析的数据。ParseResource类其他的实现代码请读者参阅本文提供的源代码。 七、键树的实现 在获取Url的过程中,难免重复获得一些Url。这些重复的Url将大大增加网络蜘蛛的下载时间,以及会导致其他的分析工具重复分析同一个html。因此,就需要对过滤出重复的Url,也就是说,要使网络蜘蛛下载的Url都是唯一的。达到这个目的的最简单的方法就是将已经下载过的Url保存到一个集合中,然后在下载新的Url之前,在这个集合中查找这个新的Url是否被下载过,如果下载过,就忽略这个Url。 这个功能从表面上看非常简单,但由于我们处理的是成千上万的Url,要是将这些Url简单地保存在类似List一样的集合中,不仅会占用大量的内存空间,而且当Url非常多时,如一百万。这时每下载一个Url,就要从这一百万的Url中查找这个待下载的Url是否存在。虽然可以使用某些查找算法(如折半查找)来处理,但当数据量非常大时,任何查找算法的效率都会大打折扣。因此,必须要设计一种新的存储结构来完成这个工作。这个新的数据存储结构需要具有两个特性: 1. 尽可能地减少存储Url所使用的内存。 2. 查找Url的速度尽可能地快(最好的可能是查找速度和Url的数量无关)。 下面先来完成第一个特性。一般一个Url都比较长,如平均每个Url有50个字符。如果有很多Url,每个Url占50个字符,一百万个Url就是会占用50M的存储空间。而我们保存Url的目的只有一个,就是查找某一个Url是否存在。因此,只需要将Url的Hashcode保存起来即可。由于Hashcode为Int类型,因此,Hashcode要比一个Url字符串使用更少的存储空间。 对于第二个特性,我们可以使用数据结构中的键树来解决。假设有一个数是4532。首先将其转换为字符串。然后每个键树节点有10个(0至9)。这样4532的存储结构如图2所示: ![]() 图2 从上面的数据结构可以看出,查找一个整数只和这个整数的位数有关,和整数的数量无关。这个键树的实现代码如下: KeyTree的实现代码 class KeyTreeNode // 键树节点的结构 { // 指向包含整数下一个的结点的指针 public KeyTreeNode[] pointers = new KeyTreeNode[10]; // 结束位标志,如果为true,表示当前结点为整数的最后一位 public bool[] endFlag = new bool[10]; } class KeyTree { private KeyTreeNode rootNode = new KeyTreeNode(); // 根结点 // 向键树中添加一个无符号整数 public void add(uint n) { string s = n.ToString(); KeyTreeNode tempNode = rootNode; int index = 0; for (int i = 0; i < s.Length; i++) { index = int.Parse(s[i].ToString()); // 获得整数每一位的值 if (i == s.Length - 1) // 在整数的最后一位时,将结束位设为true { tempNode.endFlag[index] = true; break; } if (tempNode.pointers[index] == null) // 当下一个结点的指针为空时,新建立一个结点对象 tempNode.pointers[index] = new KeyTreeNode(); tempNode = tempNode.pointers[index]; } } // 判断一个整数是否存在 public bool exists(uint n) { string s = n.ToString(); KeyTreeNode tempNode = rootNode; int index = 0; for (int i = 0; i < s.Length; i++) { if (tempNode != null) { index = int.Parse(s[i].ToString()); // 当整数的最后一位的结束标志为true时,表示n存在 if((i == s.Length - 1)&& (tempNode.endFlag[index] == true)) return true; else tempNode = tempNode.pointers[index]; } else return false; } return false; } } 上面代码中的KeyTreeNode之所以要使用结束标志,而不根据指针是否为空判断某个整数的存在,是因为可能存在长度不相等的整数,如4321和432。如果只使用指针判断。保存4321后,432也会被认为存在。而如果用结束标志后,在值为2的节点的结束标志为false,因此,表明432并不存在。下面的UrlFilter使用了上面的键树来处理Url。 UrlFilter类的实现代码 // 用于将url重新组合后再加到键树中 // 如http://www.17aspx.com和http://www.17aspx.com/是一样的 // 因此,它们的hashcode也要求一样 class UrlFilter { public static KeyTree urlHashCode = new KeyTree(); private static object syncUrlHashCode = new object(); private static string processUrl(string url) // 重新组合Url { try { Uri uri = new Uri(url); string s = uri.PathAndQuery; if(s.Equals("/")) s = ""; return uri.Host + s; } catch(Exception e) { throw e; } } private static bool exists(string url) // 判断url是否存在 { try { lock (syncUrlHashCode) { url = processUrl(url); return urlHashCode.exists((uint)url.GetHashCode()); } } catch (Exception e) { throw e; } } public static bool isOK(string url) { return !exists(url); } // 加处理完的Url加到键树中 public static void addUrl(string url) { try { lock (syncUrlHashCode) { url = processUrl(url); urlHashCode.add((uint)url.GetHashCode()); } } catch (Exception e) { throw e; } } } 八、其他部分的实现 到现在为止,网络蜘蛛所有核心代码都已经完成了。下面让我们做一个界面来使下载过程可视化。界面如图3所示。 ![]() 图3 这个界面主要通过一个定时器每2秒钟获得个一次网络蜘蛛的下载状态。包括获得的URL数和已经下载的网络资源数。其中这些状态信息都保存在一个Common类的静态变量中。Common类和主界面的代码请读者参阅本文提供的源代码。 九、结束语 至此,网络蜘蛛程序已经全部完成了。但在实际应用中,光靠一台机器下载整个的网络资源是远远不够的。这就需要通过多台机器联合下载。然而这就会给我们带来一个难题。就是这些机器需要对已经下载的Url进行同步。读者可以根据本文提供的例子,将其改成分布式的可多机同时下载的网络蜘蛛。这样网络蜘蛛的下载速度将会有一个质的飞跃。 (责任编辑:admin) |








骆驼户外男 真皮磨砂日常休闲鞋 低帮 2011秋冬新款 专柜正品特价