姓名:吴晓春
联系电话:13862744018
电子信箱:wuxc@ntzx.net.cn
通讯地址:江苏省南通市中学堂街 9 号信息技术学科
邮编:226000
作者简介:男,教师,软件设计师。
在C#中用 WebClient 类编写整站下载软件
江苏省南通中学信息技术学科 吴晓春
[ 摘要 ] 在 VC#2005 环境下,开发一个整站下载程序。使用队列作为主要的数据结构,存放下载链
接并依次下载,直到完成。
[关键词] 队列、WebClient 类、异步调用、委托、多线程
编程思路
整站下载又称为离线浏览,如大名鼎鼎 WebZipt 可以下载整个站点的内容,来离线游览或研究。本人
试着写了一个整站下载程序,介绍给大家。 本程序用队列作为数据结构,存放下载链接。首先,将起始链
接置入队列;然后,依次下载队列中的每一项链接,保存到本地。若下载的是网页,就搜索其中包含超链
接,判断队列中是否包含该链接(防止循环链接),没有则加入队列。直到队列中全部链接下载完成。队列内
容及下载进度将在界面上显示,并统计下载的文件数和搜索到的链接数。
[ 程序示意 ]
下载队列
■■■■■■■■■■■■■■■■■■■■
headi
private void startdown()
下载 headi 指示的链接
下载项为网页时
private void spreaddelegate(……)
搜索链接,加入队列。
private void writelocal(……);网页存盘
mainWc.DownloadFile(……);其它下载直接存盘
[ 程序界面 ]
下面就程序中涉及到的技术如 Webclient 类下载数据、BeginInvoke 异步委托调用、在工作线程中更新
UI、正则表达式搜索超链接等逐个讲述,并给出完整代码。
WebClient 类
WebClient 类提供向 URI 标识的任何本地、Intranet 或 Internet 资源发送数据以及从这些资源接收数
据的公共方法。WebClient 类提供的下载数据的方法主要有:
DownloadData 从资源下载数据并返回字节数组
DownloadFile 从资源将数据下载到本地文件
DownloadString 从资源下载数据并字串
本文用到 WebClient.DownloadFile 方法和 WebClient. DownloadString 方法:
public void DownloadFile(string address, string fileName);
[参数]
address 从中下载数据的 URI。
fileName 要接收数据的本地文件的名称。
[异常]
WebExceptionaddress 指示的 URI 无效。
fileName 为空引用。
SecurityException 没有写入本地文件的权限。
[C# 代码片断]
WebClient mainWc = new WebClient();
mainWc.DownloadFile("http://www.china-askpro.com/catalog.shtml", "c:\\catalog.shtml");
//利用 webclien 实现下载功能
public String DownloadString ( string address);
参数 address 从中下载数据的 URI。
返回字串类型
异常 WebException address 指示的 URI 无效。
[C# 代码片断]
WebClient mainWc = new WebClient();
String downUrl="http://www.china-askpro.com/catalog.shtml" ;
String s0 = mainWc.DownloadString(downUrl); //下载网页文件,文件内容保存到字串变量 s0
异步委托
使用 .NET 异步编程,可以在主程序继续执行的同时对耗时较长的方法进行调用。例如,本文示例中
主程序完成下载,异步调用一个方法,该方法搜索下载链接,同时主程序将继续执行。
异步编程在 .NET Framework 的许多区域都支持的功能,这些区域包括:
文件 IO、流 IO、套接字 IO
网络:HTTP、TCP
远程处理信道(HTTP、TCP)和代理
使用 ASP.NET 创建的 XML Web services
异步委托
其它
本例中涉及到的是异步委托,异步委托提供以异步方式调用同步方法的能力。当同步调用一个委托时,
使用 Invoke 方法,直接在当前线程调用目标方法,当前线程等待,直到目标方法运行完毕。当异步调用一
个委托时, 使用 BeginInvoke 方法,公共语言运行库(CRL)将对调用请求进行排队并立即返回到调用方。
目标方法将在 CRL 线程池分配到的线程中执行。提交请求的原始线程(即调用 BeginInvoker 方法所在的线
程)自由地继续,与目标方法所在的线程并行执行。从而提高了程序运行效率,加快了程序下载速度。本文
示例中多处使用异步委托。
[ 异步委托示意 ]
下载线程
异步委托调用
搜索图片超链接线程
异步委托调用
异步委托调用
搜索超链接线程
网页存盘线程
[C# 代码片断]
private void startdown()
{
……
//下载线程
delegate void spreadDelegate(string downstr, string downhost, string urlspath, string regx);
//委托(又称代理)声明
spreadDelegate SDgate1 = new spreadDelegate(spreaddelegate);
SDgate1.BeginInvoke(s0, downHost, savePath,regx,null,null);
//异步委托调用方法 spreaddelegate,搜索图片链接。
spreadDelegate SDgate2 = new spreadDelegate(spreaddelegate);
IAsyncResult ar = SDgate2.BeginInvoke(s0, downHost, savePath, regx,null,null);
while(ar.IsCompleted==false)Thread.Sleep(10);
//异步委托调用方法 spreaddelegate 搜索网页超链接,返回值用于检测该异步委托调用产生的线程是否结束
spreadDelegate SDgate3 = new spreadDelegate(writelocal);
SDgate3.BeginInvoke(s0, title, locaPath + savePath, saveFile, null, null);
//异步委托调用方法 writelocal,保存文件。
……
}
//异步委托调用的两个方法。
private void spreaddelegate(string downstr, string downhost, string urlspath, string regx);
private void writelocal(string s0, string title, string localpath, string localfile);
正则表达式
正则表达式提供了一系列方法(标准、模式),能够高效地创建、比较和修改字符串,以及迅速地分
析大量文本和数据以搜索、移除和替换文本模式。
.NET 基础类库中包含有一个名字空间 System.Text.RegularExpression 和一系列可以充分发挥
规则表达式威力的类。
在名字空间中包含着如下类,它们是:
Capture: 包含一次匹配的结果;
CaptureCollection: Capture 的序列;
Group: 一次组记录的结果,由 Capture 继承而来;
Match: 一次表达式的匹配结果,由 Group 继承而来;
MatchCollection: Match 的一个序列;
MatchEvaluator: 执行替换操作时使用的代理;
Regex: 编译后的表达式的实例。
其中,Regex 类中还包含一些静态的方法:
Escape: 对字符串中的 regex 中的转义符进行转义;
IsMatch: 如果表达式在字符串中匹配,该方法返回一个布尔值;
Match: 返回 Match 的实例;
Matches: 返回一系列的 Match 的方法;
Replace: 用替换字符串替换匹配的表达式;
Split: 返回一系列由表达式决定的字符串;
Unescape:不对字符串中的转义字符转义。
[C# 代码片断]
string regx;
regx=@"
![]()
]+src=\s*(?:'(?
[^']+)'|""(?[^""]+)""|(?[^>\s]+))\s*[^>]*>";
//能与 HTML 中的 与 IMG 标签相匹配的 c#正则表达式
// regx =
@"]+href=\s*(?:'(?[^']+)'|""(?[^""]+)""|(?[^>\s]+))\s*[^>]*>\
s*(?.*?)";
Regex re0 = new Regex(regx, RegexOptions.IgnoreCase);
能与 HTML 中的 HREF 标签相匹配的 c#正则表达式
在 downstr 字串中匹配正则表达式 regx
MatchCollection
foreach (Match m0 in mc0)
mc0 = re0.Matches(downstr);//
//
循环处理每一匹配项
{
}
Boolean bl = true;
string dl = m0.Groups["target"].Value;
……
工作线程与 UI 更新
下如代码片断是单线程的,即所有代码都集中在界面线程(UI 线程)中,当程序执行耗时长的缓慢操作时,
界面几乎被“冻结”,程序更新界面不能立刻反映并且用户操作无法立刻得到响应。
[ c# 代码片断 ]
private void button1_Click(object sender, EventArgs e)
{
startdown(textBox1.Text); }
private void startdown()
……
{
while (headi < downlist.Count)// 耗时长的缓慢操作
{ ……
Treev1.Nodes.add(downstr); //界面不能立刻反映
}
为了解决上述问题,就要把耗时长的操作如磁盘操作、搜索链接等从 UI 线程中分离。方法是在另一线程中
执行这些工作,故称之谓工作线程,有时也称为辅助线程。
[ c# 代码片断 ]
private void button1_Click(object sender, EventArgs e)//UI 线程
{
}
Thread th = new Thread(new ThreadStart(startdown));//工作线程
th.Start();
private void startdown()
{……
while (headi < downlist.Count)
{
……
Treev1.Nodes.add(downstr);
//新问题:不能在工作线路中更新 UI 界面
}
}
上述代码,创建了工作线程 th 来执行耗时的缓慢操作,有效地与 UI 线程分开,以期达到界面快速响应。
但带来了另一问题,在工作线程更新 UI 线程的界面是不安全的,因而也是不被允许的。怎么办呢,简单地
说,可以利用窗体或控件的 Invoke 或 BeginInvoke 方法来携带委托以及参数列表,送回到 UI 线程并在 UI 线
程中调用委托,更新界面。改进后的代码如下:
[ c# 代码片断 ]
private void button1_Click(object sender, EventArgs e)
{
}
Thread th = new Thread(new ThreadStart(startdown));
th.Start();
private void startdown()
{ ……
while (headi < downlist.Count)
{
……
ShowUIDelegate showUI = new ShowUIDelegate(ShowUI);
this.BeginInvoke(showUI, new object[] { downUrl, -1 });
// this.BeginInvoke,可以把数据送回到 UI 线程,在 UI 线程中执行 ShowUI 方法,更新界面
}
}
delegate void ShowUIDelegate(string downstring, int i);
void ShowUI(string downstring, int i)
{……
treev1.Nodes.Add(downstring);
if (downstring == "red")
treev1.Nodes[i].ForeColor = Color.Red;//下载连接出错
else
treev1.Nodes[i].ForeColor = Color.Blue;//已经正常下载
treev1.Nodes[i].Checked = true;
label3.Text = i.ToString();//已经处理下载文件数
label2.Text = treev1.Nodes.Count.ToString();
}
[完整调用示意图]
UI 线程
th = new Thread(…)
下载线程 TH
Sdgate1.BeginInvoke(…)
搜索图片超链接线程
this.BeginInvoke
Sdgate2.BeginInvoke(…)
搜索超链接线程
SDgate3.BeginInvoke(…)
网页存盘线程
完整代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections;
using System.Threading;
namespace mydownload
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent(); }
private ArrayList downlist = new ArrayList();
//下载队列,存放下载链接
delegate
void ShowUIDelegate(string downstring, int i); //界面更新函数委托
void ShowUI(string downstring, int i) //界面更新函数
{
if (textBox1.InvokeRequired == false)
{
if (i == -1) treev1.Nodes.Add(downstring);//添加下载链接到 treeview
if (i != -1 || downstring == "red")
{
}
if (downstring == "red")
treev1.Nodes[i].ForeColor = Color.Red;//下载连接出错
else
treev1.Nodes[i].ForeColor = Color.Blue;//已经正常下载
treev1.Nodes[i].Checked = true;
label3.Text = (i + 1).ToString();
//已经处理下载文件数
label2.Text = treev1.Nodes.Count.ToString(); //已经搜索到的下载链接数
}
else
{
}
ShowUIDelegate showUI = new ShowUIDelegate(ShowUI);
BeginInvoke(showUI, new object[] { downstring, i });
}
//界面更新函数结束
delegate void DownDelegate();
private void startdown()
//下载函数
{
int headi = 0;
//指向下载队列头部,以获取下载链接
String savePath, saveFile; //保存下载文件的相对路径和文件名
String s0, title;
//下载到的网页文件内容字串,及其中的标题
String downUrl = textBox1.Text ;
//下载起始链接
String downHost = downUrl.Substring(0, downUrl.LastIndexOf("/")+1); //起始链接中的主机
String locaPath = "d:/myaskpro/";//保存到本地路径
downlist.Add(downUrl);
// 添加初始下载链接到队列
ShowUI( downUrl, -1 );
//更新界面
WebClient mainWc = new WebClient();
//利用 webclien 实现下载功能
while (headi < downlist.Count)
{
//扫描队列,逐个下载
downUrl = downlist[headi].ToString(); //当前下载项
ShowUI("", headi );
savePath = downUrl.Replace(downHost, "");
//获取相对路径
if (savePath.LastIndexOf("/") > 0)
//分离文件和路径
saveFile = savePath.Substring(savePath.LastIndexOf("/") + 1);
savePath = savePath.Substring(0, savePath.LastIndexOf("/"));
saveFile = savePath;
savePath = "";
{
}
else
{
}
try
{
if (downUrl.IndexOf("htm") > 0 || downUrl.IndexOf("html") > 0 || downUrl.IndexOf("shtml") > 0)
{
s0 = mainWc.DownloadString(downUrl); //下载网页文件,文件内容保存到字串变量 s0
s0 = s0.ToLower();
title = s0.Substring(s0.IndexOf("
") + 7, s0.IndexOf("") - s0.IndexOf("
") - 7);
if(s0.IndexOf("")>0&&s0.IndexOf("")>0)
s0 = s0.Substring(s0.IndexOf("") + 17, s0.IndexOf("") - s0.IndexOf("") - 17);
s0 = s0.Replace(downHost, "");//过滤相关信息
//异步委托调用 spreaddelegate 函数,搜索图片链接,并加入下载队列 downlist
string regx =
@"
]+src=\s*(?:'(?[^']+)'|""(?[^""]+)""|(?[^>\s]+))\s*[^>]*>";
spreadDelegate SDgate1 = new spreadDelegate(spreaddelegate);
SDgate1.BeginInvoke(s0, downHost, savePath,regx,null,null);
//或直接调用 spreaddelegate(s0, downHost, savePath, regx);
//异步委托调用 spreaddelegate 函数,搜索超链接,并加入下载队列 downlist
regx =
@"]+href=\s*(?:'(?[^']+)'|""(?[^""]+)""|(?[^>\s]+))\s*[^>]*>\s*(?.*?)
";
spreadDelegate SDgate2 = new spreadDelegate(spreaddelegate);
IAsyncResult ar = SDgate2.BeginInvoke(s0, downHost, savePath, regx,null,null);
//或直接调用 spreaddelegate(s0, downHost, savePath, regx);