一、前言
偶然一次在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中添加已經實現的類就可以了,方面其他開發者開發出自己的實現並加入其中。
七、小節
這篇隨筆總的來說並不是講述什么高端技術的,僅僅只是提供一種大致的思路以及結構上的設計,如果讀者需要應用於實際開發中,最好加以驗證,筆者並不能保證關鍵詞的排名沒有任何誤差,因為搜索的結果會由於任何因素發生改變。