網頁資料採集器

mrhaozi發表於2010-03-18

去年年底的時候曾經發過一個資料採集器,那是專門針對某一個網站來進行採集的,如果需要採集新的網站內容,就需要修改程式碼並重新編譯。

昨晚完成了一個帶智慧策略的採集系統。其實,這個策略的方案三年前就想好了,那時候打算用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;
}
}
}
}

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/16396910/viewspace-1032132/,如需轉載,請註明出處,否則將追究法律責任。

相關文章