偶然一次在vs2012默认的项目文件夹里发现了以前自己做的一个关于SEO的类库,主要是用来查询某个网址的收录次数还有网站的排行数,后来重构了下,今天拿出来写篇文章,说说自己是如何思考的并完成的。
首先需要考虑的是能够支持哪些搜索引擎的查询,首先是百度,然后是必应、搜狗、搜搜、360。本来想支持Google但是一想不对,根本不好访问的,所以暂时不算在内。而我们实际要做的就是根据一个网址能够检索出这个网址的在各个搜索引擎的收录次数以及在不同关键词下的网址排行,这里出入的只有网址还有若干的关键词,而输出则是该网址在不同搜索引擎下的收录次数以及在各个关键词下的排行数。
但是这里有个问题,就是排行数,如果检索的网址在前100还好,如果排名很后面,那么问题就来了,那样会让用户等待很长时间才能看到结果,但是用户可能只想知道排行前100的具体排名,而那些超过的则只要显示100以后就可以了,而这些就需要我们前期考虑好,这样后面的程序才好做。
相信很多人都能够想到,就是利用WebClient将将需要的页面下载下来,然后用正则从中获取我们感兴趣的部分,然后利用程序去处理。而关键难度就是在这个正则的编写,首先我们先从简单的开始。
首先是网站的收录次数,我们可以在百度中输入site:www.cnblogs.com/然后我们就可以看到如下的页面:
而我们所需要的收录次数就是 5,280,000 这段数字,我们接着查看页面元素:
接着我们再观察其他的搜索引擎可以发现都是类似的,所以我们的思路这个时候应该就得出了,最后就是如何组织网址,这部分我们看地址栏?wd=site%3Awww.cnblogs.com%2F这段就知道怎么写了。
稍等这个时候我们可能心急一个一个实现,这样后面我们就没法集中的调用,同时也会影响以后的新增,所以我们要规定一个要实现收录数功能的抽象类,这样就能够在不知晓具体实现的情况统一使用,并且还能够在以后轻松的新增新的搜索引擎,而这种方式属于策略模式(Stategry),下面我们来慢慢分析出这个抽象类的具体内容。
首先每个实现这个抽象类的具体类都应该是对应某个搜索引擎,那么就需要有一个基本网址,同时还要留下占位符,比如根据上面百度的这个我们就得出这样一个字符串
http://www.baidu.com/s?wd=site%3A{0}
其中{0}就是为真正需要检索网址的占位符,获取下载页面的路径是所有具体类都需要的所以我们直接将实现放在抽象类中,比如下面的代码:
1 /// <summary> 2 /// 服务提供者 3 /// </summary> 4 protected String SearchProvider { get; set; } 5 6 /// <summary> 7 /// 需要检索的网址 8 /// </summary> 9 protected String SiteUrl { get; set; } 10 11 /// <summary> 12 /// 搜索服务提供网址 13 /// </summary> 14 protected String BaseUrl { get; set; } 15 16 /// <summary> 17 /// 后页面网址 18 /// </summary> 19 /// <param name="site">需要查询的网址</param> 20 /// <returns>拼接后的网址</returns> 21 protected String GetDownUrl(string site) 22 { 23 return string.Format(BaseUrl, HttpUtility.UrlEncode(site)); 24 }
其中SiteUrl和SearchProvider是用来保存检索网址和搜索引擎名称。
上面我们说了将会利用WebClient来下载页面,所以初始化WebClient的工作也在抽象类中完成,尽可能的减少重复代码,而为了防止阻塞当前线程所以我们采用了Async方法。
具体代码如下所示:
1 /// <summary> 2 /// 查询在该搜索引擎中的收录次数 3 /// </summary> 4 /// <param name="siteurl">网站URL</param> 5 public void SearchIncludeCount(string siteurl) 6 { 7 SiteUrl = siteurl; 8 WebClient client = new WebClient(); 9 client.Encoding = Encoding.UTF8; 10 client.DownloadStringCompleted += DownloadStringCompleted; 11 client.DownloadStringAsync(new Uri(GetDownUrl(siteurl))); 12 } 13 14 /// <summary> 15 /// 检索收录次数的具体实现 16 /// 子类必须要实现该方法 17 /// </summary> 18 /// <param name="sender"></param> 19 /// <param name="e"></param> 20 protected abstract void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e);
当WebClient完成下载后将会回调DownloadStringCompleted方法,而这个方法的是抽象方法也就意味着具体类必须要实现这个方法。
虽然我们内部的实现是异步的但是对于其他开发者调用这个方法还是同步的,所以我们就需要借助委托因此我们还要新建一个委托类型:
/// <summary> /// 当完成一个网站的收录查询后回调 /// </summary> public Action<SiteIncludeCountResult> OnComplatedOneSite { get; set; }
其中SiteIncludeCountResult的结构如下所示:
1 /// <summary> 2 /// 用于网站收录中委托的参数 3 /// </summary> 4 public class SiteIncludeCountResult 5 { 6 /// <summary> 7 /// 收录次数 8 /// </summary> 9 public long IncludeCount { get; set; } 10 11 /// <summary> 12 /// 搜索引擎类型 13 /// </summary> 14 public String SearchType { get; set; } 15 16 /// <summary> 17 /// 网站URL 18 /// </summary> 19 public String SiteUrl { get; set; } 20 } 21 22 最后还有一个方法用于DownloadStringCompleted完成后回调OnComplatedOneSite委托: 23 /// <summary> 24 /// 完成处理后调用该方法将结果返回 25 /// </summary> 26 /// <param name="result">网址的收录数结果</param> 27 protected void SetCompleted(SiteIncludeCountResult result) 28 { 29 if (OnComplatedOneSite != null) 30 OnComplatedOneSite(result); 31 }
这样我们需要的抽象类就完成了,下面我们就可以开始实现第一个了,通过上面的截图我们可以发现要匹配这段字符串的正则表达式很简单:
百度为您找到相关结果约([\w,]+?)个
最后再将获取的字符串去掉逗号就可以强制转换了,这样结果就出来了,具体实现就像下面这样:
1 /// <summary> 2 /// 百度网站收录次数查询 3 /// </summary> 4 public class BaiDuSiteIncludeCount : SiteIncludeCountBase 5 { 6 public BaiDuSiteIncludeCount() 7 { 8 BaseUrl = "http://www.baidu.com/s?wd=site%3A{0}"; 9 SearchProvider = "百度"; 10 } 11 12 protected override void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) 13 { 14 var result = new SiteIncludeCountResult(); 15 result.SiteUrl = SiteUrl; 16 result.SearchType = SearchProvider; 17 result.IncludeCount = 0; 18 Regex reg = new Regex(@"百度为您找到相关结果约([\w,]+?)个", RegexOptions.IgnoreCase | RegexOptions.Singleline); 19 var matchs = reg.Matches(e.Result); 20 if (matchs.Count > 0) 21 { 22 string count = matchs[0].Groups[1].Value.Replace(",", ""); 23 result.IncludeCount = long.Parse(count); 24 } 25 SetCompleted(result); 26 } 27 }
以此类推,其他的都是按照这种就可以了,有兴趣的可以下载我的源码查看。
我们按照之前的思路,还是要先规定一个抽象类,但是其结构跟上面的抽象类很相似,所以笔者这里直接给出具体的代码:
1 /// <summary> 2 /// 实现关键词查询必须继承该类 3 /// </summary> 4 public abstract class KeyWordsSeoBase 5 { 6 protected String BaseUrl { get; set; } 7 8 protected String SearchProvider { get; set; } 9 10 protected String GetDownUrl(string keyword, string site, long current) 11 { 12 return String.Format(BaseUrl, HttpUtility.UrlEncode(keyword), current); 13 } 14 15 protected void SetCompleted(KeyWordsSeoResult result) 16 { 17 if (OnComplatedOneKeyWord != null) 18 { 19 OnComplatedOneKeyWord(result); 20 } 21 } 22 23 /// <summary> 24 /// 完成一个关键词的查询后回调该委托 25 /// </summary> 26 public Action<KeyWordsSeoResult> OnComplatedOneKeyWord { get; set; } 27 28 /// <summary> 29 /// 查询指定关键词和网站在该搜索引擎中的排行 30 /// 子类需要重写该方法 31 /// </summary> 32 /// <param name="keywords">关键词</param> 33 /// <param name="site">网站URL</param> 34 public abstract void SearchRanking(IEnumerable<string> keywords, string site,long count); 35 }
最大的区别在于具体的实现全部集中在SearchRanking中,通过keywords参数可以看出我们会支持多个关键词的查询,最后不同的就是下载路径的组织,因为涉及到翻页所以多了一个参数。
其中KeyWordsSeoResult的结构如下所示:
1 /// <summary> 2 /// 用于关键词排行查询的委托参数 3 /// </summary> 4 public class KeyWordsSeoResult 5 { 6 /// <summary> 7 /// 搜索引擎类型 8 /// </summary> 9 public String SearchType { get; set; } 10 11 /// <summary> 12 /// 关键词 13 /// </summary> 14 public String KeyWord { get; set; } 15 16 /// <summary> 17 /// 排行 18 /// </summary> 19 public long Ranking { get; set; } 20 }
废话不多说,我们来看百度的搜索结果页:
以上是笔者在百度中搜索程序员的排名第九个的html结构,或许你会觉得很简单只要获取div的id以及网址就可以了,但是很多搜索引擎的路径并不是直接的路径,而是会先链到百度然后重定向的,如果非要匹配我们就需要多做一件事就是访问这个路径得到真实的路径,那样就会加大这中间的等待时间,所以笔者采用的是直接截取上图中的<span class=”g”>后面的内容,这样就避免了一次请求。(不知道当初笔者怎么想的,实现的时候并没有采用id那个值而是在内部递增,估计这个id的序号在翻页后会出现问题吧),最后亮出我们神圣的正则表达式:
<span\s+class=""(?:g|c-showurl)"">([^/&]*)
以为这样就大公告成了?错了,在某些结果里面百度会给这个网址加上b标签,而笔者则采用全部赶尽杀绝的方式,利用正则全部删掉(反正又不看页面,只要拿到我想要的就OK了),实现的时候我们可不能直接实现多个关键词的判明,应该是实现一个关键词的,然后循环调用即可了,下面是笔者的单个关键词的实现:
1 protected KeyWordsSeoResult SearchFunc(string key, string siteurl, long total) 2 { 3 var result = new KeyWordsSeoResult(); 4 result.KeyWord = key; 5 result.Ranking = total + 1; 6 var reg = new Regex(@"<span\s+class=""(?:g|c-showurl)"">([^/&]*)", RegexOptions.IgnoreCase | RegexOptions.Singleline); 7 var replace = new Regex("</?b>", RegexOptions.IgnoreCase | RegexOptions.Singleline); 8 var client = new WebClient(); 9 long current = 0; 10 long pos = 0; 11 for (; ; ) 12 { 13 String url = GetDownUrl(key, siteurl, current); 14 String downstr = client.DownloadString(url); 15 downstr = replace.Replace(downstr, ""); 16 var matchs = reg.Matches(downstr); 17 foreach (Match match in matchs) 18 { 19 pos++; 20 string suburl = match.Groups[1].Value; 21 try 22 { 23 if (suburl.ToLower() == siteurl.ToLower()) 24 { 25 result.Ranking = pos; 26 return result; 27 } 28 } 29 catch 30 { 31 continue; 32 } 33 } 34 current += 10; 35 if (current > total) 36 { 37 current -= 10; 38 if (current >= total) 39 { 40 break; 41 } 42 current = total; 43 } 44 } 45 return result; 46 }
注意for循环的结束部分,这里是用来处理分页的,以翻到下一页继续检索。其他的大体部分都跟笔者说的一样,下载页面->正则匹配->根据匹配结果判断。剩下的就是SearchRanking的实现,就是循环关键词,只是这里笔者为每个搜索引擎新建线程来实现,当然这不怎么好,所以读者可以改用更好的方式来做:
1 public override void SearchRanking(IEnumerable<string> keywords, string site, long count) 2 { 3 new Thread(() => 4 { 5 foreach (string key in keywords) 6 { 7 KeyWordsSeoResult result = SearchFunc(key, site, count); 8 result.SearchType = SearchProvider; 9 SetCompleted(result); 10 } 11 }).Start(); 12 }
有了这些我们就可以写出一个简洁的类来负责管理,笔者这里直接给出代码:
1 /// <summary> 2 /// 查询网站的收录次数以及排行 3 /// </summary> 4 public class RankingAndIncludeSeo 5 { 6 /// <summary> 7 /// 关键词列表 8 /// </summary> 9 public IList<KeyWordsSeoBase> KeyWordsSeoList { get; private set; } 10 11 /// <summary> 12 /// 收录次数列表 13 /// </summary> 14 public IList<SiteIncludeCountBase> SiteIncludeCountList { get; private set; } 15 16 public RankingAndIncludeSeo() 17 { 18 KeyWordsSeoList = new List<KeyWordsSeoBase>(); 19 SiteIncludeCountList = new List<SiteIncludeCountBase>(); 20 } 21 22 /// <summary> 23 /// 当完成一个关键词的查询后回调该委托 24 /// </summary> 25 public Action<KeyWordsSeoResult> OnComplatedAnyKeyWordsSearch { get; set; } 26 27 /// <summary> 28 /// 当完成一个网站的收录次数查询后回调该委托 29 /// </summary> 30 public Action<SiteIncludeCountResult> OnComplatedAnySiteIncludeSearch { get; set; } 31 32 /// <summary> 33 /// 查询网址的排行 34 /// </summary> 35 /// <param name="keywords">关键词组</param> 36 /// <param name="siteurl">查询的网址</param> 37 /// <param name="count">最大限制排行数</param> 38 public void SearchKeyWordsRanking(IEnumerable<string> keywords, string siteurl, long count = 100) 39 { 40 if (keywords == null) 41 throw new ArgumentNullException("keywords", "必须存在关键词"); 42 if (siteurl == null) 43 throw new ArgumentNullException("siteurl", "必须存在网站URL"); 44 foreach (KeyWordsSeoBase kwsb in KeyWordsSeoList) 45 { 46 kwsb.OnComplatedOneKeyWord = kwsb.OnComplatedOneKeyWord ?? OnComplatedAnyKeyWordsSearch; 47 kwsb.SearchRanking(keywords, siteurl, count); 48 } 49 } 50 51 /// <summary> 52 /// 查询网址的收录次数 53 /// </summary> 54 /// <param name="siteurl">查询的网址</param> 55 public void SearchSiteIncludeCount(string siteurl) 56 { 57 if (siteurl == null) 58 throw new ArgumentNullException("siteurl", "必须指定网站"); 59 foreach (SiteIncludeCountBase sicb in SiteIncludeCountList) 60 { 61 sicb.OnComplatedOneSite = sicb.OnComplatedOneSite ?? OnComplatedAnySiteIncludeSearch; 62 sicb.SearchIncludeCount(siteurl); 63 } 64 } 65 }
RankingAndIncludeSeo中提供了公共的委托,如果单个搜索引擎没有提供委托那么就采用这个公共的,如果已经指定了单独的委托就不会被赋值了,而其他开发者调用的时候只要向KeyWordsSeoList和SiteIncludeCountList中添加已经实现的类就可以了,方面其他开发者开发出自己的实现并加入其中。
这篇随笔总的来说并不是讲述什么高端技术的,仅仅只是提供一种大致的思路以及结构上的设计,如果读者需要应用于实际开发中,最好加以验证,笔者并不能保证关键词的排名没有任何误差,因为搜索的结果会由于任何因素发生改变。