• 多线程采集


    原文发布时间为:2010-05-08 —— 来源于本人的百度文章 [由搬家工具导入]

    这两天因为要从网上采集数据,就改造了下之前写的那个采集器(之前那个是单个线 程的,效率非常低,昨天改为多线程的了。),使用了ThreadPool(线程池)。感觉线程池还是相当的好用的。但同时也还有好多问题没有搞清楚。虽然 数据时顺利采下来了,但在接下来的日子里还是要好好把没弄清楚的搞清楚。今天就暂时先做下粗略的小结。

          采集器的界面如下,做的非常简单。

          采集器的原理说起来也非常的简单,首先根据Uri读取页面的HTML源代码,读取好页面之后,使用正则匹配,将你需要的内容扣出来写入数据库就OK了。

          读取网页的HTML源代码有好几种方式,可以使用不同的类,如WebClient,HttpRequest等等,我这次使用的是WebClient。具体代码如下:

            /// <summary>
    /// 获取页面的HTML代码
    /// </summary>
    /// <param name="uri">页面所在的网址</param>
    /// <returns>页面的HTML源代码</returns>
    public string GetHTMLCode(Uri uri) {
    WebClient webclient = new WebClient();
    webclient.Headers.Add("user-agent", "mozilla/5.0 (windows; u; windows nt 5.2; zh-cn; rv:1.9.1.3) gecko/20090824 firefox/3.5.3");
    try
    {
    Stream data = webclient.OpenRead(@uri); //将HTML源码读入data
    StreamReader reader = new StreamReader(data, Encoding.Default); //以默认编码从data中读入到reader
    string s = reader.ReadToEnd(); //读取完整的HTML源码到s中
    data.Close();
    reader.Close();
    return s;
    }
    catch (WebException ex)
    {
    return "";
    }
    catch (ArgumentNullException ex) {
    return "";
    }
    }

          通过调用这个方法,就可以实现将HTML源码写入字符串s中。下一步就是通过正则来筛选数据,代码如下:

            /// <summary>
    /// 通过正则截取需要的信息
    /// </summary>
    /// <param name="HTMLCode">页面的HTML源代码</param>
    /// <returns>ArrayList</returns>
    public ArrayList RegFetch(string HTMLCode) {
    try
    {
    ArrayList alist = new ArrayList();
    Regex r = new Regex("<li><a href="(.*?)">(.*?)</a>", RegexOptions.Compiled);
    Match m = r.Match(HTMLCode);
    while (m.Success)
    {
    for (int i = 1; i <= 2; i++)
    {
    Group g = m.Groups[i];
    CaptureCollection cc = g.Captures;
    for (int j = 0; j < cc.Count; j++)
    {
    Capture c = cc[j];
    alist.Add(c);
    }
    }
    m = m.NextMatch();
    }
    return alist;
    }
    catch {
    return null;
    }
    }

          通过调用这个匹配方法,就可以把超链接的地址及文字存入ArrayList中,下一步只需要将这些收集是数据入库即可(具体代码省略了)。

          做完以上这几步,我们其实已经实现了数据的采集,只不过现在这个采集还只是主线程在做,即单线程的采集。效率之低可想而知了。所以下面需要对代码稍加改 动,把单线程转为多线程。这里我使用ThreadPool线程池,这样比较省事,直接把任务丢给线程池,由线程池自动分配任务给空闲的线程。代码如下:

            /// <summary>
    /// 点击开始采集按钮
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btn_autostart_Click(object sender, EventArgs e)
    {
    try
    {
    ThreadPool.SetMaxThreads(100, 100); //设置最大线程数
    foreach (var i in lbox_urllist.Items)
    {
    ThreadPool.QueueUserWorkItem(new WaitCallback(AutoStartCollection), i);//线程池指定线程执行AutoStartCollection方法
    }
    }
    catch (ApplicationException ex)
    {
    MessageBox.Show(ex.Message);
    }
    catch (OutOfMemoryException ex)
    {
    MessageBox.Show(ex.Message);
    }
    catch (ArgumentNullException ex)
    {
    MessageBox.Show(ex.Message);
    }
    }
    /// <summary>
    /// AutoStart按钮点击
    /// </summary>
    /// <param name="Object"></param>
    private void AutoStartCollection(object Object)
    {
    Uri uri = new Uri(Object.ToString());
    string HTMLCode = cdobj.GetHTMLCode(uri);
    ArrayList alist = cdobj.RegFetch(HTMLCode);
    bool flag = cdobj.AutoSaveData(alist);
    if (flag)
    {
    this.Invoke(new Action(delegate()
    {
    this.lbox_success.Items.Add(uri);
    this.lb_success.Text = "( " + this.lbox_success.Items.Count.ToString() + "/" + this.lbox_urllist.Items.Count.ToString() + " )";
    }));
    }
    else
    {
    this.Invoke(new Action(delegate()
    {
    this.lbox_failure.Items.Add(uri);
    this.lb_failure.Text = "( " + this.lbox_failure.Items.Count.ToString() + "/" + this.lbox_urllist.Items.Count.ToString() + " )";
    }));
    }
    }

          这样就变成多线程的采集器了,当时在测试的时候发现了一个问题,我原本想没采完一张网页,那个成功列表的Listbox里面就要加一条。但是当时一运行就 报错,后来经过周哥提醒(在此表示感谢)发现是少了InVoke的调用。因为这涉及到不同线程间的访问,.NET本身是不允许执行线程去访问其他线程创建 的控件的,所以需要调用this.Invoke(new Action(delegate(){...}));这样的委托。而且特别强调一点,该方法只适用于.NET 3.5 如果是.NET 2.0的话就需要先定义委托,然后通过委托来调用。.NET 3.5中直接简化了。

          当时还有一个问题搞的不是很清楚,那就是多线程的线程最多能开多少?我在网上查了下,有些人说最多只能开64个,估计不止。如果有高手清楚的,还请赐教。 最后还想实现一个功能就是在执行采集的时候,底部能循环显示进度条。我的思路是开始执行的时候主线程开始调用显示进度条的方法,当线程池内的所有线程均完 成任务时通知主线程停止调用。不知道思路对不对,到现在都没做出来。

    PS. 数据库连接的是MySql,当时是用的参数传递的方式。用惯了SQL Server的参数传递方式,这次也理所当然的写成了@paramname,然后发现参数明明已经赋值却一直没法写入数据库。郁闷了好久,在网上搜了搜, 才发现,Mysql用的是跟Java那样的,用?来表示,所以应该写成?paramname。非常郁闷!

  • 相关阅读:
    [HDOJ1261]最少拦截系统
    Gym 100637F F. The Pool for Lucky Ones
    Codeforces Gym 100637A A. Nano alarm-clocks 前缀和处理
    HDU 1166 敌兵布阵 线段树
    【Tyvj1038】忠诚 线段树
    Codeforces Gym 100513G G. FacePalm Accounting
    Codeforces Round #313 (Div. 2)B.B. Gerald is into Art
    Codeforces Round #313 (Div. 2) D. Equivalent Strings
    Codeforces Round #313 (Div. 2) C. Gerald's Hexagon 数学
    Codeforces Round #313 (Div. 2) A. Currency System in Geraldion
  • 原文地址:https://www.cnblogs.com/handboy/p/7158399.html
Copyright © 2020-2023  润新知