C#实现网络爬虫
下面看看网络蜘蛛的原理:
网络蜘蛛即Web Spider,是一个很形象的名字。把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛。网络蜘蛛是 通过网页的链接地址来寻找网页,从 网站某一个页面(通常是首页)开始,读取网页的内容,找到在网页中的其它链接地址,然后通过这些链接地址寻找下一个网页,这样一直循环下去,直到把这个网 站所有的网页都抓取完为止。如果把整个互联网当成一个网站,那么网络蜘蛛就可以用这个原理把互联网上所有的网页都抓取下来。
对于搜索引擎来 说,要抓取互联网上所有的网页几乎是不可能的,从目前公布的数据来看,容量最大的搜索引擎也不过是抓取了整个网页数量的百分之四十左右。这 其中的原因一方面是抓取技术的瓶颈,无法遍历所有的网页,有许多网页无法从其它网页的链接中找到;另一个原因是存储技术和处理技术的问题,如果按照每个页 面的平均大小为20K计算(包含图片),100亿网页的容量是100×2000G字节,即使能够存储,下载也存在问题(按照一台机器每秒下载20K计算, 需要340台机器不停的下载一年时间,才能把所有网页下载完毕)。同时,由于数据量太大,在提供搜索时也会有效率方面的影响。因此,许多搜索引擎的网络蜘 蛛只是抓取那些重要的网页,而在抓取的时候评价重要性主要的依据是某个网页的链接深度。
在抓取网页的时候,网络蜘蛛一般有两种策略:广度优先和深度优先(如下图所示)。
广 度优先是指网络蜘蛛会先抓取起始网页中链接的所有网页,然后再选择其中 的一个链接网页,继续抓取在此网页中链接的所有网页。这是最常用的方式,因为这个方法可以让网络蜘蛛并行处理,提高其抓取速度。深度优先是指网络蜘蛛会从 起始页开始,一个链接一个链接跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。这个方法有个优点是网络蜘蛛在设计的时候比较容易。两种策 略的区别,下图的说明会更加明确。
由 于不可能抓取所有的网页,有些网络蜘蛛对一些不太重要的网站,设置了访问的层数。例如,在上图中,A为起始网页,属于0层,B、C、D、E、F属于第1 层,G、H属于第2层,I属于第3层。如果网络蜘蛛设置的访问层数为2的话,网页I是不会被访问到的。这也让有些网站上一部分网页能够在搜索引擎上搜索 到,另外一部分不能被搜索到。对于网站设计者来说,扁平化的网站结构设计有助于搜索引擎抓取其更多的网页。
网络蜘蛛在访问网站网页的时 候,经常会遇到加密数据和网页权限的问题,有些网页是需要会员权限才能访问。当然,网站的所有者可以通过协议让网络蜘蛛不去抓 取(下小节会介绍),但对于一些出售报告的网站,他们希望搜索引擎能搜索到他们的报告,但又不能完全免费的让搜索者查看,这样就需要给网络蜘蛛提供相应的 用户名和密码。网络蜘蛛可以通过所给的权限对这些网页进行网页抓取,从而提供搜索。而当搜索者点击查看该网页的时候,同样需要搜索者提供相应的权限验证。
网站与网络蜘蛛
网 络蜘蛛需要抓取网页,不同于一般的访问,如果控制不好,则会引起网站服务器负担过重。今年4月,淘宝网就因为雅虎搜索引擎的网络蜘蛛抓取其数据引起淘宝网服务器的不稳定。网站是否就无法和网络蜘蛛交流呢?其实不然,有多种方法可以让网站和网络蜘蛛进行交 流。一方面让网站管理员了解网络蜘蛛都来自哪儿,做了些什么,另一方面也告诉网络蜘蛛哪些网页不应该抓取,哪些网页应该更新。
每个网络蜘蛛都有 自己的名字,在抓取网页的时候,都会向网站标明自己的身份。网络蜘蛛在抓取网页的时候会发送一个请求,这个请求中就有一个字段为User -agent,用于标识此网络蜘蛛的身份。例如Google网络蜘蛛的标识为GoogleBot,Baidu网络蜘蛛的标识为BaiDuSpider, Yahoo网络蜘蛛的标识为Inktomi Slurp。如果在网站上有访问日志记录,网站管理员就能知道,哪些搜索引擎的网络蜘蛛过来过,什么时候过来的,以及读了多少数据等等。如果网站管理员发 现某个蜘蛛有问题,就通过其标识来和其所有者联系。下面是博客中国(blogchina.com)2004年5月15日的搜索引擎访问日志:
网 络蜘蛛进入一个网站,一般会访问一个特殊的文本文件Robots.txt,这个文件一般放在网站服务器的根目录下, 如:http://www.blogchina.com/robots.txt。 网站管理员可以通过robots.txt来定义哪些目录网络蜘蛛不能访问,或者哪些目录对于某些特定的网络蜘蛛不能访问。例如有些网站的可执行文件目录和 临时文件目录不希望被搜索引擎搜索到,那么网站管理员就可以把这些目录定义为拒绝访问目录。Robots.txt语法很简单,例如如果对目录没有任何限 制,可以用以下两行来描述:
User-agent: *
Disallow:
当然,Robots.txt只是一个协议,如果网络蜘蛛的设计者不遵循这个协议,网站管理员也无法阻止网络蜘蛛对于某些页面的访问,但一般的网络蜘蛛都会遵循这些协议,而且网站管理员还可以通过其它方式来拒绝网络蜘蛛对某些网页的抓取。
网络蜘蛛在下载网页的时候,会去识别网页的HTML代码,在其代码的部分,会有META标识。通过这些标识,可以告诉网络蜘蛛本网页是否需要被抓取,还可 以告诉网络蜘蛛本网页中的链接是否需要被继续跟踪。例如:表示本网页不需要被抓取,但是网页内的链接需要被跟踪。
关于Robots.txt的语法和META Tag语法,有兴趣的读者查看文献[4]
现 在一般的网站都希望搜索引擎能更全面的抓取自己网站的网页,因为这样可以让更多的访问者能通过搜索引擎找到此网站。为了让本网站的网页更全面被抓取到, 网站管理员可以建立一个网站地图,即Site Map。许多网络蜘蛛会把sitemap.htm文件作为一个网站网页爬取的入口,网站管理员可以把网站内部所有网页的链接放在这个文件里面,那么网络蜘 蛛可以很方便的把整个网站抓取下来,避免遗漏某些网页,也会减小对网站服务器的负担。
内容提取
搜 索引擎建立网页索引,处理的对象是文本文件。对于网络蜘蛛来说,抓取下来网页包括各种格式,包括html、图片、doc、pdf、多媒体、动态网页及其 它格式等。这些文件抓取下来后,需要把这些文件中的文本信息提取出来。准确提取这些文档的信息,一方面对搜索引擎的搜索准确性有重要作用,另一方面对于网 络蜘蛛正确跟踪其它链接有一定影响。
对于doc、pdf等文档,这种由专业厂商提供的软件生成的文档,厂商都会提供相应的文本提取接口。
网络蜘蛛只需要调用这些插件的接口,就可以轻松的提取文档中的文本信息和文件其它相关的信息。
HTML 等文档不一样,HTML有一套自己的语法,通过不同的命令标识符来表示不同的字体、颜色、位置等版式,如:、、等, 提取文本信息时需要把这些标识符都过滤掉。过滤标识符并非难事,因为这些标识符都有一定的规则,只要按照不同的标识符取得相应的信息即可。但在识别这些信 息的时候,需要同步记录许多版式信息,例如文字的字体大小、是否是标题、是否是加粗显示、是否是页面的关键词等,这些信息有助于计算单词在网页中的重要程 度。同时,对于HTML网页来说,除了标题和正文以外,会有许多广告链接以及公共的频道链接,这些链接和文本正文一点关系也没有,在提取网页内容的时候, 也需要过滤这些无用的链接。例如某个网站有“产品介绍”频道,因为导航条在网站内每个网页都有,若不过滤导航条链接,在搜索“产品介绍”的时候,则网站内 每个网页都会搜索到,无疑会带来大量垃圾信息。过滤这些无效链接需要统计大量的网页结构规律,抽取一些共性,统一过滤;对于一些重要而结果特殊的网站,还 需要个别处理。这就需要网络蜘蛛的设计有一定的扩展性。
对于多媒体、图片等文件,一般是通过链接的锚文本(即,链接文本)和相关的文件 注释来判断这些文件的内容。例如有一个链接文字为“张曼玉照片”,其链接指 向一张bmp格式的图片,那么网络蜘蛛就知道这张图片的内容是“张曼玉的照片”。这样,在搜索“张曼玉”和“照片”的时候都能让搜索引擎找到这张图片。另 外,许多多媒体文件中有文件属性,考虑这些属性也可以更好的了解文件的内容。
动态网页一直是网络蜘蛛面临的难题。所谓动态网页,是相对 于静态网页而言,是由程序自动生成的页面,这样的好处是可以快速统一更改网页风格,也可以减少网 页所占服务器的空间,但同样给网络蜘蛛的抓取带来一些麻烦。由于开发语言不断的增多,动态网页的类型也越来越多,如:asp、jsp、php等。这些类型 的网页对于网络蜘蛛来说,可能还稍微容易一些。网络蜘蛛比较难于处理的是一些脚本语言(如VBScript和JavaScript)生成的网页,如果要完 善的处理好这些网页,网络蜘蛛需要有自己的脚本解释程序。对于许多数据是放在数据库的网站,需要通过本网站的数据库搜索才能获得信息,这些给网络蜘蛛的抓 取带来很大的困难。对于这类网站,如果网站设计者希望这些数据能被搜索引擎搜索,则需要提供一种可以遍历整个数据库内容的方法。
对于网页内容的提取,一直是网络蜘蛛中重要的技术。整个系统一般采用插件的形式,通过一个插件管理服务程序,遇到不同格式的网页采用不同的插件处理。这种 方式的好处在于扩充性好,以后每发现一种新的类型,就可以把其处理方式做成一个插件补充到插件管理服务程序之中。
更新周期
由于网站的内容经常在变化,因此网络蜘蛛也需不断的更新其抓取网页的内容,这就需要网络蜘蛛按照一定的周期去扫描网站,查看哪些页面是需要更新的页面,哪些页面是新增页面,哪些页面是已经过期的死链接。
搜 索引擎的更新周期对搜索引擎搜索的查全率有很大影响。如果更新周期太长,则总会有一部分新生成的网页搜索不到;周期过短,技术实现会有一定难度,而且会 对带宽、服务器的资源都有浪费。搜索引擎的网络蜘蛛并不是所有的网站都采用同一个周期进行更新,对于一些重要的更新量大的网站,更新的周期短,如有些新闻 网站,几个小时就更新一次;相反对于一些不重要的网站,更新的周期就长,可能一两个月才更新一次。
一般来说,网络蜘蛛在更新网站内容的时候,不用把网站网页重新抓取一遍,对于大部分的网页,只需要判断网页的属性(主要是日期),把得到的属性和上次抓取的属性相比较,如果一样则不用更新。
文章虽然老了一点,但是这些内容相信是有用的!
用C#来构建蜘蛛程序,看看最简单的程序:
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
2. 获取其他网站网页内容的关键代码
WebResponse response = request.GetResponse();
StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("gb2312")); //reader.ReadToEnd() 表示取得网页的源码
TextBox1.Text = reader.ReadToEnd();
3. 获取其他网站网页源码之后通过{正则表达式}帅选有用信息
foreach (Match NextMatch in TitleMatchs) {
s += "<br>" + NextMatch.Groups[1].Value;
TextBox1.Text += "\n" + NextMatch.Groups[1].Value;
}
RegexOptions.IgnoreCase: 表示不区分大小写, 一般网站源码大小写不敏感所以取消之.
RegexOptions.Multiline: 表示对多行内容进行帅选.
数据采集函数c#版本,网页抓取函数
/// 传入URL返回网页的html代码
/// </summary>
/// <param name="Url">网址 如http://www.aligong.com</param>
/// <returns>返回页面的源代码</returns>
public static string GetUrltoHtml(string Url, Encoding encode)
{
try
{
//构造httpwebrequest对象,注意,这里要用Create而不是new
HttpWebRequest wReq = (HttpWebRequest)WebRequest.Create(Url);
//伪造浏览器数据,避免被防采集程序过滤
wReq.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322; .NET CLR 2.0.50215; CrazyCoder.cn;www.aligong.com)";
//注意,为了更全面,可以加上如下一行,避开ASP常用的POST检查
wReq.Referer = "http://www.aligong.com/";//您可以将这里替换成您要采集页面的主页
HttpWebResponse wResp = wReq.GetResponse() as HttpWebResponse;
// 获取输入流
System.IO.Stream respStream = wResp.GetResponseStream();
System.IO.StreamReader reader = new System.IO.StreamReader(respStream, encode);
string content = reader.ReadToEnd();
reader.Close();
reader.Dispose();
return content;
}
catch (System.Exception ex)
{
}
return "";
}
首先将网页内容整个抓取下来,数据放在byte[]中(网络上传输时形式是byte),进一步转化为String,以便于对其操作,实例如下:
{
if (url == null || url.Trim() == "")
return null;
WebClient wc = new WebClient();//定义
wc.Credentials = CredentialCache.DefaultCredentials;
Byte[] pageData = wc.DownloadData(url);
return Encoding.Default.GetString(pageData);//.ASCII.GetString
}
上面只是简单地获取网页数据,但网页中还存在编码问题,这时候为了避免乱码问题,就需要进行处理了,代码如下:
//url是要访问的网站地址,charSet是目标网页的编码,如果传入的是null或者"",那就自动分析网页的编码
{
string strWebData = string.Empty;
if (url != null || url.Trim() != "")
{
WebClient myWebClient = new WebClient();
//创建WebClient实例myWebClient
// 需要注意的:
//有的网页可能下不下来,有种种原因比如需要cookie,编码问题等等
//这是就要具体问题具体分析比如在头部加入cookie
// webclient.Headers.Add("Cookie", cookie);
//这样可能需要一些重载方法。根据需要写就可以了
//获取或设置用于对向 Internet 资源的请求进行身份验证的网络凭据。
myWebClient.Credentials = CredentialCache.DefaultCredentials;
//如果服务器要验证用户名,密码
//NetworkCredential mycred = new NetworkCredential(struser, strpassword);
//myWebClient.Credentials = mycred;
//从资源下载数据并返回字节数组。(加@是因为网址中间有"/"符号)
byte[] myDataBuffer = myWebClient.DownloadData(url);
strWebData = Encoding.Default.GetString(myDataBuffer);
//获取网页字符编码描述信息
Match charSetMatch = Regex.Match(strWebData, "<meta([^<]*)charset=([^<]*)\"", RegexOptions.IgnoreCase | RegexOptions.Multiline);
string webCharSet = charSetMatch.Groups[2].Value;
if (charSet == null || charSet == "")
{
//如果未获取到编码,则设置默认编码
if (webCharSet == null || webCharSet == "")
{
charSet = "UTF-8";
}
else
{
charSet = webCharSet;
}
}
if (charSet != null && charSet != "" && Encoding.GetEncoding(charSet) != Encoding.Default)
{
strWebData = Encoding.GetEncoding(charSet).GetString(myDataBuffer);
}
}
return strWebData;
}
得到了数据的字符串形式,然后可以对网页进行解析了(其实就是对字符串的各种操作和正则表达式的应用):
// 解析页面,查找链接
// 此处尚需扩展,还有某些形式的链接不被识别
MatchCollection matches = new Regex(strRef).Matches(strResponse);
strStatus += "找到: "+matches.Count+" 个链接\r\n";
上面的例子将网页中的链接解析出来,strRef变量表示了正则表达式的模式,变量matches表示符合匹配的项目的集合,后面的 Regex(strRef).Matches(strResponse)就是创建正则规则使得strResponse里符合strRef模式的字符串都返 回。然后调用matches的变量就可以取得各种信息了。
当然,这里只能识别一些基本的链接形式,像script中的链接和一些不带“”的链接都没有被支持,这个的扩展还是比较简单的。
常用的的解析还有以下几种:
//获取标题
title = TitleMatch.Groups[1].Value;
//获取描述信息
//去除Html标签
{
Regex objRegExp = new Regex("<(.|\n)+?>");
string strOutput = objRegExp.Replace(strHtml, "");
strOutput = strOutput.Replace("<", "<");
strOutput = strOutput.Replace(">", ">");
return strOutput;
}
有些例外会使得去除不干净,所以建议连续两次转化。这样将Html标签转化为了空格。太多连续的空格会影响之后对字符串的操作。所以再加入这样的语句:
//把所有空格变为一个空格
wordsOnly = r.Replace(strResponse, " ");
wordsOnly.Trim();
写的不简单,但我还可以看的明白,注意using System.Text;
using System.Text.RegularExpressions;要写上
实际应用中:我做测试,获取一个区的气象部门的天气预报情况
{
if (url == null || url.Trim() == "")
return null;
WebClient wc = new WebClient();
wc.Credentials = CredentialCache.DefaultCredentials;
Byte[] pageData = wc.DownloadData(url);
return Encoding.Default.GetString(pageData);//.ASCII.GetString
}
protected void Button1_Click(object sender, EventArgs e)
{
string strResponse = GetPageData(TextBox1 .Text );
string strRef = @"(href|HREF|src|SRC|action|ACTION|Action)[ ]*=[ ]*[""'][^""'#>]+[""']";
MatchCollection matches = new Regex(strRef).Matches(strResponse);//在strResponse匹配的字符串
TextBox2.Text = "找到: " + matches.Count + " 个链接\r\n";
//以上为仿照学习例子,测试是否成功,果然成功,下面为应用
string strRef2 = @"MARQUEE";//定义关键字符串,我原来在要获得数据的网页上看到的关键字,看html源就行
TextBox3.Text = strResponse.Substring(strResponse.IndexOf(strRef2,2800)+120, 135).ToString();
//strResponse字符串中截取从strResponse.IndexOf(strRef2,2800)+120开始的 135个字符。
}
测试:我在TextBox1 .Text 中输入:http://tmall.aligong.com/,那么,立刻可以获得天气情况的报道文字了,这就是最基本的c#抓取类了。
最后还需要小小修改哦。因为有些时候在字符不确定性就要做调整,譬如该天气情况,有时候没有这么多字符显示,你硬要显示,则会出错哦。我的源码段
//抓天气情况
string strRef2 = @"MARQUEE";
string str_last_index = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, 70).Trim().ToString();
if (str_last_index.IndexOf('时') > 1)//091110发现始终不能解决两个 '时' 出现的问题,所以10行后有修改正版
{
get_weathe = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, str_last_index.IndexOf('时') + 2).Trim().ToString();//字符串从IndexOf(strRef2, 2800) + 113开始,'时'为标志结束
}
else
{
get_weathe = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, 60).Trim().ToString();
}
get_weathe为全局变量,到时候就可以在前台用javascript调用了。ok
091110更新为
{
if (str_last_index.IndexOf('时') < 50)//判断是否 '时' 在字符串中 大于1索引,但又不止一个 '时'
{
get_weathe = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, str_last_index.LastIndexOf ('时') + 10).Trim().ToString();//字符串从IndexOf(strRef2, 2800) + 113开始,'时'为标志结束
}
else
{
get_weathe = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, str_last_index.IndexOf('时') + 2).Trim().ToString();//字符串从IndexOf(strRef2, 2800) + 113开始,'时'为标志结束
}
}
else
{
get_weathe = strResponse.Substring(strResponse.IndexOf(strRef2, 2800) + 113, 60).Trim().ToString();
}
当然,我们还可以抓取某网站数据的变化更新,这要用到output&&input 数据库.
该例子的关键还是:对字符串的各种操作和正则表达式的应用
简单介绍一下WebClient:
WebClient 类提供向 URI 标识的任何本地、Intranet 或 Internet 资源发送数据以及从这些资源接收数据的公共方法。
WebClient 类使用 WebRequest 类提供对资源的访问。
WebClient 实例可以通过任何已向 WebRequest.RegisterPrefix 方法注册的 WebRequest 子代访问数据。
注意:默认情况下,.NET Framework 支持以 http:、https:、ftp:、和 file: 方案标识符开头的 URI。
下面描述从资源下载数据的 WebClient 方法:
OpenRead从资源以 Stream 的形式返回数据。
OpenReadAsync在不阻止调用线程的情况下,从资源返回数据。
DownloadData从资源下载数据并返回 Byte 数组。
DownloadDataAsync在不阻止调用线程的情况下,从资源下载数据并返回 Byte 数组。
DownloadFile从资源将数据下载到本地文件。
DownloadFileAsync在不阻止调用线程的情况下,将数据从资源下载到本地文件。
DownloadString从资源下载 String 并返回 String。
DownloadStringAsync在不阻止调用线程的情况下,从资源下载 String。
您可以使用 CancelAsync 方法取消尚未完成的异步操作。
默认情况下,WebClient 实例不发送可选的 HTTP 报头。如果您的请求需要可选报头,必须将该报头添加到 Headers 集合。
例如,要在响应中保留查询,必须添加用户代理报头。此外,如果用户代理标头丢失,服务器可能返回 500(内部服务器错误)。
在 WebClient 实例中,AllowAutoRedirect 设置为 true。
给继承者的说明 派生类应调用 WebClient 的基类实现,以确保派生类按预期方式工作。
例如:
首先我们每个人都想查对方的手机所在地及属于什么类型卡,那我们利用C#封装的类 WebClient,NameValueCollection,Regex等分别属于的命名空间是using System.Net、using System.Text、using System.Collections.Specialized、using System.Text.RegularExpressions。
在Vs2010环境下测试,下面代码测试如下:
View Code
{
WebClient wb = new WebClient();
NameValueCollection myNameValueCollection = new NameValueCollection();
myNameValueCollection.Add("mobile", "13777483912");
myNameValueCollection.Add("action", "mobile");
byte[] pagedata = wb.UploadValues(http://www.divmy.com/, myNameValueCollection);
string result = Encoding.Default.GetString(pagedata);
string pat = "tdc2>([^<]*)</TD>";
Regex r = new Regex(pat, RegexOptions.IgnoreCase);
Match m = r.Match(result);
string[] strInfo = new string[3] { "", "", "" };
int i = 0;
while (m.Success)
{
if (i < strInfo.Length)
{
strInfo[i] = m.ToString().Substring(5);
}
m = m.NextMatch();
i++;
}
string a = strInfo[0].ToString();
string g = strInfo[1].ToString();
string f = strInfo[2].ToString();
}