把之前做過的校園新聞小項目拆開來,簡單分析每個部分的功能,希望能給感興趣的童鞋一些借鑑和啓發。純手工打造,尊重原創,轉載必究!
先聊聊爬蟲的思想,想象你往Internet丟了一隻貪婪但只會爬行的小蟲子,從第一個根節點開始,它找到了網頁,繼而在這個網頁裏探尋更多的節點,把這些節點放到兩個動態棧裏,一個是存放已到過的節點,另一個存放已知的但還沒到的節點。當到達新的節點時,先檢測兩個棧裏是否已有這個節點,如果有則跳過,如果沒有就重複上述過程,直到想去的節點全都去過了,就停止爬蟲。
我的爬蟲小程序結構如下:
用VS2010生成的類關係依賴圖如下:
MyConApp是程序入口,MainWindow是窗體函數,主要是一些控件邏輯,這裏接受用戶輸入的根節點(輸入的網址也會被簡單處理以規範化),有了根節點就開啓了漫漫爬蟲之旅,爲了提高爬取更多網址的效率,採用多線程是必然,所以如何管理這些線程又是一個重要話題。但我們先暫且撇開這部分,理清楚在單線程的情況下,爬蟲應該如何工作吧。
我們可以逆向分析,爬蟲的結果是什麼,文本和圖片?這其實不是最原始的結果,文本內容是來自網頁通過正則表達式匹配得到的所需內容,圖片是通過圖片標籤獲取到url重新向網絡請求得到的,這些工作採用一個MyFileWriter來處理。在先前的博客裏我討論了在多生產者單消費者和多生產者多消費者如何選擇的問題,在這個系統裏,爲了保證文件寫入的順序(多線程寫入可能導致文本被多次寫入而覆蓋),我用了MyFileWriter單例模式。之後的圖片上傳和ID回傳我就不多說了,因爲採用了別人的圖片服務器,不方便公開哈。
如何獲取最原始的網頁呢,只需要網頁的url即可,由於本項目並不涉及cookie等模擬用戶登錄步驟,相對簡單。一張網頁裏一般都有很多url,在存儲網頁內容之前就需要你檢測此時的url是否是符合需求的。當然想要讓爬取url和存儲每個url對應網頁的內容能保持速度一致,幾乎是不可能的,而且我們還需要排除重複的url,所以需要專門寫一個隊列爲url做維護(也可以用stack類型,不過用queue在邏輯上更容易被人類接受吧),而且在整個程序中url棧應該是全局唯一的,這由MyUrlStack來處理。
怎麼獲取url呢?c#有HttpWebRequest類非常符合網絡爬蟲的需求,封裝在MyHttpServer,負責請求單個網頁的類。獲取到html網頁內容後,又要用到正則表達式了,找到所有匹配。這裏爲了保證全局url棧的功能的純淨,特別是涉及到多線程的時候更需要保證對url棧操作(添加,刪除)的併發性,我把從網頁分析並獲取更多url的操作單獨分離出來寫成了MyProcessor類。
理清單線程的具體工作後,我們就來分析一下如何管理多線程服務,主要涉及到3個類:
- 首先來看一下工作線程的代碼,它封裝了我們的單個線程需要做的事情和數據
/// <summary>
/// MyWorkThread是工作線程類,
/// 包括解析html的類對象,請求html的類對象,存儲urls的實例,存儲html新聞內容的類對象
/// </summary>
class MyWorkThread
{
private MyProcessor _processor = new MyProcessor();
internal MyProcessor Processor
{
get { return _processor; }
}
private MyHttpServer _httpserver = new MyHttpServer();
internal MyHttpServer HttpServer
{
get { return _httpserver; }
}
internal MyUrlStack UrlStack
{
get { return MyUrlStack.UrlStackInstance; }
}
internal MyFileWriter FileWriter
{
get { return MyFileWriter.MyFileWriteInstance; }
}
private bool _isRun = false;
internal bool IsRun
{
get { return _isRun; }
}
/// <summary>
/// 工作線程開始工作,
/// 從urlstack中取出url,讀出html,存儲html中的新聞,解析html中的urls添加到urlstack中
/// </summary>
public void StartWorkThread()
{
bool flag_isEmpty_firstTime = true;
try
{
this._isRun = true;
while (_isRun)
{
string url = this.UrlStack.Pop();
if (!string.IsNullOrEmpty(url))
{
string html = _httpserver.GetResponse(url);
if (!string.IsNullOrEmpty(html))
{
FileWriter.SaveElement(url, html);
_processor.AddUrl(_processor.GetLinks(html));
}
}
else
{
if (flag_isEmpty_firstTime)
{
flag_isEmpty_firstTime = false;
}
else
{
_isRun = false;
}
}
}
}
catch
{
Console.WriteLine("Error in MyWorkThread.StartWorkThread!");
}
}
/// <summary>
/// 停止工作線程
/// </summary>
public void StopWorkThread()
{
this._isRun = false;
}
}
- 然後我們把上述工作邏輯和一個託管線程封裝在一個線程類型裏,這樣在啓動任何一個線程的時候都可以方便的控制單線程邏輯:
/// <summary>
/// MyObject類是一種線程類型
/// </summary>
class MyObjThread
{
//封裝線程需要做的事情和數據
private MyWorkThread _workThread;
internal MyWorkThread WorkThread
{
get { return _workThread; }
set { _workThread = value; }
}
//託管的線程
private System.Threading.Thread _thread;
internal System.Threading.Thread Thread
{
get { return _thread; }
set { _thread = value; }
}
}
- 最後,就是多線程的管理了,如何讓這些自動啓動新的線程,如何自動清理已經邏輯上結束或者terminated或者aborted的線程:
/// <summary>
/// MyThreadManager用於開啓工作線程和監控線程,
/// 監控線程用於清除已死線程,更新線程list
/// </summary>
public class MyThreadManager
{
public int _maxThread = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["MaxCount"]);
internal List<MyObjThread> _list = new List<MyObjThread>();
private bool _isRun = false;
/// <summary>
/// 監控線程list裏的線程存活死亡的主線程
/// </summary>
private System.Threading.Thread _watchThread = null;
public int Current
{
get { return MyUrlStack.UrlStackInstance.Count; }
}
public void StartThreadManager(string url)
{
try
{
MyUrlStack.UrlStackInstance.Push(url);
_isRun = true;
for (int i = 0; i < _maxThread; i++)
{
this.AddThread();
}
_watchThread = new System.Threading.Thread(Watch);
_watchThread.Start();
}
catch
{
Console.WriteLine("Errors in MyThreadManager.StartThreadManager!");
}
}
public void StopThreadManager()
{
try
{
_isRun = false;
_watchThread.Join();//阻塞調用線程,直到線程終止
foreach (MyObjThread obj in _list)
{
obj.WorkThread.StopWorkThread();
obj.Thread.Abort();//終止過程開始
obj.Thread.Join();//阻塞主(調用)線程,直到obj線程終止
}
_list.RemoveRange(0, _list.Count);
}
catch
{
Console.WriteLine("Errors in MyThreadManager.StopThreadManager!");
}
}
private void AddThread()
{
MyObjThread thread = new MyObjThread();
thread.WorkThread = new MyWorkThread();
//Thread 初始化參數ThreadStart類型(委託),它表示此線程開始執行時要調用的方法。
thread.Thread = new System.Threading.Thread(thread.WorkThread.StartWorkThread);
_list.Add(thread);
thread.Thread.Start();
}
/// <summary>
/// 刪除已終止線程,更新線程列表
/// </summary>
private void Watch()
{
List<MyObjThread> _newList = new List<MyObjThread>();
while (_isRun)
{
try
{
//檢測存活下來的線程,並保存
foreach (MyObjThread temp in _list)
{
if (temp.WorkThread.IsRun && temp.Thread.IsAlive)
{
_newList.Add(temp);
}
}
//更新list中的線程
this._list.RemoveRange(0, _list.Count);
_list.AddRange(_newList);
int _leftCount = _maxThread - _list.Count;
for (int i = 0; i < _leftCount; i++)
{
this.AddThread();
}
_newList.RemoveRange(0, _newList.Count);
}
catch
{
Console.WriteLine("Errors in MyObjThread.Watch!");
}
}
}
}
好了,現在簡單的爬蟲程序就完成啦,快去喝杯咖啡,讓他奔跑起來吧!