11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

QiitaのいいねランキングをSlackに自動投稿するbotを作った

Last updated at Posted at 2018-03-30

はじめに

Qiitaのトレンドをチェックする時によく以下ページを参照させていただいています。
【毎日自動更新】Qiitaのデイリーストックランキング!ウィークリーもあるよ

よりチェックしやすくしたいと思い、Slackに毎日自動投稿するようにしてみました。

構成

  • プログラム(.NET Core 2.0)
  • 上記Webページをスクレイピングし、ランキング情報を取得する
  • Slackチャネルへ投稿する
  • 定期実行
  • 上記プログラムをAWS Lambdaの関数としてデプロイする
  • AWS CloudWatch Eventsで実行スケジュールを設定する

事前準備

WebhookURLの取得

  • SlackのApp管理ページから[カスタムインテグレーション]を選択する
  • [着信Webフック]を選択する
  • [設定を追加]を選択する
  • 投稿するSlackチャネルを選択して[インテグレーションを追加]をクリックする
  • 表示されたWebhook URLをコピーするなどして手元に置いておく
  • 名前やアイコンをお好みで変更する
  • 保存して終了

スクリーンショット.png

AWS Toolkit for Visual Studioのセットアップ

以下を参考にしました。
https://docs.aws.amazon.com/ja_jp/toolkit-for-visual-studio/latest/user-guide/getting-set-up.html

プログラム

  • 言語: C#
  • フレームワーク: .NET Core 2.0
  • NuGetパッケージ:
  • AngleSharp ... HTML解析を簡略化するため
  • Json.NET ... json処理を簡略化するため
  • IDE: Visual Studio Community 2017
  • 拡張機能: AWS Toolkit for Visual Studio

プロジェクトの作成

[AWS Lambda Project(.NET Core)]を選択します。
ブループリントは[Empty Function]を選択します。

スクリーンショット.png

スクレイピング処理

※記事として見やすくするため1メソッドにまとめています。
(実際はもう少し細かくメソッド分け、クラス分けをしています。)

WebScraper.cs
using AngleSharp;
...

namespace QiitaRankingBot
{
    internal class WebScraper
    {
        public async Task<string> GenerateText()
        {
            // 対象ページを読み込み
            const string targetUrl = "https://qiita.com/takeharu/items/bb154a4bc198fb102ff3";
            var config = Configuration.Default.WithDefaultLoader();
            var context = BrowsingContext.New(config);
            var doc = await context.OpenAsync(targetUrl);

            // 更新日を取得
            var date = System.DateTime.Parse(doc.QuerySelector("time[itemprop='dateModified']").GetAttribute("datetime"));

            // ランキングを取得
            const int rankingCount_Daily = 10;
            var elements = doc.QuerySelectorAll("h4").Take(rankingCount_Daily);
            var items = elements.Select((_, i) =>
            {
                var link = _.QuerySelector("a[href^='https://qiita.com/']");
                return new Item
                {
                    Ranking = i + 1, // 構成上取得が難しかったのでちょっとダサいけど手動カウント
                    Title = link.InnerHtml,
                    Url = link.GetAttribute("href"),
                };
            });

            // 整形
            var separator = Environment.NewLine + Environment.NewLine;
            var text = new StringBuilder();
            text.Append($"更新日: {date:yyyy/MM/dd}");
            text.Append(separator);
            text.Append(items.Select(_ => $"<{_.Url}|{_.Ranking}. {_.Title}>").Aggregate((x, y) => $"{x}{separator}{y}"));
            return text.ToString();
        }

        private class Item
        {
            public int Ranking { get; set; }
            public string Title { get; set; }
            public string Url { get; set; }
        }
    }
}

以下のような文字列を生成します。
体裁は個人の好みです。
(空行を挟んでいるのは、スマホで見た時にタップしやすくするため。)

更新日: 2018/03/30

<https://qiita.com/...|1. ○○ >

<https://qiita.com/...|2. ○○ >

<https://qiita.com/...|3. ○○ >

...

※参考)https://api.slack.com/docs/message-formatting

Slackへ投稿

UTF8でエンコードした以下のようなJSON文字列をPOSTします。
POST先のURLは、先程取得したWebhookURLです。

{
    "text":"更新日: 2018/03/30\r\n\r\n<https://qiita.com/...|1. ○○>\r\n\r\n<https://qiita.com/...|2. ○○>\r\n\r\n<https://qiita.com/...|3. ○○>\r\n\r\n..."
}

※参考)https://api.slack.com/docs/messages

SlackClient.cs
using Newtonsoft.Json;
...

namespace QiitaRankingBot
{
    internal class SlackClient
    {
        public async Task PostAsync(string text)
        {
            const string webHookUrl = "https://hooks.slack.com/services/XXX...";

            using (var client = new WebClient())
            {
                client.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=UTF-8");
                client.Encoding = Encoding.UTF8;

                await client.UploadDataTaskAsync(new Uri(webHookUrl)
                    , Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new PostData
                    {
                        Text = text,
                    })));
            }
        }

        // https://api.slack.com/docs/messages
        [JsonObject]
        private class PostData
        {
            [JsonProperty("text")]
            public string Text { get; set; }
        }
    }
}

あとはエントリポイントからそれぞれ順番に実行します。

Function.cs
using Amazon.Lambda.Core;
...

namespace QiitaRankingBot
{
    public class Function
    {
        public void FunctionHandler(ILambdaContext context)
        {
            var scraper = new WebScraper();
            var generateText = scraper.GenerateText();
            generateText.Wait();

            var slack = new SlackClient();
            slack.PostAsync(generateText.Result).Wait();
        }
    }
}

定期実行

AWS Lambda

こちらのページがとても参考になりました。
AWS Lambda で C# が使えるようになったので早速試してみた

バージョンは違いますが、基本的な流れは同じでした。

AWS CloudWatch Events

デプロイしたLambda関数を確認すると、以下のようにトリガーが未設定です。

スクリーンショット.png

ここにAWS CloudWatch Eventsで定期実行するトリガーを設定します。

  • [トリガーの追加]から[AWS CloudWatch Events]を選択
  • [トリガーの設定]内で[新規ルールの作成]を選択
  • スケジュール式にcronで希望の実行間隔を指定
  • 例)cron(0 0 ? * * *) ... 毎日9:00(JST)に実行

動作確認

スクリーンショット.png

各リンクをクリックして該当ページへ遷移することを確認します。

おわりに

Qiitaのランキング等のデータはAPIで提供されていないようだったので、Webスクレイピングという手法を使いました。
面白いですね、スクレイピング。「最後の手段」って感じなので、仕事ではできるだけ使いたくはないですが。
今回のbotは家族用に作った1日1アクセスの処理なので問題ないと考えましたが、スクレイピングって対象媒体(今回ならQiita)の規約や著作権法等に違反していないかどうか、少しドキドキします。

AWSで定期実行バッチを作りましたが、EC2でサーバ立てるより安くて良い感じでした。
ただ、Lambda関数のタイムアウトは最大でも5分らしいので、重いバッチ処理を実装するには向いてないですし、そもそもLambdaってこういったバッチ処理を載せる入れ物ではない気がします。

AWSもっと勉強しなきゃな、と思いました。

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?