基于C#的远程数据下载系统的设计与实现

摘要:该系统应用微软公司新开发的语言C # , 以正则表达式对字符串的过滤和获取为基础,实现对远程数据的抓取和下载。该系统通过远程页面的URL获取到该页面及与这个URL有关的其它页面的文本流,并对文本流进行分析和筛选,最后利用C#的ADO.NET对象将所需要的信息保存在SQLServer 2000数据库中。
关键字:C#语言,数据下载,正则表达式

0 引言
    C#是微软.NET战略中核心开发工具,是C语言系列中第一个面向组件的安全代码编程语言。同时,C#语言利用了.NET作为其强大的平台,使得它在Windows图形用户界面、ASP.NET Web应用、XML Web Service及ADO.NET数据库等方面有广泛的应用。
    C#是面向对象的程序设计语言,安全稳定,支持多线程,并有丰富的类库和详细的中英文帮助文档资料,这些都为利用C#开发本系统提供了基本的保障。如ADO.NET组件,为基于网络的可扩展的应用程序和服务提供数据访问服务;Windows表单组件,为开发人员提供了强大的Windwos应用程序模丰富的Windows用户接口。
    2005年十月份,我校师生在刘海涛校长的带动下,在远程服务器敏思博客平台开设了教研创博客群组,现共有组员117人,经过半年多的发展,现已积累文章近五千篇,评论近三万条,这些大部分都是师生共创的学习资源。为了让这些资源能更广泛的共享与应用,必须将这些数据保存在本校服务器的数据库中,所以本系统便在这样的背景下开发的。目前这个软件已经投入使用,定期将教研创群组的数据下载到本校服务器数据库中。下面就把这个系统简单介绍一下。
1 系统特点
   1、分栏下载,简单易用
   使用的时候,只要打开软件界面,在文本框中输入敏思博客群组的网址,再选择相关的栏目就可以自动的将该栏的所有文章下载到指定的数据库中了。在下载之前,软件程序会自动识别该群组所包含的栏目,并将栏目的名称和ID号添加到数据库中。
    2、下载数据完整且不重复
    这个软件通过一个网址就可以将该页面及与这个页面相关的页面上的所有数据下载回来,并自动筛选所需要的信息添加到数据库中,其中的原理就是利用正则表达式对页面中的HTML代码进行过滤,然后将有用的信息如文章标题、作者、发布时间、回复率和点击率分别储存在不同的数组中,最后循环写入指定的数据库中。软件工作时能根据页面中的HTML标记,不断缩小所要筛选内容的范围,最后精确的获取到所需要的信息。同时下载过程中,软件会根据内容里的链接在本机自动生成目录,并自动将文章中所包含的图片、声音等资料下载到指定的目录中。在文章写入数据库时,软件会根据文章的ID判断该文章在库中是否存在,如存在则不重复写入。
    3、软件开设了多个线程,加快了下载速度。
    为了避免下载过程中由于工作时间过长出现异常,故软件除了设置了分栏下载以外,还开设了多个线程同时下载,加快了下载的速度。
    4、软件为了避免下载数据有误,在多处进行了判断和异常处理,尽量将错误减到最少。
2 系统实现
    该系统主要是一个C/S端的的可执行软件。下载数据之前需要输入群组地址、服务器名称,数据库名称、数据库登陆的用户名和密码。下载之前可以直接读取数据库中已有群组的栏目,然后直接选择单个栏目下载该栏目内容,也可以要根据提供的网址直接获取该群组里栏目,然后再选择单个栏目下载该栏目内容或一次性将该群组的文章全部下载。每一条记录都包含了群组名称、栏目ID、栏目名称、文章ID、文章标题、文章内容等信息,这些信息在添加进数据之前系统会根据文章ID先判断该文章在数据库中是否已存在,如不存在,则保存到库中,如存在,则跳出循环,继续下载下一篇文章。当一个栏目的文章下载完毕下,则继续下载下一个栏目的文章,直到整个群组的文章全部下载完毕为止。
   1、开发流程
    该软件主要是面向(C/S)模型设计的,可以在任何一台电脑上运行,但要成功下载数据,必须保证这台上安装有SQL Server数据库,并导入指定的数据表及存储过程,并且能连接上要下载的群组页面。
    该软件主要分为几个模块,分别为获取群组模块、获取页面HTML模块、文件下载模块及一个贯穿整个过程的主模块。关系图如下:

    2、实现过程
    该软件主要采用Visual Studio.NET作为开发工具,Sql Server作为数据库,整个开发过程主要采用了C#里的正则表达式、ADO.NET、多线程及代理等技术。
    2.1 获取HTML
    在分析数据之前,必先获取到HTML源文件,这里根 据软件提供的URL地址作为参数,调用GetText(string UrlStr)函数,获取到该页面相应的HMTL源文件并以字符串的形式返回。
    根据函数参数字符串,创建一个新的URL对象
    Uri myUri =new Uri(@UrlStr);
    接着为以上指定的URL创建一个WebRequest对象,从WebRequest对象创建WebResponse实例,并设置WebRequest对象TimeOut属性为-1,即永不超时。
WebRequest myWebRequest= WebRequest.Create(myUri);
myWebRequest.Timeout = -1;
    WebResponse myWebResponse= myWebRequest.GetResponse();
    第三步即调用WebResponse对象的GetResponseStream方法,返回数据流ReceiveStream,并设置数据编码为gb2312,最后用所需要的字符编码为指定的数据流ReceiveStream初始化 StreamReader 类的新实例readStream,并读取数据流中全部内容到字符串变量OldText中
    Stream ReceiveStream = myWebResponse.GetResponseStream();
Encoding encode = System.Text.Encoding.GetEncoding("gb2312");
StreamReader readStream = new StreamReader( ReceiveStream, encode );
string OldText = readStream.ReadToEnd();
    第四步,关闭StreamReader、Stream及WebResponse对象实例,返回字符串OldText
    readStream.Close();
    ReceiveStream.Close();
    myWebResponse.Close();
    return OldText;
    2.1 分析HTML
    开发该软件的重点及难点就是如何在数千行的HTML源码中筛选出所需要的信息,然后再将这些信息分类整理,保存到相应的数据库中。这就充分利用C#中正则表达式技术及字符串处理技术。下面就如何获取文章标题、作者、发布时间、回复数及点击数为例来说明正则表达式在本软件中的应用。
    在获取群组文章信息时,本软件是采用分栏下载的形式,即先采用正则表达式筛选出该群组所有栏目的名称、ID号及访问地址等,然后再用GetText(string UrlStr)循环获取到每个栏目的HTML源文件,接着用正则表达式及字符串函数对这些源文件分类筛选,最后保存在相关的数组中。
    (1)获取栏目的名称、ID号及访问地址
    筛选栏目的名称、ID号及访问地址时,主要用到一个GetUrlByReg(string InputStr,string RegStr,int GroupInt)函数,函数有三个参数,第一个为输入的字符串,第二个是根据要筛选的字符串构造的正则表达式,第三个是指定返回的字符串数组匹配项的位置。在这里先用指定的正则表达式初始化一个Regex类的实例,再用Regex实例的Match()方法返回指定的字符串匹配的结果。同时这里初始化一个StringBuilder的实例,并将返回的字符串追加到该实例中,然后赋给字符串变量ReValue返回。
       Regex r = new Regex(ResStr,RegexOptions.IgnoreCase);
Match m = r.Match(InputStr);
StringBuilder SB = new StringBuilder();
while(m.Success)
{
SB.Append(m.Groups[GroupInt].ToString());
SB.Append("$");
m = m.NextMatch();
}
string ReValue = SB.ToString();
SB.Remove(0,SB.ToString().Length);
return ReValue;
     在主函数中,就可以使用上面提供的函数构造相关的正则表达式,并返回所需要的信息,如:
     string TotalUrl = GetUrlByReg
(ReceiveText,@"PageIndex=\d+|BlogCode=\w+|GroupCode=\w+|BlogColCode=\d+",0);
    //返回每个栏目访问地址的参数
     string TotalColName = GetUrlByReg(ReceiveText,@"(alt=)+(\S+)+(\>\<)",2);
    //返回每个栏目的名称
string TotalColID = GetUrlByReg(ReceiveText,@"(BlogColCode=)+(\S+)+(\>\<)",2);
    //返回每个栏目的ID号
    (2)获取文章的标题、作者、点击数、回复数、ID号和发表日期
获取到每个栏目的访问地址后,就可以以地址为参数,获取该栏目的HMTL源文件,然后再通过截取字符串的形式,获取到该栏目文章的总页数,如:
     int FirPagePoint = PageString.IndexOf('第')+1;
     int SecPagePoint = PageString.IndexOf('页');
     int TotalPage = Convert.ToInt32(PageString.Substring(FirPagePoint,SecPagePoint-FirPagePoint).Trim().Split('/')[1]);
     其中PageString是通过对该栏目的HMTL源文件筛选得来的,通过对字符串截取而缩小搜索的范围,从而能准确获取到文章的总页码。获取到总页码后,就可以循环获取每个页面的数据。下面主要是解释如何获取到文章标题、作者、点击数和回复数,共分五步进行。
     第一步,缩小HMTL源文件的搜索范围。下面首先用字符串处理方法IndexOf()和SubString()从该栏目的源文件InnerText中截取所需要的一部分HMTL源文件。
     int Begin2 = InnerText.IndexOf("<img src=\"../images/dot.gif\" border=\"0\">",Begin1);
     int End2 = InnerText.IndexOf("<!--Begin FCS: Blog.stdsub.TagCBlogLogPage -->");
     int StrLen = End2 - Begin2;
     string NewText = InnerText.Substring(Begin2,StrLen);
     第二步,构造正则表达式,去除多余的HTML源文件。如“<[^>]*”表示匹配“<”与“>”之间的所有内容。R.Replace(NewText,"$")就是表示将所匹配的内容替换成“$”。
     Regex R = new Regex(@"<[^>]*",RegexOptions.IgnoreCase);
     string RText = R.Replace(NewText,"$").Replace(">","");
     第三步,构造正则表达式,清除所有包括空格、制表符、换页符等空白字符。
     Regex R1 = new Regex(@"[\s]*",RegexOptions.IgnoreCase);
     string R1Text = R1.Replace(RText,"");
     第四步,构造正则表达式,将多个“$”替换成一个“$”,并清除头尾的“$”及源文件中的“$ ”,最后得到文章标题、作者、点击数和回复数分别以“$”为分隔符,保存在一个RRTextArr数组中。于是
     Regex RR = new Regex("[$]+",RegexOptions.IgnoreCase);
     string RRText = RR.Replace(R1Text,"$").TrimStart('$').TrimEnd('$').Replace("$ ","");
     string[] RRTextArr = RRText.Split('$');
     下面使用GetUrlByReg()获取到文章的ID号及发布时间,然后分别将其保存在TextIDArr和TextTimeArr数组中。其中“BlogLogCode=)+(\d+)”表示匹配“BlogLogCode=”后一个或多个数字字符,获取发表日期采用的正则表达式道理跟获取ID号的差不多,里面“/s”表示匹配一个空白字符,如“2006年4月22日 13:23”这样的日期格式便可以被匹配。
     string TextID = GetUrlByReg(NewText,@"(CommList.aspx\?BlogLogCode=)+(\d+)",2).TrimEnd('$');
     string TextTime = GetUrlByReg(NewText,@"(发表日期:)+(\d+(年)\d+(月)\d+(日)\s\d+(:)\d+)",2).TrimEnd('$');
     string[] TextIDArr = TextID.Split('$');
string[] TextTimeArr = TextTime.Split('$');
    (3)获取文章内容
    文章内容的获取主要使用了GetContent(string leftStr,string ContentID)这个函数,函数有两个参数,第一个就是文章地址前缀部分,第二个是文章的ID号,两者通过组合后便是这篇文章的访问地址了。对于文章内容的获取,主要是采用截取HTML源文件的方式,因为对于提取单条信息且信息前后有明显标记时,使用截取的方式是最后有效的方式,程序编写简单且处理速度快。
    因为在每一篇文章开始部分都有一个“id="bloglogcontent”的标记且在刚好第一次出现,故就可以用字符串处理方法IndexOf()截取内容开头部分,离每一篇文章内容结尾不远的地方都有一个“日志链接地址”的标记且在全文中是唯一的,故又可以初步取得内容的结尾点,初步取得内容TextContent1,接下来进一步缩小截取范围,因为所有文章的内容都是位于单元格“<td>”与“</td>”之间的,故对于文章的开头又可以以“<”为标记,精确的获取到内容的开头位置。对于文章的结尾部分可以分两种情况进行处理,一种是内容中没有含有表格,别一种是内容中含有表格,对于第一种情况,由于内容中没有含表格,原则上中间就不会出现“</td>”的标记,故只要以“</td>”为结尾的标记运用LastIndexOf(“</td>”)函数就可以获取到内容的结尾位置。对于含有表格的部分内容,先LastIndexOf(“</table>”)函数获取到内容中最后一个表格的结尾位置,然后再以这个位置为一段新字符串的开始位置,内容的结尾为这段新字符串的结尾位置,截取到这段新字符串如TextContent2,接着就在这段新字符串中获取“</td>”的开始位置,最后以这个位置为内容的结尾标记截取出一段字符串如TextContent3,那这这段字符串与最后一个表格的结尾位置前面那段字符相加起来就是整篇文章的内容部分了。
   (4)文件下载
    文件下载主要运用了一个DownFile(string img,string leftStr)的函数,函数有一两个参数,第一个是文件名称,另一个是文件地址的前缀部分,两部分通过组合后便是该文件的下载地址了。关于如何在文章内容中提取到文件的名称,本软件主要是运用了前面定义过的一个函数GetUrlByReg(),通过构建相关的正则的表达式来匹配内容中的要下载的文件名称,提取出要下载的文件名称。这里以图片文件下载为例,具体如下:
    string img = GetUrlByReg
   (TextContent,@"(IMG SRC=\"")+([.]*(([\/](\w+))*([.])(jpg|gif|bmp)))",2).TrimEnd('$').TrimStart('.');
   //返回内容中图片名称
    仔细分析以上的正则表达式就知道,该正则表达式共有三个匹配项,第一个就是“(IMG SRC=\"")”,其中这是一个固定的字符串匹配,第二个是“[.]*”,这个意思就是可以匹配零个或多个“.”,第三个是“(([\/](\w+))*([.])(jpg|gif|bmp))”,这个正则表达式前半部分“([\/](\w+))*”表示匹配多组一个“/”加一个或多个单词的字符串,如“/abc”,后半部分“([.])(jpg|gif|bmp)”表示匹配一个“.”加该文件扩展名。通过分析知道,如整个表达式可以匹配以下“SRC="../blogpath/UploadImgPath1/2006/03/1000353733.jpg"”的字符串。
     获取到文件的地址后,就构建一个WebClient的实例,然后再调用这个实例的DownloadFile方法,将数据下载到本地文件。具体以下:
   WebClient client = new WebClient();
   string imgfrom = leftImageUrl + imgArr[k].TrimStart('.');
    string imgto = Application.StartupPath+imgArr[k].TrimStart('.');
    if(File.Exists(imgto))
{
File.Delete(imgto);
}
string imgDir = imgto.Substring(0,imgto.LastIndexOf("/")).Replace("/","\\");
if(!Directory.Exists(imgDir))
{
Directory.CreateDirectory(imgDir);
}
client.DownloadFile(imgfrom,imgto);
    这里“imgfrom”表示远程文件的地址,“imgto”表示本地文件址。
  (5)保存数据
    数据下载完毕后,剩下的工作就是要将已下载的数据保存到相关的数据库中。这个工作主要是运用ADO.NET组件来完成的。ADO. NET 的数据库访问是通过被称为数据提供程序的软件模块进行的,. NET 框架有两个数据提供程序。一个是SQL Server.NET提供程序。它Microsoft SQLServer 数据库的接口, 不需任何非托管提供程序的帮助。另一个是OLEDB.NET提供程序,是通过OLEDB提供程序访问数据库的接口,许多非SQL 数据库都可以使用OLEDB 提供程序。本系统本使用的是SQL Server数据库,所以使用了SQL Server.NET提供程序。
……
SqlConnection Conn = new SqlConnection
("server="+ServerName+";database="+DataBaseName+";UID="+DataBaseUser+";password="+DataBasePass+";");  //连接数据库
string Sql_Str="Insert Into content
(contentID,ColID,ColName,GroupName,title,author,content,addTime,Hits,resay)  values
("+TextIDArr[i]+","+ColID1+",'"+ColName1+"','"+GroupName+"','"+title+"',
'"+author+"','"+TextContent+"','"+TextAddTime+"',"+Hits+","+Resay+")";
SqlCommand command = new SqlCommand(Sql_str,Conn);//初始化一个Command对象
Conn.Open(); //打开数据连接,将数据写入数据库中
command.ExecuteNonQuery();
command.Dispose();
Conn.Close();
   ……
3 结束语
    本文简单介绍了基于C#的远程数据下载系统的特点,并对系统的整个开发流程及实现方式作了详细的阐述。该系统除了能够下载敏思博客平台中教研创群组的文章以外,同样也可以下载该平台其他群组的数据,但系统还有一些局限性,比如只能下载敏思博客平台的资料而不能下载其他平台的资料,不过只要掌握了方法,应该也很容易的开发出适合其他平台所使用的系统。
posted on 2008-01-18 16:29  Ameng  阅读(446)  评论(0编辑  收藏  举报