手把手教你用.NET Core寫爬蟲

寫在前面

自從上一個項目58HouseSearch從.NET遷移到.NET core以後,磕磕碰碰磨蹭了一個月才正式上線到新版本。
而後最近又開了個新坑,搞了個Dy2018Crawler用來爬dy2018電影天堂上面的電影資源。這裏也藉機簡單介紹一下如何基於.NET Core寫一個爬蟲。
PS:若有偏錯,敬請指明...
PPS:該去電影院仍是多去電影院,畢竟美人良時可無價。javascript

準備工做(.NET Core準備)

首先,確定是先安裝.NET Core咯。下載及安裝教程在這裏:.NET - Powerful Open Source Development。不管你是Windows、linux仍是mac,通通能夠玩。html

我這裏的環境是: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程序,至於用什麼工具寫代碼都可有可無了。java

安裝好以上工具以後,在VS2015的新建項目就能夠看到.NET Core的模板了。以下圖:node

爲了簡單起見,咱們建立的時候,直接選擇VS .NET Core tools自帶的模板。linux

一個爬蟲的自我修養

分析網頁

寫爬蟲以前,咱們首先要先去了解一下即將要爬取的網頁數據組成。git

具體到網頁的話,即是分析咱們要抓取的數據在HTML裏面是用什麼標籤抑或有什麼樣的標記,而後使用這個標記把數據從HTML中提取出來。在我這裏的話,用的更多的是HTML標籤的ID和CSS屬性。github

以本文章想要爬取的dy2018.com爲例,簡單描述一下這個過程。dy2018.com主頁以下圖:web

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

接着咱們開始分析頁面數據:

通過簡單分析HTML,咱們獲得如下結論:

  1. www.dy2018.com首頁的電影數據存儲在一個class爲co_content222的div標籤裏面

  2. 電影詳情連接爲a標籤,標籤顯示文本就是電影名稱,URL即詳情URL

那麼總結下來,咱們的工做就是:找到class='co_content222' 的div標籤,從裏面提取全部的a標籤數據。

開始寫代碼...

以前在寫58HouseSearch項目遷移到asp.net core簡單提過AngleSharp庫,一個基於.NET(C#)開發的專門爲解析xHTML源碼的DLL組件。

  1. AngleSharp主頁在這裏:https://anglesharp.github.io/

  2. 博客園文章:解析HTML利器AngleSharp介紹

  3. Nuget地址:Nuget AngleSharp 安裝命令:Install-Package AngleSharp

獲取電影列表數據
private static HtmlParser htmlParser = new HtmlParser();

   private  ConcurrentDictionary<string, MovieInfo> _cdMovieInfo = new ConcurrentDictionary<string, MovieInfo>();
  private void AddToHotMovieList()
        {
            //此操做不阻塞當前其餘操做,因此使用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)
                {

                }
            });
        }

獲取電影詳細信息

private MovieInfo FillMovieInfoFormWeb(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();

        public static string GetHTMLByURL(string url)
        {
            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社區大神兼前微軟MVPAmamiyaYuuko(入職微軟以後就卸任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.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
            //Add TimedJob services
            services.AddTimedJob();
        }
        
         public void Configure(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)]
        public void Run()
        {
             //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

在線地址:http://codelover.win/

PS:回頭寫個爬片你們滋持不啊...

相關文章
相關標籤/搜索