はじめに
Qiitaのトレンドをチェックする時によく以下ページを参照させていただいています。
【毎日自動更新】Qiitaのデイリーストックランキング!ウィークリーもあるよ
よりチェックしやすくしたいと思い、Slackに毎日自動投稿するようにしてみました。
構成
- プログラム(.NET Core 2.0)
- 上記Webページをスクレイピングし、ランキング情報を取得する
- Slackチャネルへ投稿する
- 定期実行
- 上記プログラムをAWS Lambdaの関数としてデプロイする
- AWS CloudWatch Eventsで実行スケジュールを設定する
事前準備
WebhookURLの取得
- SlackのApp管理ページから[カスタムインテグレーション]を選択する
- [着信Webフック]を選択する
- [設定を追加]を選択する
- 投稿するSlackチャネルを選択して[インテグレーションを追加]をクリックする
- 表示された
Webhook URL
をコピーするなどして手元に置いておく - 名前やアイコンをお好みで変更する
- 保存して終了
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]を選択します。
スクレイピング処理
※記事として見やすくするため1メソッドにまとめています。
(実際はもう少し細かくメソッド分け、クラス分けをしています。)
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
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; }
}
}
}
あとはエントリポイントからそれぞれ順番に実行します。
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関数を確認すると、以下のようにトリガーが未設定です。
ここにAWS CloudWatch Eventsで定期実行するトリガーを設定します。
- [トリガーの追加]から[AWS CloudWatch Events]を選択
- [トリガーの設定]内で[新規ルールの作成]を選択
- スケジュール式にcronで希望の実行間隔を指定
- 例)
cron(0 0 ? * * *)
... 毎日9:00(JST)に実行
動作確認
各リンクをクリックして該当ページへ遷移することを確認します。
おわりに
Qiitaのランキング等のデータはAPIで提供されていないようだったので、Webスクレイピングという手法を使いました。
面白いですね、スクレイピング。「最後の手段」って感じなので、仕事ではできるだけ使いたくはないですが。
今回のbotは家族用に作った1日1アクセスの処理なので問題ないと考えましたが、スクレイピングって対象媒体(今回ならQiita)の規約や著作権法等に違反していないかどうか、少しドキドキします。
AWSで定期実行バッチを作りましたが、EC2でサーバ立てるより安くて良い感じでした。
ただ、Lambda関数のタイムアウトは最大でも5分らしいので、重いバッチ処理を実装するには向いてないですし、そもそもLambdaってこういったバッチ処理を載せる入れ物ではない気がします。
AWSもっと勉強しなきゃな、と思いました。