多线程采集
原文发布时间为: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。非常郁闷!