[開源 .NET 跨平台 數據采集 爬蟲框架: DotnetSpider] [一] 初衷與架構設計
一 ,為什么要造輪子
有興趣的同學可以去各大招聘網站看一下爬蟲工程師的要求,大多是JAVA,PYTHON甚至於還有NODEJS,C++,再或者在開源中國查詢C#的爬蟲,僅有幾個非常簡單或是幾年沒有更新的項目。從我看的一些文章來說,單純性能上.NET對比JAVA,PYTHON並沒有處於弱勢,另根據我多年的開發經驗大多爬蟲性能瓶頸在並發下載(網速)、IP池,因此我認為用C#寫一個爬蟲框架絕對是可行的,那么為什么我大.NET沒有一個強大的爬蟲框架呢?說真的我不知道,可能爬蟲框架核心上比較簡單而沒有被大牛看上,也可能.NET的開發人員沒有別的語言的開發人員勤奮,或是.NET的開源氛圍沒有別的語言高。隨着.NET開源消息的公布,我覺得是時候開發一個跨平台,跨語言的爬蟲框架了。我不喜歡復雜的東西,總是覺得復雜的東西容易出問題,可能跟我個人能力有限,駕馭不了有關。所以設計DotnetSpider的時候是參考JAVA下一個輕量級爬蟲框架webmagic,但是肯定有我自己的理解和改進在內的。此文是系列介紹第一篇,后面陸續會介紹詳細用法及程序改動
另:個人代碼水平有限,如果寫得不好請大家指正海涵
二 ,框架設計
其實爬蟲的設計我覺得還是挺成熟的,大部分都會拿出下圖來說事,由於我是參考的webmagic,所以也少不得得貼上來給大家一看(圖片是直接從webmagic上拿的)

- Scheduler:負責URL的調度,可以實現如Queue, PriorityScheduler, RedisScheduler(可用於分布式)等等
- Downloader: 負責下載HTML,可以實現如HttpDownloader, 瀏覽器的Downloader(WebDriver), FiddlerDownloader,本地文件Downloader等等
- PageProcesser: 負責HTML解析及新的符合規則的URL解析,從上圖可以看到傳入Processer的是Page對象,里面包含了下載好的完整HTML或者JSON數據
- Pipeline: 負責數據的存儲, 可以實現如MySql, MySqlFile,MSSQL,MongoDb等等
三 ,與別的爬蟲的差異
- 使用JSON定義爬蟲,所以可以最終實現跨語言(不同語言只要寫一個JSON轉換的provider就好)
- 由於使用JSON做解析,所以可以實現類中屬性是別的類的情況(僅限MongoDB, 關系型數據庫不好存這種數據)\
- 自動建表
- 有.NET CORE版本,因此可以跨平台(已經在LINUX下運行大量任務了)
- 有感於IP代理的不穩定性,因此代理模塊沒有細致測試使用,而是實現了另一種換IP手段(ADSL撥號)
- 加入基本的數據驗證模塊
四 ,最基本使用方法
最基本的使用方法是不需要引用Extension, 引用Common, Core, JLog就好,然后需要你自己實現IPipeline和Processer
public static void Main()
{
HttpClientDownloader downloader = new HttpClientDownloader();
Core.Spider spider = Core.Spider.Create(new MyPageProcessor(), new QueueDuplicateRemovedScheduler()).AddPipeline(new MyPipeline()).SetThreadNum(1);
var site = new Site() { EncodingName = "UTF-8" };
for (int i = 1; i < 5; ++i)
{
site.AddStartUrl("http://www.youku.com/v_olist/c_97_g__a__sg__mt__lg__q__s_1_r_0_u_0_pt_0_av_0_ag_0_sg__pr__h__d_1_p_1.html");
}
spider.Site = site;
spider.Start();
}
private class MyPipeline : IPipeline
{
public void Process(ResultItems resultItems, ISpider spider)
{
foreach (YoukuVideo entry in resultItems.Results["VideoResult"])
{
Console.WriteLine($"{entry.Name}:{entry.Click}");
}
//May be you want to save to database
//
}
public void Dispose()
{
}
}
private class MyPageProcessor : IPageProcessor
{
public void Process(Page page)
{
var totalVideoElements = page.Selectable.SelectList(Selectors.XPath("//div[@class='yk-col3']")).Nodes();
List<YoukuVideo> results = new List<YoukuVideo>();
foreach (var videoElement in totalVideoElements)
{
var video = new YoukuVideo();
video.Name = videoElement.Select(Selectors.XPath("/div[4]/div[1]/a")).GetValue();
video.Click = int.Parse(videoElement.Select(Selectors.Css("p-num")).GetValue().ToString());
results.Add(video);
}
page.AddResultItem("VideoResult", results);
}
public Site Site => new Site { SleepTime = 0 };
}
public class YoukuVideo
{
public string Name { get; set; }
public string Click { get; set; }
}
五 ,高級使用方法
- 定義一個實體類,並在類上加合適的Attribute以便知道你要如何解析數據
- 定義一個SpiderContextBuilder類,在里面配置爬蟲名字,線程數,Scheduler,downloader等等
- 在main中實類化你的爬蟲類,調用run方法
public class JdSkuSpider : ISpiderContext
{
public SpiderContextBuilder GetBuilder()
{
Log.TaskId = "JD SKU Weekly";
SpiderContext context = new SpiderContext
{
SpiderName = "JD SKU " + DateTimeUtils.MONDAY_RUN_ID,
CachedSize = 1,
ThreadNum = 8,
Site = new Site
{
},
Scheduler = new QueueScheduler()
{
},
StartUrls=new Dictionary<string, Dictionary<string, object>> {
{ "http://list.jd.com/list.html?cat=9987,653,655&page=1&go=0&JL=6_0_0&ms=5", new Dictionary<string, object> { { "name","手機" }, { "cat3","9987" } } },
},
Pipeline = new MysqlPipeline()
{
ConnectString = "[your mysql connect string]"
},
Downloader = new HttpDownloader()
};
return new SpiderContextBuilder(context, typeof(Product));
}
[Schema("jd", "sku_v2", Suffix = TableSuffix.Monday)]
[TargetUrl(new[] { @"page=[0-9]+" }, "//*[@id=\"J_bottomPage\"]")]
[TypeExtractBy(Expression = "//div[contains(@class,'j-sku-item')]", Multi = true)]
[Indexes(Primary = "sku")]
public class Product : ISpiderEntity
{
[StoredAs("category", DataType.String, 20)]
[PropertyExtractBy(Expression = "name", Type = ExtractType.Enviroment)]
public string CategoryName { get; set; }
[StoredAs("cat3", DataType.String, 20)]
[PropertyExtractBy(Expression = "cat3", Type = ExtractType.Enviroment)]
public int CategoryId { get; set; }
[StoredAs("url", DataType.Text)]
[PropertyExtractBy(Expression = "./div[1]/a/@href")]
public string Url { get; set; }
[StoredAs("sku", DataType.String, 25)]
[PropertyExtractBy(Expression = "./@data-sku")]
public string Sku { get; set; }
[StoredAs("commentscount", DataType.String, 20)]
[PropertyExtractBy(Expression = "./div[@class='p-commit']/strong/a")]
public long CommentsCount { get; set; }
[StoredAs("shopname", DataType.String, 100)]
[PropertyExtractBy(Expression = "./div[@class='p-shop hide']/span[1]/a[1]")]
public string ShopName { get; set; }
[StoredAs("name", DataType.String, 50)]
[PropertyExtractBy(Expression = "./div[@class='p-name']/a/em")]
public string Name { get; set; }
[StoredAs("shopid", DataType.String, 25)]
public string ShopId { get; set; }
[StoredAs("venderid", DataType.String, 25)]
[PropertyExtractBy(Expression = "./@venderid")]
public string VenderId { get; set; }
[StoredAs("jdzy_shop_id", DataType.String, 25)]
[PropertyExtractBy(Expression = "./@jdzy_shop_id")]
public string JdzyShopId { get; set; }
[StoredAs("cdate", DataType.Time)]
[PropertyExtractBy(Expression = "now", Type = ExtractType.Enviroment)]
public DateTime CDate { get; set; }
}
}
JdSkuSpider spiderBuilder = new JdSkuSpider(); var context = spiderBuilder.GetBuilder().Context; ContextSpider spider = new ContextSpider(context); spider.Run();
五 ,代碼地址
https://github.com/zlzforever/DotnetSpider 望各位大佬加星:)

