利用C#實現(xiàn)網(wǎng)絡爬蟲
網(wǎng)絡爬蟲在信息檢索與處理中有很大的作用,是收集網(wǎng)絡信息的重要工具。
接下來就介紹一下爬蟲的簡單實現(xiàn)。
爬蟲的工作流程如下
爬蟲自指定的URL地址開始下載網(wǎng)絡資源,直到該地址和所有子地址的指定資源都下載完畢為止。
下面開始逐步分析爬蟲的實現(xiàn)。
1. 待下載集合與已下載集合
為了保存需要下載的URL,同時防止重復下載,我們需要分別用了兩個集合來存放將要下載的URL和已經(jīng)下載的URL。
因為在保存URL的同時需要保存與URL相關的一些其他信息,如深度,所以這里我采用了Dictionary來存放這些URL。
具體類型是Dictionary<string, int> 其中string是Url字符串,int是該Url相對于基URL的深度。
每次開始時都檢查未下載的集合,如果已經(jīng)為空,說明已經(jīng)下載完畢;如果還有URL,那么就取出第一個URL加入到已下載的集合中,并且下載這個URL的資源。
2. HTTP請求和響應
C#已經(jīng)有封裝好的HTTP請求和響應的類HttpWebRequest和HttpWebResponse,所以實現(xiàn)起來方便不少。
為了提高下載的效率,我們可以用多個請求并發(fā)的方式同時下載多個URL的資源,一種簡單的做法是采用異步請求的方法。
控制并發(fā)的數(shù)量可以用如下方法實現(xiàn)
private void DispatchWork()
{
if (_stop) //判斷是否中止下載
{
return;
}
for (int i = 0; i < _reqCount; i++)
{
if (!_reqsBusy[i]) //判斷此編號的工作實例是否空閑
{
RequestResource(i); //讓此工作實例請求資源
}
}
}
由于沒有顯式開新線程,所以用一個工作實例來表示一個邏輯工作線程
private bool[] _reqsBusy = null; //每個元素代表一個工作實例是否正在工作 private int _reqCount = 4; //工作實例的數(shù)量
每次一個工作實例完成工作,相應的_reqsBusy就設為false,并調(diào)用DispatchWork,那么DispatchWork就能給空閑的實例分配新任務了。
接下來是發(fā)送請求
private void RequestResource(int index)
{
int depth;
string url = "";
try
{
lock (_locker)
{
if (_urlsUnload.Count <= 0) //判斷是否還有未下載的URL
{
_workingSignals.FinishWorking(index); //設置工作實例的狀態(tài)為Finished
return;
}
_reqsBusy[index] = true;
_workingSignals.StartWorking(index); //設置工作狀態(tài)為Working
depth = _urlsUnload.First().Value; //取出第一個未下載的URL
url = _urlsUnload.First().Key;
_urlsLoaded.Add(url, depth); //把該URL加入到已下載里
_urlsUnload.Remove(url); //把該URL從未下載中移除
}
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.Method = _method; //請求方法
req.Accept = _accept; //接受的內(nèi)容
req.UserAgent = _userAgent; //用戶代理
RequestState rs = new RequestState(req, url, depth, index); //回調(diào)方法的參數(shù)
var result = req.BeginGetResponse(new AsyncCallback(ReceivedResource), rs); //異步請求
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, //注冊超時處理方法
TimeoutCallback, rs, _maxTime, true);
}
catch (WebException we)
{
MessageBox.Show("RequestResource " + we.Message + url + we.Status);
}
}
第7行為了保證多個任務并發(fā)時的同步,加上了互斥鎖。_locker是一個Object類型的成員變量。
第9行判斷未下載集合是否為空,如果為空就把當前工作實例狀態(tài)設為Finished;如果非空則設為Working并取出一個URL開始下載。當所有工作實例都為Finished的時候,說明下載已經(jīng)完成。由于每次下載完一個URL后都調(diào)用DispatchWork,所以可能激活其他的Finished工作實例重新開始工作。
第26行的請求的額外信息在異步請求的回調(diào)方法作為參數(shù)傳入,之后還會提到。
第27行開始異步請求,這里需要傳入一個回調(diào)方法作為響應請求時的處理,同時傳入回調(diào)方法的參數(shù)。
第28行給該異步請求注冊一個超時處理方法TimeoutCallback,最大等待時間是_maxTime,且只處理一次超時,并傳入請求的額外信息作為回調(diào)方法的參數(shù)。
RequestState的定義是
class RequestState
{
private const int BUFFER_SIZE = 131072; //接收數(shù)據(jù)包的空間大小
private byte[] _data = new byte[BUFFER_SIZE]; //接收數(shù)據(jù)包的buffer
private StringBuilder _sb = new StringBuilder(); //存放所有接收到的字符
public HttpWebRequest Req { get; private set; } //請求
public string Url { get; private set; } //請求的URL
public int Depth { get; private set; } //此次請求的相對深度
public int Index { get; private set; } //工作實例的編號
public Stream ResStream { get; set; } //接收數(shù)據(jù)流
public StringBuilder Html
{
get
{
return _sb;
}
}
public byte[] Data
{
get
{
return _data;
}
}
public int BufferSize
{
get
{
return BUFFER_SIZE;
}
}
public RequestState(HttpWebRequest req, string url, int depth, int index)
{
Req = req;
Url = url;
Depth = depth;
Index = index;
}
}
TimeoutCallback的定義是
private void TimeoutCallback(object state, bool timedOut)
{
if (timedOut) //判斷是否是超時
{
RequestState rs = state as RequestState;
if (rs != null)
{
rs.Req.Abort(); //撤銷請求
}
_reqsBusy[rs.Index] = false; //重置工作狀態(tài)
DispatchWork(); //分配新任務
}
}
接下來就是要處理請求的響應了
private void ReceivedResource(IAsyncResult ar)
{
RequestState rs = (RequestState)ar.AsyncState; //得到請求時傳入的參數(shù)
HttpWebRequest req = rs.Req;
string url = rs.Url;
try
{
HttpWebResponse res = (HttpWebResponse)req.EndGetResponse(ar); //獲取響應
if (_stop) //判斷是否中止下載
{
res.Close();
req.Abort();
return;
}
if (res != null && res.StatusCode == HttpStatusCode.OK) //判斷是否成功獲取響應
{
Stream resStream = res.GetResponseStream(); //得到資源流
rs.ResStream = resStream;
var result = resStream.BeginRead(rs.Data, 0, rs.BufferSize, //異步請求讀取數(shù)據(jù)
new AsyncCallback(ReceivedData), rs);
}
else //響應失敗
{
res.Close();
rs.Req.Abort();
_reqsBusy[rs.Index] = false; //重置工作狀態(tài)
DispatchWork(); //分配新任務
}
}
catch (WebException we)
{
MessageBox.Show("ReceivedResource " + we.Message + url + we.Status);
}
}
第19行這里采用了異步的方法來讀數(shù)據(jù)流是因為我們之前采用了異步的方式請求,不然的話不能夠正常的接收數(shù)據(jù)。
該異步讀取的方式是按包來讀取的,所以一旦接收到一個包就會調(diào)用傳入的回調(diào)方法ReceivedData,然后在該方法中處理收到的數(shù)據(jù)。
該方法同時傳入了接收數(shù)據(jù)的空間rs.Data和空間的大小rs.BufferSize。
接下來是接收數(shù)據(jù)和處理
private void ReceivedData(IAsyncResult ar)
{
RequestState rs = (RequestState)ar.AsyncState; //獲取參數(shù)
HttpWebRequest req = rs.Req;
Stream resStream = rs.ResStream;
string url = rs.Url;
int depth = rs.Depth;
string html = null;
int index = rs.Index;
int read = 0;
try
{
read = resStream.EndRead(ar); //獲得數(shù)據(jù)讀取結果
if (_stop)//判斷是否中止下載
{
rs.ResStream.Close();
req.Abort();
return;
}
if (read > 0)
{
MemoryStream ms = new MemoryStream(rs.Data, 0, read); //利用獲得的數(shù)據(jù)創(chuàng)建內(nèi)存流
StreamReader reader = new StreamReader(ms, _encoding);
string str = reader.ReadToEnd(); //讀取所有字符
rs.Html.Append(str); // 添加到之前的末尾
var result = resStream.BeginRead(rs.Data, 0, rs.BufferSize, //再次異步請求讀取數(shù)據(jù)
new AsyncCallback(ReceivedData), rs);
return;
}
html = rs.Html.ToString();
SaveContents(html, url); //保存到本地
string[] links = GetLinks(html); //獲取頁面中的鏈接
AddUrls(links, depth + 1); //過濾鏈接并添加到未下載集合中
_reqsBusy[index] = false; //重置工作狀態(tài)
DispatchWork(); //分配新任務
}
catch (WebException we)
{
MessageBox.Show("ReceivedData Web " + we.Message + url + we.Status);
}
}
第14行獲得了讀取的數(shù)據(jù)大小read,如果read>0說明數(shù)據(jù)可能還沒有讀完,所以在27行繼續(xù)請求讀下一個數(shù)據(jù)包;
如果read<=0說明所有數(shù)據(jù)已經(jīng)接收完畢,這時rs.Html中存放了完整的HTML數(shù)據(jù),就可以進行下一步的處理了。
第26行把這一次得到的字符串拼接在之前保存的字符串的后面,最后就能得到完整的HTML字符串。
然后說一下判斷所有任務完成的處理
private void StartDownload()
{
_checkTimer = new Timer(new TimerCallback(CheckFinish), null, 0, 300);
DispatchWork();
}
private void CheckFinish(object param)
{
if (_workingSignals.IsFinished()) //檢查是否所有工作實例都為Finished
{
_checkTimer.Dispose(); //停止定時器
_checkTimer = null;
if (DownloadFinish != null && _ui != null) //判斷是否注冊了完成事件
{
_ui.Dispatcher.Invoke(DownloadFinish, _index); //調(diào)用事件
}
}
}
第3行創(chuàng)建了一個定時器,每過300ms調(diào)用一次CheckFinish來判斷是否完成任務。
第15行提供了一個完成任務時的事件,可以給客戶程序注冊。_index里存放了當前下載URL的個數(shù)。
該事件的定義是
public delegate void DownloadFinishHandler(int count); /// <summary> /// 全部鏈接下載分析完畢后觸發(fā) /// </summary> public event DownloadFinishHandler DownloadFinish = null;
3. 保存頁面文件
這一部分可簡單可復雜,如果只要簡單地把HTML代碼全部保存下來的話,直接存文件就行了。
private void SaveContents(string html, string url)
{
if (string.IsNullOrEmpty(html)) //判斷html字符串是否有效
{
return;
}
string path = string.Format("{0}\\{1}.txt", _path, _index++); //生成文件名
try
{
using (StreamWriter fs = new StreamWriter(path))
{
fs.Write(html); //寫文件
}
}
catch (IOException ioe)
{
MessageBox.Show("SaveContents IO" + ioe.Message + " path=" + path);
}
if (ContentsSaved != null)
{
_ui.Dispatcher.Invoke(ContentsSaved, path, url); //調(diào)用保存文件事件
}
}
第23行這里又出現(xiàn)了一個事件,是保存文件之后觸發(fā)的,客戶程序可以之前進行注冊。
public delegate void ContentsSavedHandler(string path, string url); /// <summary> /// 文件被保存到本地后觸發(fā) /// </summary> public event ContentsSavedHandler ContentsSaved = null;
4. 提取頁面鏈接
提取鏈接用正則表達式就能搞定了,不懂的可以上網(wǎng)搜。
下面的字符串就能匹配到頁面中的鏈接
http://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?
詳細見代碼
private string[] GetLinks(string html)
{
const string pattern = @"http://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?";
Regex r = new Regex(pattern, RegexOptions.IgnoreCase); //新建正則模式
MatchCollection m = r.Matches(html); //獲得匹配結果
string[] links = new string[m.Count];
for (int i = 0; i < m.Count; i++)
{
links[i] = m[i].ToString(); //提取出結果
}
return links;
}
5. 鏈接的過濾
不是所有的鏈接我們都需要下載,所以通過過濾,去掉我們不需要的鏈接
這些鏈接一般有:
1)、已經(jīng)下載的鏈接
2)、深度過大的鏈接
3)、其他的不需要的資源,如圖片、CSS等
//判斷鏈接是否已經(jīng)下載或者已經(jīng)處于未下載集合中
private bool UrlExists(string url)
{
bool result = _urlsUnload.ContainsKey(url);
result |= _urlsLoaded.ContainsKey(url);
return result;
}
private bool UrlAvailable(string url)
{
if (UrlExists(url))
{
return false; //已經(jīng)存在
}
if (url.Contains(".jpg") || url.Contains(".gif")
|| url.Contains(".png") || url.Contains(".css")
|| url.Contains(".js"))
{
return false; //去掉一些圖片之類的資源
}
return true;
}
private void AddUrls(string[] urls, int depth)
{
if (depth >= _maxDepth)
{
return; //深度過大
}
foreach (string url in urls)
{
string cleanUrl = url.Trim(); //去掉前后空格
cleanUrl = cleanUrl.TrimEnd('/'); //統(tǒng)一去掉最后面的'/'
if (UrlAvailable(cleanUrl))
{
if (cleanUrl.Contains(_baseUrl))
{
_urlsUnload.Add(cleanUrl, depth); //是內(nèi)鏈,直接加入未下載集合
}
else
{
// 外鏈處理
}
}
}
}
第34行的_baseUrl是爬取的基地址,如http://news.sina.com.cn/,將會保存為news.sina.com.cn,當一個URL包含此字符串時,說明是該基地址下的鏈接;否則為外鏈。
_baseUrl的處理如下,_rootUrl是第一個要下載的URL
/// <summary>
/// 下載根Url
/// </summary>
public string RootUrl
{
get
{
return _rootUrl;
}
set
{
if (!value.Contains("http://"))
{
_rootUrl = "http://" + value;
}
else
{
_rootUrl = value;
}
_baseUrl = _rootUrl.Replace("www.", ""); //全站的話去掉www
_baseUrl = _baseUrl.Replace("http://", ""); //去掉協(xié)議名
_baseUrl = _baseUrl.TrimEnd('/'); //去掉末尾的'/'
}
}
至此,基本的爬蟲功能實現(xiàn)就介紹完了。
最后附上源代碼和DEMO程序,爬蟲的源代碼在Spider.cs中,DEMO是一個WPF的程序,Test里是一個控制臺的單線程版版本。
下載地址:C#實現(xiàn)網(wǎng)絡爬蟲DEMO
以上就是C#實現(xiàn)網(wǎng)絡爬蟲的全部過程,代碼解析很詳細,希望對大家的學習有所幫助。
上一篇:C#簡單實現(xiàn)在網(wǎng)頁上發(fā)郵件的案例
欄 目:C#教程
下一篇:C#中的數(shù)組作為參數(shù)傳遞所引發(fā)的問題
本文地址:http://www.jygsgssxh.com/a1/C_jiaocheng/6612.html
您可能感興趣的文章
- 01-10C#實現(xiàn)txt定位指定行完整實例
- 01-10WinForm實現(xiàn)仿視頻播放器左下角滾動新聞效果的方法
- 01-10C#實現(xiàn)清空回收站的方法
- 01-10C#實現(xiàn)讀取注冊表監(jiān)控當前操作系統(tǒng)已安裝軟件變化的方法
- 01-10C#實現(xiàn)多線程下載文件的方法
- 01-10C#實現(xiàn)Winform中打開網(wǎng)頁頁面的方法
- 01-10C#實現(xiàn)遠程關閉計算機或重啟計算機的方法
- 01-10C#自定義簽名章實現(xiàn)方法
- 01-10C#文件斷點續(xù)傳實現(xiàn)方法
- 01-10winform實現(xiàn)創(chuàng)建最前端窗體的方法


閱讀排行
本欄相關
- 01-10C#通過反射獲取當前工程中所有窗體并
- 01-10關于ASP網(wǎng)頁無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實現(xiàn)txt定位指定行完整實例
- 01-10WinForm實現(xiàn)仿視頻播放器左下角滾動新
- 01-10C#停止線程的方法
- 01-10C#實現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實現(xiàn)讀取注冊表監(jiān)控當前操作系統(tǒng)已
隨機閱讀
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 01-10C#中split用法實例總結
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-10SublimeText編譯C開發(fā)環(huán)境設置
- 04-02jquery與jsp,用jquery
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10delphi制作wav文件的方法
- 01-11ajax實現(xiàn)頁面的局部加載


