Net Core 透過Docker 利用Quartz 定時執行擷取中央氣象局網頁內容

在一般日常,應該有些是需要定時去執行功能的程式,
但往往程式如果一多,光想著該佈署在哪些機器上,跟安裝所需的執行環境.其實也是件很繁瑣的事.

但透過Docker的機制,就可以很方便的將程式跟執行環境在包在一起.省去一些繁瑣的建置時間
好讓自己可以偷懶XD 笑~~~~

此篇文章是小弟在執行上遇到的一些問題,
稍做記事分享,如果各位前輩有更好的方法.
也請多多指導!

此範例只是剛好有現成程式稍微改一下,如果真的需要氣象局的資料.
有開放資料平台提供API介接,不用爬的這累.
以下為申請連結:
https://opendata.cwb.gov.tw/index
需求假設:需要有一支背景程式,
                  定時一分鐘定時去擷取中央氣象局網頁內容,
                  並在Docker內執行.           

開發環境: Linux Mint 19.1
                   Docker 18.06.1-ce
                   Net Core 2.2
 

第一步.
先來寫個Dockerfile來打包自己的程式跟執行環境.

# Stage 1. dotnet 程式編譯環境SDK
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 as build_env
# 參數
ARG DOTNET_BUILD_CONFIG=Release
# 複製原始碼 
#左邊的./代表要打包的映像檔內根目錄位置,./Parseweather右邊為專案檔內資料夾的位置
COPY ./ ./Parseweather
# 移至要建置的專案
WORKDIR ./Parseweather
# 編譯發布 
RUN dotnet publish -o /app -c $DOTNET_BUILD_CONFIG  
#移至編譯過後指定的的資料夾 
WORKDIR /app

# Stage 2. 建置程式執行環境runtime
FROM mcr.microsoft.com/dotnet/core/runtime:2.2 as runtime
# 從Stage 1 複製編譯過後的檔案至執行環境
COPY --from=build_env /app /app
# 切換執行位置
WORKDIR /app
#執行
ENTRYPOINT ["dotnet", "Parseweather.dll"]

程式目錄結構

在此小弟就不在多贅述Quartz跟AngleSharp會在最後附上其他前輩所寫的參考連結
Program.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Parseweather.Jobs;
using Quartz;
using Quartz.Impl;

namespace Parseweather
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            Console.WriteLine("Console Start");
            var schedulerFactory = new StdSchedulerFactory();
            var schedule = await schedulerFactory.GetScheduler();

            var job = JobBuilder.Create<ParseWeather>()
                .WithIdentity("ParseWeatherJob")
                .Build();

            var trigger = TriggerBuilder.Create()
                .WithCronSchedule("0 0/1 * * * ?") // 每一分鐘觸發一次。
                .WithIdentity("ParseWeatherTrigger")
                .Build();

            // 把工作加入排程
            await schedule.ScheduleJob(job, trigger);

            // 啟動排程器
            await schedule.Start();

            SpinWait.SpinUntil(() => false);
        }
    }
}

這邊比較特別的是透過的SpinWait.SpinUntil的方式讓他等待.
因為在Docker 容器內起來時,如果沒讓主要Thread等待,
只要一啟動容器程式跑完就會離開.
並不會有等待的動作
如下圖

接下擷取網頁內容的部份
ParseWeather.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using Parseweather.Models;
using Quartz;

namespace Parseweather.Jobs
{
    public class ParseWeather : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            Console.WriteLine($"job start {DateTime.Now:yyyyMMdd-HHmm}");

            var config = Configuration.Default.WithDefaultLoader();
            const string url = "https://www.cwb.gov.tw/V7/forecast/f_index.htm";
            using (var dom = await BrowsingContext.New(config).OpenAsync(url))
            {
                //取出網頁上所有縣市ID
                var parseCityIds = dom
                    .QuerySelectorAll("body > div > div > div > table > tbody > tr")
                    .Select(s => s.GetAttribute("id")).Where(w => w != null);
                //針對id向下擷取各個內容
                var result = parseCityIds.Select(item => new WeatherResponse
                {
                    City = dom.QuerySelector($"#{item} > td:nth-child(1)").Text(),
                    Temperature = dom.QuerySelector($"#{item} > td:nth-child(2) > a").Text(),
                    RainPercent = dom.QuerySelector($"#{item} > td:nth-child(3) > a").Text(),
                    ImageUrl = dom.QuerySelector($"#{item} > td:nth-child(4) > a > div > img").GetAttribute("src"),
                    Explain = dom.QuerySelector($"#{item} > td:nth-child(4) > a > div > img").GetAttribute("title")
                });

                foreach (var item in result)
                    Console.WriteLine(
                        $"城市:{item.City}  溫度:{item.Temperature}  降雨機率:{item.RainPercent} 圖片位置:{item.ImageUrl}  說明:{item.Explain}{Environment.NewLine}");
            }

            Console.WriteLine($"job Leave {DateTime.Now:yyyyMMdd-HHmm}");
        }
    }
}



執行Docker建置Image的動作嚕

docker build  -t {optional-name}/{optional-image-name}:{tag} .

看看建置的Image是否有出現在列表中

docker images {optional-name}/{optional-image-name}:{tag}
#or
docker images


啟動來~試一試~~~~
 

#前景執行
docker run -t --name {optione_name}  {optional-name}/{optional-image-name}:{tag}
#OR
#背景執行
docker run -d --name {optione_name} {optional-name}/{optional-image-name}:{tag}

確定每分鐘有執行擷取網頁的動作


確認容器啟動的狀態

docker ps -a


有了Docker的存在,在程式的執行跟建置上提供了不少的便利,但不是偷懶XD

在此附上原始程式碼
請點我

 

參考連結:
Prepare a .Net Core Console App for Docker
The AngleSharp Project • Documentation
ASP.NET 程式中的背景工作 (3) - 使用 Quartz.NET
[C#.NET][Thread] 善用 SpinWait 處理 執行緒空轉 以利提昇效能