C#抓取数据、正则表达式+线程池初步运用

时间:2022-06-09 21:05:16

  去年底用 多线程+HtmlAgilityPack.dll 写了一个抓取“慧聪网” 公司信息的小程序,代码惨不忍赌。好在能抓到数据,速度也能让人忍受就很久没管了。

  最近这段时间把这个小程序发给同事看着玩,没想到他老感兴趣了。然后写了一个抓“新浪微博”个人资料的小程序,由于用正则表达式,代码精简不少,效率也很高,顿时觉得有种挫败感啊。

  于是不懂正则的我决定学习下正则,顺便学习一下线程池的用法。

  没有用正则和线程池之前,我的代码是这样的。

//下面这段代码使用HtmlAgilityPack写的,由于对它了解不深就写成下面这个鸟样子了.....
           foreach (HtmlNode node in GetSource.GetHtmlDocument(GetSource.TrimCode(htmlStr)).DocumentNode.ChildNodes[].ChildNodes)
{
if (node.InnerHtml.Trim() == "")
{
continue;
}
              //一个又一个点.....
url = node.ChildNodes[].ChildNodes[].ChildNodes[].ChildNodes[].ChildNodes[].Attributes["href"].Value;
CompanyInfo company = new CompanyInfo();
company.CompanyName = node.ChildNodes[].ChildNodes[].ChildNodes[].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, )));
if (Regex.IsMatch(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=页</span>)"))
{
pageHtml = Regex.Match(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=页</span>)").Value;
PageCount = FunLayer.Transform.Int(pageHtml, );
}
//提取公司名称
user.CompanyName = Regex.Match(htmlStr, "<h1[^>]+?>(?'CompanyName'.+?)</h1>", RegexOptions.Singleline | RegexOptions.IgnoreCase).Groups["CompanyName"].Value.Trim();

  比起那些一串点的用法,我更喜欢正则带来的便利和效率。

首先说下正则

  以下是基础语法...

.匹配除换行符以外的任意字符。(在.net里用 "RegexOptions.Singleline"可以改变(.)的含义,让它能匹配每一个字符)

\w匹配字母、数字、下划线和汉字。(试过匹配“汉字”,可就是没匹配出来。。。求解!!)

\s匹配空白符,它不是“&nbsp;”。(在得到网页源码后你会发现的)

上面是简单的“元字符”,更多元字符请百度...或者问大神...

接着介绍“重复”,实际应用中用的非常频繁。

* 重复匹配零次或更多次 、+ 重复一次或更多次。  两者的区别:+ 重复至少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等等。

  本文的方向不是深入讲解这一块,侧重讲解正则和线程池,抓取到数据是在这两项技术下的结果。被骗了....

  以下贴出部分代码,复制下来绝对能抓到“慧聪网”数据!

  有图有真相:

  C#抓取数据、正则表达式+线程池初步运用

     /// <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("&nbsp;","");
}
  

    static void Main(string[] args)
    {

       //设置同时处于活动状态的线程池的请求数目
   ThreadPool.SetMaxThreads(, );
//执行主要函数
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 = ;
/// <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, )));
if (Regex.IsMatch(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=页</span>)"))
{
pageHtml = Regex.Match(pageHtml, "(?<=<span class=\"total\">共)\\d+(?=页</span>)").Value;
PageCount = FunLayer.Transform.Int(pageHtml, );
}
for (int i = ; 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(, itemtext.IndexOf(":") + );
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;
}
} }

  好吧,感觉线程池解释的不好,还没理解透彻。再去学习学习。。

  今日成语:温故而知新,可以为师矣。

  子曰:"温新,可矣。"

  孔子说:"温习旧知识知道新的理解与体会,可以凭借(这一点)成为老师了。"

  故:旧的 而:就 可:可以 以:凭借 为:成为

       ↓↓↓扫描下面的二维码,关注我的公众号

C#抓取数据、正则表达式+线程池初步运用