去年底用 多線程+HtmlAgilityPack.dll 寫了一個抓取“慧聰網” 公司信息的小程序,代碼慘不忍賭。好在能抓到數據,速度也能讓人忍受就很久沒管了。
最近這段時間把這個小程序發給同事看着玩,沒想到他老感興趣了。然后寫了一個抓“新浪微博”個人資料的小程序,由於用正則表達式,代碼精簡不少,效率也很高,頓時覺得有種挫敗感啊。
於是不懂正則的我決定學習下正則,順便學習一下線程池的用法。
沒有用正則和線程池之前,我的代碼是這樣的。
//下面這段代碼使用HtmlAgilityPack寫的,由於對它了解不深就寫成下面這個鳥樣子了.....
foreach (HtmlNode node in GetSource.GetHtmlDocument(GetSource.TrimCode(htmlStr)).DocumentNode.ChildNodes[0].ChildNodes) { if (node.InnerHtml.Trim() == "") { continue; }
//一個又一個點..... url = node.ChildNodes[0].ChildNodes[0].ChildNodes[0].ChildNodes[0].ChildNodes[0].Attributes["href"].Value; CompanyInfo company = new CompanyInfo(); company.CompanyName = node.ChildNodes[0].ChildNodes[0].ChildNodes[0].InnerText; SearchInfo(url, company); } //對於多線程,只會這么寫...
th = new Thread(new ThreadStart(delegate { hc.SearchData(); })); th.Name = "hc360線程"; th.IsBackground = true; th.Start(); //天知道這個線程什么時候執行完。
我能確認一點的是,上面這段代碼雖慘不忍睹卻能實實在在的把數據找出來。可是,這不是我想要的。
經過學習“正則表達式”和"線程池",且先不說其它的,光代碼看着就舒服了O(∩_∩)O~。
//從網頁代碼提取出總頁數
string pageHtml = TrimOther(new Common.HttpRequestHelper().getHTML(string.Format(SiteUrl, 1))); if (Regex.IsMatch(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=頁</span>)")) { pageHtml = Regex.Match(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=頁</span>)").Value; PageCount = FunLayer.Transform.Int(pageHtml, 0); } //提取公司名稱
user.CompanyName = Regex.Match(htmlStr, "<h1[^>]+?>(?'CompanyName'.+?)</h1>", RegexOptions.Singleline | RegexOptions.IgnoreCase).Groups["CompanyName"].Value.Trim();
比起那些一串點的用法,我更喜歡正則帶來的便利和效率。
首先說下正則
以下是基礎語法...
.匹配除換行符以外的任意字符。(在.net里用 "RegexOptions.Singleline"可以改變(.)的含義,讓它能匹配每一個字符)
\w匹配字母、數字、下划線和漢字。(試過匹配“漢字”,可就是沒匹配出來。。。求解!!)
\s匹配空白符,它不是“ ”。(在得到網頁源碼后你會發現的)
上面是簡單的“元字符”,更多元字符請百度...或者問大神...
接着介紹“重復”,實際應用中用的非常頻繁。
* 重復匹配零次或更多次 、+ 重復一次或更多次。 兩者的區別:+ 重復至少1次,* 0次也可以哦。
? 重復零次或一次 、 {n} 重復n次 、 {n,}重復n到更多次 (個人理解{1,}和+應該是有同樣效果的)
{n,m}重復 n到m次 ({0,1}和?也應該是等效的)
分枝條件
符號:“|”,我理解成了 運算符“||”,或者的關系。代碼執行順序也和運算符“||”一樣。
C#簡單例子 ,相信各位都能理解。
if("你妹".Equals("你妹")||"牛逼".Equals("很屌")) { Console.WriteLine("你妹都和你妹一樣了,誰管你牛逼是不是很屌"); }
正則里面:表達式1|表達式2|表達式3,如果表達式1匹配成功了,那么后面的表達式不會執行,用的時候需要注意左右順序,是從左往右執行。
反義
就介紹一個 [^],例子:[^>] => 匹配除了 > 以外的任意字符,秒懂了吧。其它的反義字符,請百度...
后向引用-捕獲
(exp) 匹配exp,並捕獲文本到自動命名的組里
(?<name>exp) 匹配exp,並捕獲文本到名稱為name的組里,也可以寫成(?'name'exp) 。這個很好用,當你試圖用一條正則表達式匹配出多個不同結果,且需要保存起來時,你會發現這個東西非常好。
貪婪與懶惰
理解正則的貪婪與懶惰非常重要,因為這會直接影響你想要的。
*? 重復任意次,但盡可能少重復
+? 重復1次或更多次,......(懶得復制了,都是“但盡可能少重復”這幾個字。)
?? 重復0次或1次,.....
{n,m}? 重復n到m次,......
{n,}? 重復n次以上,......
對比重復,上面的區別就是“盡可能少重復”,那么盡可能少重復是什么意思呢?
來個例子:
1、adsfasdfad 表達式 a.+ 結果:adsfasdfad
2、adsfasdfad 表達式 a.+? 結果:ad、as、ad (分組結果)
好吧,以上例子可能會有疑問,為啥2結果不是 “adsf”、“asdf”、“ad”?
簡單粗暴的解釋方式: a.+? 意思是“a開頭除換行以外的任意字符重復1次或更多次但盡可能少重復”(對照意思一個個復制過來)。問題就出在了“但盡可能少重復”上,正則匹配的時候是這么回事:
按照主人寫的正則,程序找到了 a ,后面跟了個 d ,找到了ad,試圖去找后面的s的時候主人說了“要盡可能少重復,ad不是已經找到了嘛”,所以d后面的就不找了。
那為毛后面的第二個a又找到了呢?這就和重復、正則本身有關。重復的是a后面的字符,滿足條件本輪匹配則終止,進行一下輪匹配...直到找到最后一個字符(不是絕對的,比如用到“^$\b”等元字符的時候)。
再來個例子,瞬間就秒懂了。
aaasdfsdfsdf .+? 結果:aa,as
個人覺得,相比“但盡可能少重復”一詞,“貪婪與懶惰”描述的形象得多。你想,懶惰的方式不就是“ 找到了啊,好耶,可以收工下班了”。貪婪“ 地上有1塊錢?不管前面還有沒有錢都要一直走一直走...走到頭”(當然這里的盡頭,在正則里是可以設定的)。
aaasdfsdfsdf a.+d 找到的是 aaasdfsdfsd , 最后的 f 是不會找到的。 (注意區分好源字符和正則字符)
線程池
在多線程的程序中,經常會出現兩種情況:
1. 應用程序中線程把大部分的時間花費在等待狀態,等待某個事件發生,然后給予響應。這一般使用 ThreadPool(線程池)來解決。
2. 線程平時都處於休眠狀態,只是周期性地被喚醒。這一般使用 Timer(定時器)來解決。
ThreadPool 類提供一個由系統維護的線程池(可以看作一個線程的容器),該容器需要 Windows 2000 以上系統支持,因為其中某些方法調用了只有高版本的Windows 才有的 API 函數。
將線程安放在線程池里,需使用 ThreadPool.QueueUserWorkItem() 方法,該方法的原型如下:
// 將一個線程放進線程池,該線程的 Start() 方法將調用 WaitCallback 代理對象代表的函數
public static bool QueueUserWorkItem(WaitCallback);
// 重載的方法如下,參數 object 將傳遞給 WaitCallback 所代表的方法
public static bool QueueUserWorkItem(WaitCallback, object);
以上內容都是從網上抄的....
不用線程池的代碼:
th = new Thread(new ThreadStart(delegate { hc.SearchData(); })); th.Name = "hc360線程"; th.IsBackground = true; th.Start();
不用線程池,隨時都可以開N個線程跑起來。然后你會發現CPU使用率直接飆到99%...
而用了線程池后,你會發現cpu使用率保持在一個穩定的高度,也就是說,系統在協調線程的創建執行。
ThreadPool.QueueUserWorkItem() 詳細解釋:
//
// 摘要: // 將方法排入隊列以便執行,並指定包含該方法所用數據的對象。此方法在有線程池線程變得可用時執行。 //
// 參數: // callBack: // System.Threading.WaitCallback,它表示要執行的方法。 //
// state: // 包含方法所用數據的對象。 //
// 返回結果: // 如果此方法成功排隊,則為 true;如果未能將該工作項排隊,則引發 System.NotSupportedException。 //
// 異常: // System.NotSupportedException: // 承載公共語言運行時 (CLR) 的宿主不支持此操作。 //
// System.ArgumentNullException: // callBack 為 null。
也就是說線程池是將線程排隊入池,當達到線程池中運行做多線程時,其它線程均處於等待狀態。
相關:ThreadPool.SetMaxThreads(50, 50); //設置池中同時活動的線程請求數 ,那我有51個線程需要入池怎么辦?個人理解,此時51個線程都會入池,但是不是所有線程都立即請求執行,超過可以同時處於活動狀態的線程池的請求數目的線程將保持等待或排隊狀態。
// 摘要: // 設置可以同時處於活動狀態的線程池的請求數目。所有大於此數目的請求將保持排隊狀態,直到線程池線程變為可用。 //
// 參數: // workerThreads: // 線程池中輔助線程的最大數目。 //
// completionPortThreads: // 線程池中異步 I/O 線程的最大數目。 //
// 返回結果: // 如果更改成功,則為 true;否則為 false。
廢話一推還沒有說抓取數據,對不起標題啊。
對於抓取數據,原理老簡單了。就是從“網頁源碼內獲取你想要的”,不簡單的地方在於要涉及到http請求的方式、是否需要cookie、是否需要注意是代理ip等等。
本文的方向不是深入講解這一塊,側重講解正則和線程池,抓取到數據是在這兩項技術下的結果。被騙了....
以下貼出部分代碼,復制下來絕對能抓到“慧聰網”數據!
有圖有真相:
/// <summary> /// get獲取網頁源碼(最簡單的方式) /// </summary> /// <param name="url"></param> /// <returns></returns> public string getHTML(string url) { HttpWebRequest httpWebRequest; httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(url); httpWebRequest.ContentType = _contentType; httpWebRequest.Referer = url; httpWebRequest.Accept = _accept; httpWebRequest.UserAgent = _userAgent; httpWebRequest.Method = "Get"; HttpWebResponse httpWebResponse; httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); Stream streamresponse = httpWebResponse.GetResponseStream(); StreamReader sr = new StreamReader(streamresponse, Encoding.Default); string html = sr.ReadToEnd(); sr.Close();//關閉流 streamresponse.Close();//關閉流 return html; } /// <summary> /// 去除很多帶“\”的符號,例:回車符 /// </summary> /// <param name="source"></param> /// <returns></returns> public string TrimOther(string source) { source = Regex.Replace(source, "<script[^>]*?>.*?</script>", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, "<noscript[^>]*?>.*?</noscript>", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, "<style[^>]*?>.*?</style>", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, "\n|\t|\r", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, ">\\s+<", "><", RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, "\\s+<", "<", RegexOptions.IgnoreCase | RegexOptions.Singleline); source = Regex.Replace(source, ">\\s+", ">", RegexOptions.IgnoreCase | RegexOptions.Singleline); return source
.Replace(@"\""", "\"") .Replace(" ", " ")
.Replace(" ",""); }
static void Main(string[] args)
{
//設置同時處於活動狀態的線程池的請求數目
ThreadPool.SetMaxThreads(50, 50); //執行主要函數
HuiCong.Instance.Grab(); //阻止當前線程,等收到信號燈后再放行
HuiCong.Instance.eventX.WaitOne(Timeout.Infinite, true); Console.WriteLine("執行完了"); Console.ReadKey();
} public partial class HuiCong : BaseModel<HuiCong> { //創建信號燈
public ManualResetEvent eventX = new ManualResetEvent(false); protected int iCount = 0; /// <summary>
/// 主函數 /// </summary>
public void Grab() { SiteUrl = "http://s.hc360.com/?mc=enterprise&ee={0}&z=%D6%D0%B9%FA%3A%B1%B1%BE%A9"; string pageHtml = TrimOther(new Common.HttpRequestHelper().getHTML(string.Format(SiteUrl, 1))); if (Regex.IsMatch(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=頁</span>)")) { pageHtml = Regex.Match(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=頁</span>)").Value; PageCount = FunLayer.Transform.Int(pageHtml, 0); } for (int i = 1; i <= PageCount; i++) { //循環每一頁將線程排入線程池 //將函數排入隊列,線程池將自動分配線程執行
ThreadPool.QueueUserWorkItem(new WaitCallback(Search), i); } } /// <summary>
/// 每一頁數據的分析 /// </summary>
/// <param name="index"></param>
private void Search(object index) { string htmlStr = TrimOther(new Common.HttpRequestHelper().getHTML(string.Format(SiteUrl, index))); MatchCollection mc = Regex.Matches(htmlStr, "(?<=<!-- col S -->).+?(?=<!-- bottom_info bm-info -->)", RegexOptions.IgnoreCase | RegexOptions.Singleline); foreach (var item in mc) { if (Regex.IsMatch(item.ToString(), "href=\"(?'link'.*?)\"\\s?onclick", RegexOptions.IgnoreCase | RegexOptions.Singleline)) { string companyLink = Regex.Match(item.ToString(), "href=\"(?'link'.*?)\"\\s?onclick", RegexOptions.IgnoreCase).Groups["link"].Value; GetDeatil(companyLink); } } //多線程並行執行情況下,執行原子操作(此處是在某一正在進行的線程執行完上面的代碼后執行原子操作,標志當前線程完成任務。)
Interlocked.Increment(ref iCount); //完成的線程數和頁數一致時發出結束信號,標志本線程池中線程已全部執行結束。
if (iCount == PageCount) { Console.WriteLine("發出結束信號!"); eventX.Set(); } } /// <summary>
/// 具體某個公司信息的匹配 /// </summary>
/// <param name="url"></param>
private void GetDeatil(string url) { string htmlStr = TrimOther(new Common.HttpRequestHelper().getHTML(url + "shop/show.html")); UserInfo user = new UserInfo(); user.CompanyName = Regex.Match(htmlStr, "<h1[^>]+?>(?'CompanyName'.+?)</h1>", RegexOptions.Singleline | RegexOptions.IgnoreCase).Groups["CompanyName"].Value.Trim(); string pattern = string.Concat( @"<h5\s?[^>]+?>\s?" , @"<li\s?title=""(?'name'[^>]+?)"">\s?" , @"<span\s?title=""(?'ContactName'[^>]+?)"">([^<]+?)</span>\s?" , @"<span\s?title=""(?'ContactPost'[^>]+?)"">([^<]+?)</span>\s?" , @"</li>\s?" , @"</h5>\s?" , @"(?'textlist'(<h5><li\s+title=""[^>]+?"">[^>]+?</li></h5>)+)"); if (Regex.IsMatch(htmlStr, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline)) { Match mc = Regex.Match(htmlStr, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); user.ContactName = mc.Groups["ContactName"].Value; user.ContactPost = mc.Groups["ContactPost"].Value; htmlStr = mc.Groups["textlist"].Value; if (Regex.IsMatch(htmlStr, "title=\".+?\"", RegexOptions.IgnoreCase | RegexOptions.Singleline)) { MatchCollection mcdetail = Regex.Matches(htmlStr, "title=\"(?'text'.+?)\"", RegexOptions.IgnoreCase | RegexOptions.Singleline); foreach (var item in mcdetail) { string itemtext = item.ToString().Replace("title=", "").Replace("\"", "").Trim(); string itemname = itemtext.Substring(0, itemtext.IndexOf(":") + 1); switch (itemname) { case "電話:": user.FixedTelephone = itemtext.Replace(itemname, ""); break; case "手機:": user.Mobile = itemtext.Replace(itemname, ""); break; case "傳真:": user.Fax = itemtext.Replace(itemname, ""); break; default: ; break; } } } Console.WriteLine("=========" + user.CompanyName + "==========="); Console.WriteLine(user.ContactName + "|" + user.ContactPost + "|" + user.Mobile); } } private static object lockObject = new object(); private static volatile HuiCong _instance; public static HuiCong Instance { get { if (_instance == null) { lock (lockObject) { return _instance ?? (_instance = new HuiCong()); } } return _instance; } } }
好吧,感覺線程池解釋的不好,還沒理解透徹。再去學習學習。。
今日成語:溫故而知新,可以為師矣。
子曰:"溫新,可矣。"
孔子說:"溫習舊知識知道新的理解與體會,可以憑借(這一點)成為老師了。"
故:舊的 而:就 可:可以 以:憑借 為:成為
↓↓↓掃描下面的二維碼,關注我的公眾號