自從上一個項目從.NET遷移到.NET core之後,磕磕碰碰磨蹭了一個月才正式上線到新版本。
然後最近又開了個新坑,搞了個爬蟲用來爬dy2018電影天堂上面的電影資源。這裡也借機簡單介紹一下如何基於.NET Core寫一個爬蟲。
PS:如有偏錯,敬請指明…
PPS:該去電影院還是多去電影院,畢竟美人良時可無價。
准備工作(.NET Core准備)
首先,肯定是先安裝.NET Core咯。下載及安裝教程在這裡: http://www.bkjia.com/article/87907.htm http://www.bkjia.com/article/88735.htm。無論你是Windows、linux還是mac,統統可以玩。
我這裡的環境是:Windows10 + VS2015 community updata3 + .NET Core 1.1.0 SDK + .NET Core 1.0.1 tools Preview 2.
理論上,只需要安裝一下 .NET Core 1.1.0 SDK 即可開發.NET Core程序,至於用什麼工具寫代碼都無關緊要了。
安裝好以上工具之後,在VS2015的新建項目就可以看到.NET Core的模板了。如下圖:

為了簡單起見,我們創建的時候,直接選擇VS .NET Core tools自帶的模板。
一個爬蟲的自我修養 分析網頁
寫爬蟲之前,我們首先要先去了解一下即將要爬取的網頁數據組成。
具體到網頁的話,便是分析我們要抓取的數據在HTML裡面是用什麼標簽抑或有什麼樣的標記,然後使用這個標記把數據從HTML中提取出來。在我這裡的話,用的更多的是HTML標簽的ID和CSS屬性。
以本文章想要爬取的dy2018.com為例,簡單描述一下這個過程。dy2018.com主頁如下圖:

在chrome裡面,按F12進入開發者模式,接著如下圖使用鼠標選擇對應頁面數據,然後去分析頁面HTML組成。

接著我們開始分析頁面數據:


經過簡單分析HTML,我們得到以下結論:
www.dy2018.com首頁的電影數據存儲在一個class為co_content222的div標簽裡面
電影詳情鏈接為a標簽,標簽顯示文本就是電影名稱,URL即詳情URL
那麼總結下來,我們的工作就是:找到class='co_content222' 的div標簽,從裡面提取所有的a標簽數據。
開始寫代碼…
之前在寫做項目的時候用到過AngleSharp庫,一個基於.NET(C#)開發的專門為解析xHTML源碼的DLL組件。
AngleSharp主頁在這裡: https://anglesharp.github.io/ ,
詳細介紹:http://www.bkjia.com/article/99082.htm
Nuget地址: Nuget AngleSharp 安裝命令:Install-Package AngleSharp
獲取電影列表數據
private static HtmlParser htmlParser = new HtmlParser();
private ConcurrentDictionary<string, MovieInfo> _cdMovieInfo = new ConcurrentDictionary<string, MovieInfo>();
privatevoidAddToHotMovieList()
{
//此操作不阻塞當前其他操作,所以使用Task
// _cdMovieInfo 為線程安全字典,存儲了當期所有的電影數據
Task.Factory.StartNew(()=>
{
try
{
//通過URL獲取HTML
var htmlDoc = HTTPHelper.GetHTMLByURL("http://www.dy2018.com/");
//HTML 解析成 IDocument
var dom = htmlParser.Parse(htmlDoc);
//從dom中提取所有class='co_content222'的div標簽
//QuerySelectorAll方法接受 選擇器語法
var lstDivInfo = dom.QuerySelectorAll("div.co_content222");
if (lstDivInfo != null)
{
//前三個DIV為新電影
foreach (var divInfo in lstDivInfo.Take(3))
{
//獲取div中所有的a標簽且a標簽中含有"/i/"的
//Contains("/i/") 條件的過濾是因為在測試中發現這一塊div中的a標簽有可能是廣告鏈接
divInfo.QuerySelectorAll("a").Where(a =>
a.GetAttribute("href").Contains("/i/"))
.ToList().ForEach(
a =>
{
//拼接成完整鏈接
var onlineURL = "http://www.dy2018.com" + a.GetAttribute("href");
//看一下是否已經存在於現有數據中
if (!_cdMovieInfo.ContainsKey(onlineURL))
{
//獲取電影的詳細信息
MovieInfo movieInfo = FillMovieInfoFormWeb(a, onlineURL);
//下載鏈接不為空才添加到現有數據
if (movieInfo.XunLeiDownLoadURLList != null
&& movieInfo.XunLeiDownLoadURLList.Count != 0)
{
_cdMovieInfo.TryAdd
(movieInfo.Dy2018OnlineUrl,movieInfo);
}
}
});
}
}
}
catch(Exception ex)
{
}
});
}
獲取電影詳細信息
privateMovieInfoFillMovieInfoFormWeb(AngleSharp.Dom.IElement a,
string onlineURL)
{
var movieHTML = HTTPHelper.GetHTMLByURL(onlineURL);
var movieDoc = htmlParser.Parse(movieHTML);
//http://www.dy2018.com/i/97462.html 分析過程見上,不再贅述
//電影的詳細介紹 在id為Zoom的標簽中
var zoom = movieDoc.GetElementById("Zoom");
//下載鏈接在 bgcolor='#fdfddf'的td中,有可能有多個鏈接
var lstDownLoadURL = movieDoc.QuerySelectorAll("[bgcolor='#fdfddf']");
//發布時間 在class='updatetime'的span標簽中
var updatetime = movieDoc.QuerySelector("span.updatetime");
var pubDate = DateTime.Now;
if(updatetime!=null && !string.IsNullOrEmpty(updatetime.InnerHtml))
{
//內容帶有“發布時間:”字樣,
//replace成""之後再去轉換,轉換失敗不影響流程
DateTime.TryParse(updatetime.InnerHtml.Replace("發布時間:",
""), out pubDate);
}
var movieInfo = new MovieInfo()
{
//InnerHtml中可能還包含font標簽,做多一個Replace
MovieName = a.InnerHtml.Replace("<font color=\"#0c9000\">","")
.Replace("<font color=\" #0c9000\">","")
.Replace("</font>", ""),
Dy2018OnlineUrl = onlineURL,
MovieIntro = zoom != null ? WebUtility.HtmlEncode(zoom.InnerHtml) : "暫無介紹...",
//可能沒有簡介,雖然好像不怎麼可能
XunLeiDownLoadURLList = lstDownLoadURL != null ?
lstDownLoadURL.Select(d => d.FirstElementChild.InnerHtml).ToList() : null,
//可能沒有下載鏈接
PubDate = pubDate,
};
return movieInfo;
}
HTTPHelper
這邊有個小坑,dy2018網頁編碼格式是GB2312,.NET Core默認不支持GB2312,使用Encoding.GetEncoding(“GB2312”)的時候會拋出異常。
解決方案是手動安裝System.Text.Encoding.CodePages包(Install-Package System.Text.Encoding.CodePages),
然後在Starup.cs的Configure方法中加入Encoding.RegisterProvider(CodePagesEncodingProvider.Instance),接著就可以正常使用Encoding.GetEncoding(“GB2312”)了。
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace Dy2018Crawler
{
public class HTTPHelper
{
public static HttpClient Client { get; } = new HttpClient();
publicstaticstringGetHTMLByURL(stringurl)
{
try
{
System.Net.WebRequest wRequest = System.Net.WebRequest.Create(url);
wRequest.ContentType = "text/html; charset=gb2312";
wRequest.Method = "get";
wRequest.UseDefaultCredentials = true;
// Get the response instance.
var task = wRequest.GetResponseAsync();
System.Net.WebResponse wResp = task.Result;
System.IO.Stream respStream = wResp.GetResponseStream();
//dy2018這個網站編碼方式是GB2312,
using (System.IO.StreamReader reader =
new System.IO.StreamReader(respStream,
Encoding.GetEncoding("GB2312")))
{
return reader.ReadToEnd();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
return string.Empty;
}
}
}
}
定時任務的實現
定時任務我這裡使用的是 Pomelo.AspNetCore.TimedJob 。
Pomelo.AspNetCore.TimedJob是一個.NET Core實現的定時任務job庫,支持毫秒級定時任務、從數據庫讀取定時配置、同步異步定時任務等功能。
由.NET Core社區大神兼前微軟MVP AmamiyaYuuko (入職微軟之後就卸任MVP…)開發維護,不過好像沒有開源,回頭問下看看能不能開源掉。
nuget上有各種版本,按需自取。地址: https://www.nuget.org/packages/Pomelo.AspNetCore.TimedJob/1.1.0-rtm-10026
作者自己的介紹文章: Timed Job - Pomelo擴展包系列
Startup.cs相關代碼
我這邊使用的話,首先肯定是先安裝對應的包:Install-Package Pomelo.AspNetCore.TimedJob -Pre
然後在Startup.cs的ConfigureServices函數裡面添加Service,在Configure函數裡面Use一下。
// This method gets called by the runtime. Use this method to add services to the container.
publicvoidConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
//Add TimedJob services
services.AddTimedJob();
}
publicvoidConfigure(IApplicationBuilder app,
IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//使用TimedJob
app.UseTimedJob();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
Job相關代碼
接著新建一個類,明明為XXXJob.cs,引用命名空間using Pomelo.AspNetCore.TimedJob,XXXJob繼承於Job,添加以下代碼。
public class AutoGetMovieListJob:Job
{
// Begin 起始時間;Interval執行時間間隔,單位是毫秒,建議使用以下格式,此處為3小時;
//SkipWhileExecuting是否等待上一個執行完成,true為等待;
[Invoke(Begin = "2016-11-29 22:10", Interval = 1000 * 3600*3, SkipWhileExecuting =true)]
publicvoidRun()
{
//Job要執行的邏輯代碼
//LogHelper.Info("Start crawling");
//AddToLatestMovieList(100);
//AddToHotMovieList();
//LogHelper.Info("Finish crawling");
}
}
項目發布相關 新增runtimes節點
使用VS2015新建的模板工程,project.json配置默認是沒有runtimes節點的.
我們想要發布到非Windows平台的時候,需要手動配置一下此節點以便生成。
"runtimes": {
"win7-x64": {},
"win7-x86": {},
"osx.10.10-x64": {},
"osx.10.11-x64": {},
"ubuntu.14.04-x64": {}
}
刪除/注釋scripts節點
生成時會調用node.js腳本構建前端代碼,這個不能確保每個環境都有bower存在…注釋完事。
//"scripts": {
// "prepublish": [ "bower install", "dotnet bundle" ],
// "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
//},
刪除/注釋dependencies節點裡面的type
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0"
//"type": "platform"
},
project.json的相關配置說明可以看下這個官方文檔: Project.json-file ,
或者張善友老師的文章 .NET Core系列 : 2 、project.json 這葫蘆裡賣的什麼藥
開發編譯發布
//還原各種包文件 dotnet restore; //發布到C:\code\website\Dy2018Crawler文件夾 dotnet publish -r ubuntu.14.04-x64 -c Release -o "C:\code\website\Dy2018Crawler";
最後,照舊開源……以上代碼都在下面找到:
Gayhub地址: https://github.com/liguobao/Dy2018Crawler
PS:回頭寫個爬片大家滋持不啊…