去年年底的時候曾經發過一個資料採集器,那是專門針對某一個網站來進行採集的,如果需要採集新的網站內容,就需要修改程式碼並重新編譯。
昨晚完成了一個帶智慧策略的採集系統。其實,這個策略的方案三年前就想好了,那時候打算用VB做,做了一半就擱置了。現在用C#才終於把這個方案實現了。
整個方案大概是這樣的:
需要建立一個AC資料庫,MSSQL也行,有四個表:PageType用於記錄頁面的種類,比如列表頁和詳細頁兩類;Url表用於記錄要採集的網址,另外還有一個欄位TypeID標明該網址屬於哪一種頁面型別,比如是列表頁還是詳細頁;Rule表記錄著各種規則,主要有三個欄位,FromTypeID源頁型別,ToTypeID目的頁型別,Pattern規則;CjPage用於儲存採集到的網頁內容,還包含網址和頁面種類。
採集策略的核心就在於規則庫Rule。
工作過程大概這樣:
1,採集執行緒從Url表抽取一個網址,並馬上在表中將其刪除,為了防止衝突,這個過程需要用多執行緒同步解決;
2,用WebClient請求該網址的頁面內容;
3,取得內容後,給執行緒池的執行緒來分析處理,本執行緒回到1,繼續去Url表取下一個網址;
4,執行緒池在有空閒執行緒時,會呼叫分析函式ParsePage去處理上次獲得的頁面內容;
5,先到Rule中取所有FromTypeID為當前網址TypeID;
6,如果沒有取到任何規則Rule,則將本頁內容寫入到CjPage中;
7,如果取到規則,那麼遍歷規則,為每條規則執行ParseUrl方法;
8,ParseUrl根據規則的Pattern匹配到頁面內容中的所有網址,並記錄到Url中,規則的 ToTypeID就是Url的TypeID。
至此,整個流程就完成了。下面舉一個實際例子來說明一下:
我要擷取動網開發者網路的所有ASP文章;
首先,在頁面型別庫中加入列表頁和詳細頁兩行,再把寫入到Url中,頁面型別是列表頁;
其次,在Rule中加入兩條規則:
一,從列表頁取得詳細頁的網址FromTypeID=1 ToTypeID=2,Pattern是· ,這條規則將會識別列表頁上的所有詳細頁的連結,並記入到Url中,TypeID是詳細頁;
二,從列表頁取得列表頁的網址FromTypeID=1 ToTypeID=1,Pattern是下一頁,這條規則將會取得當前列表頁上的下一頁的連結,並記入到Url 中,TypeID還是列表頁。
採集器工作時,如果採集的是詳細頁的內容,將會直接寫入到CjPage中,因為沒有FromTypeID=2的規則;而採集的是列表頁的內容時,就要做兩件事了,因為有兩條FromTypeID=1的規則,一件事是識別當前列表頁中所有文章的連結並存入Url,另一件事是識別下一列表頁連結並存入Url。
由於規則具有遞迴性,使得采集器能遞迴採集到所有的文章。
下面是一些核心原始碼(沒有公開的都是一些資料層的添刪改查的程式碼):
以下是程式碼片段:
using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Threading; using CJData; using System.Text.RegularExpressions; using NLog; namespace CJ { /// /// 寫日誌委託 /// /// public delegate void WriteLogCallBack(String log); /// /// 採集 /// public class CaiJi { private WebClient _wc; public WebClient Wc { get { if (_wc == null) _wc = new WebClient(); return _wc; } } private Thread thread; public String Name = ""; public event WriteLogCallBack OnWriteLog; /// /// 開始工作 /// public void Start() { if (thread != null) return; thread = new Thread(new ThreadStart(Work)); thread.Start(); } /// /// 停止工作 /// public void Stop() { if (thread != null) thread.Abort(); thread = null; } private void Work() { int times = 0; while (times < 100) { Url url = Url.SelectOne(); try { if (url != null) { String page = Wc.DownloadString(url.UrlAddress); if (!String.IsNullOrEmpty(page)) { OnWriteLog(Name + " 成功抓取:" + url.UrlAddress); times = 0; ThreadPool.QueueUserWorkItem(new WaitCallback(ParsePage), new Object[] { url, page }); } } else { //OnWriteLog(Name + " 沒有工作,休息半秒"); times++; //沒有工作,休息半秒 Thread.Sleep(500); } } catch (ThreadAbortException e) { OnWriteLog(Name + " 外部終止"); break; } catch (Exception e) { times++; OnWriteLog(Name + " 賺取" + url.UrlAddress + "出錯,休息半秒。" + e.Message); Trace.WriteLine(url.UrlAddress); //出錯,休息半秒 Thread.Sleep(500); } } OnWriteLog(Name + " 完成!"); } private void ParsePage(Object state) { Object[] objs = (Object[])state; Url url = objs[0] as Url; String page = (String)objs[1]; IList rs = Rule.SelectAll(Rule._.FromTypeID, url.TypeID); //if (url.PageType.TypeName == "詳細頁") if (rs == null || rs.Count < 1) { CjPage cp = new CjPage(); cp.CjTime = DateTime.Now; cp.Content = page; cp.Url = url.UrlAddress; cp.TypeID = url.TypeID; cp.Insert(); } else { foreach (Rule r in rs) { ParseUrl(url, r, page); } } } private void ParseUrl(Url u, Rule r, String page) { Regex reg = new Regex(r.Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); MatchCollection ms = reg.Matches(page); foreach (Match m in ms) { Url url = new Url(); url.TypeID = r.ToTypeID; url.UrlAddress = m.Groups[1].Value; if (!url.UrlAddress.StartsWith(")) { if (url.UrlAddress.Substring(0, 1) == "/") { url.UrlAddress = u.UrlAddress.Substring(0, u.UrlAddress.IndexOf("/", 8)) + url.UrlAddress; } else { if (u.UrlAddress.Substring(u.UrlAddress.Length - 1) == "/") url.UrlAddress = u.UrlAddress + url.UrlAddress; else if (u.UrlAddress.LastIndexOf("/") < u.UrlAddress.LastIndexOf(".")) url.UrlAddress = u.UrlAddress.Substring(0, u.UrlAddress.LastIndexOf("/") + 1) + url.UrlAddress; else url.UrlAddress = u.UrlAddress + "/" + url.UrlAddress; } } url.Insert(); } } } } |
以下是程式碼片段:
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; namespace CJ { public partial class Form1 : Form { public Form1() { InitializeComponent(); } CaiJi[] cjs; private void button1_Click(object sender, EventArgs e) { Button btn = sender as Button; if (btn.Text == "停止") { foreach (CaiJi cj in cjs) { if (cj != null) cj.Stop(); } cjs = null; btn.Text = "開始"; return; } richTextBox1.Text = ""; btn.Text = "停止"; int k = 100; if (!int.TryParse(textBox1.Text, out k)) k = 100; cjs = new CaiJi[k]; for (int i = 0; i < cjs.Length; i++) { cjs[i] = new CaiJi(); cjs[i].Name = "執行緒" + i.ToString("00"); cjs[i].OnWriteLog += new WriteLogCallBack(cj_OnWriteLog); } foreach (CaiJi cj in cjs) { cj.Start(); } } void cj_OnWriteLog(string log) { if (richTextBox1.InvokeRequired) { richTextBox1.Invoke(new WriteLogCallBack(cj_OnWriteLog), new object[] { log }); } else { if (richTextBox1.Lines.Length > 3000) richTextBox1.Text = ""; richTextBox1.Text = log + Environment.NewLine + richTextBox1.Text; } } } } |