去年年底的時候曾經(jīng)發(fā)過一個數(shù)據(jù)采集器《網(wǎng)頁數(shù)據(jù)采集器》,那是專門針對某一個網(wǎng)站來進行采集的,如果需要采集新的網(wǎng)站內容,就需要修改代碼并重新編譯。
昨晚完成了一個帶智能策略的采集系統(tǒng)。其實,這個策略的方案三年前就想好了,那時候打算用VB做,做了一半就擱置了。現(xiàn)在用C#才終于把這個方案實現(xiàn)了。
去年年底的時候曾經(jīng)發(fā)過一個數(shù)據(jù)采集器《網(wǎng)頁數(shù)據(jù)采集器》,那是專門針對某一個網(wǎng)站來進行采集的,如果需要采集新的網(wǎng)站內容,就需要修改代碼并重新編譯。
昨晚完成了一個帶智能策略的采集系統(tǒng)。其實,這個策略的方案三年前就想好了,那時候打算用VB做,做了一半就擱置了。現(xiàn)在用C#才終于把這個方案實現(xiàn)了。
整個方案大概是這樣的:
需要建立一個AC數(shù)據(jù)庫,MSSQL也行,有四個表:PageType用于記錄頁面的種類,比如列表頁和詳細頁兩類;Url表用于記錄要采集的網(wǎng)址,另外還有一個字段TypeID標明該網(wǎng)址屬于哪一種頁面類型,比如是列表頁還是詳細頁;Rule表記錄著各種規(guī)則,主要有三個字段,F(xiàn)romTypeID源頁類型,ToTypeID目的頁類型,Pattern規(guī)則;CjPage用于存儲采集到的網(wǎng)頁內容,還包含網(wǎng)址和頁面種類。
采集策略的核心就在于規(guī)則庫Rule。
工作過程大概這樣:
1,采集線程從Url表抽取一個網(wǎng)址,并馬上在表中將其刪除,為了防止沖突,這個過程需要用多線程同步解決;
2,用WebClient請求該網(wǎng)址的頁面內容;
3,取得內容后,給線程池的線程來分析處理,本線程回到1,繼續(xù)去Url表取下一個網(wǎng)址;
4,線程池在有空閑線程時,會調用分析函數(shù)ParsePage去處理上次獲得的頁面內容;
5,先到Rule中取所有FromTypeID為當前網(wǎng)址TypeID;
6,如果沒有取到任何規(guī)則Rule,則將本頁內容寫入到CjPage中;
7,如果取到規(guī)則,那么遍歷規(guī)則,為每條規(guī)則執(zhí)行ParseUrl方法;
8,ParseUrl根據(jù)規(guī)則的Pattern匹配到頁面內容中的所有網(wǎng)址,并記錄到Url中,規(guī)則的ToTypeID就是Url的TypeID。
至此,整個流程就完成了。下面舉一個實際例子來說明一下:
我要截取動網(wǎng)開發(fā)者網(wǎng)絡的所有ASP文章http://www.cndw.com/tech/asp/;
首先,在頁面類型庫中加入列表頁和詳細頁兩行,再把http://www.cndw.com/tech/asp/寫入到Url中,頁面類型是列表頁;
其次,在Rule中加入兩條規(guī)則:
一,從列表頁取得詳細頁的網(wǎng)址FromTypeID=1 ToTypeID=2,Pattern是· <a href="([^>]*)" target=_blank>,這條規(guī)則將會識別列表頁上的所有詳細頁的鏈接,并記入到Url中,TypeID是詳細頁;
二,從列表頁取得列表頁的網(wǎng)址FromTypeID=1 ToTypeID=1,Pattern是<a href='([^>]*)'>下一頁<\/a>,這條規(guī)則將會取得當前列表頁上的下一頁的鏈接,并記入到Url中,TypeID還是列表頁。
采集器工作時,如果采集的是詳細頁的內容,將會直接寫入到CjPage中,因為沒有FromTypeID=2的規(guī)則;而采集的是列表頁的內容時,就要做兩件事了,因為有兩條FromTypeID=1的規(guī)則,一件事是識別當前列表頁中所有文章的鏈接并存入Url,另一件事是識別下一列表頁鏈接并存入Url。
由于規(guī)則具有遞歸性,使得采集器能遞歸采集到所有的文章。
下面是一些核心源碼(沒有公開的都是一些數(shù)據(jù)層的添刪改查的代碼):
以下是代碼片段:
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 { /// <summary> /// 寫日志委托 /// </summary> /// <param name="log"></param> public delegate void WriteLogCallBack(String log); /// <summary> /// 采集 /// </summary> 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;
/// <summary> /// 開始工作 /// </summary> public void Start() { if (thread != null) return; thread = new Thread(new ThreadStart(Work)); thread.Start(); } /// <summary> /// 停止工作 /// </summary> 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<Rule> 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("http://")) { 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; } } } }
|