はじめに
最近仕事でAWSを触る機会が多くなってきたので、インプットのために様々な関連記事を読んでいます。
その中でもよく読んでるのがDevelopers.IOさんの記事です。
投稿されるAWSの記事には「こんなサービスがあるんだ!」「こういう使い方もできるんだ!」という発見がよくあるので、毎日軽く目を通すようにしてます。
ただ、新着記事が投稿されてるかをブラウザで毎日確認するなんてこと、怠惰な僕にはできません。
なので、日次で新着記事の通知がSlackに飛ぶように自動化しました。
#やりたいこと/前提
Developers.IOに投稿されるAWS関連の新着記事を日次でslackに通知する
利用するツール/ライブラリ
・GAS
・Parser(GASライブラリ)
・Slack
事前準備
・Slackにて、Incoming WebhookURLと通知先チャンネルを作成しておく
作っていく
新しいGASプロジェクトを作成する
GASとは、Googleが提供しているJacaScriptをベースにしたプログラミング言語です。
Googleが提供する様々なクラウドサービス(Google Calendar, Spread Sheetなど)を操作する目的で作られてます。
実行環境(プログラムを実行する場所)もgoogleが用意してくれているので、すぐに使い始めることができます。
今回のプログラムにGoogleサービスを利用する場面は1つも出てこないのですが、以下の理由からGASで作ることにしました。
・標準で利用できる「トリガー」が便利
・実装&利用までのスピードが速い
ではGASプロジェクトを作成します。プロジェクト名は適当につけちゃいます。
GASプロジェクトにParserライブラリを追加する
今回は、htmlを解析・スクレイピングするためにParserというライブラリを使用します。
プログラムを作るときに外部ライブラリを使うことは、言語を問わずよくあることだと思います。GASも例外ではありません。
GASでライブラリを追加するには、追加したいライブラリのスクリプトIDを調べておく必要があります。
ParserライブラリのスクリプトIDは「1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw」です。
では追加します。
コードを実装していく
下準備は終わったので、あとはゴリゴリ実装していくだけです。
コードを記載する前にプログラムの流れを簡単に説明しておきます。
大まかなプログラムの流れ
- トリガーによってプログラム実行開始
- スクレイピング対象の複数のサイトページに対して、繰り返し以下の処理を行う
- 対象のサイトページからhtmlを取得
- htmlをスクレイピングして記事の一覧を取得
- 昨日投稿された記事のみ抽出
- 「2」で得た記事情報全てをslackに通知
では一つ一つコードの説明をしていきます。(注意:見やすさのため、コードブロックの拡張子はgsでなくjsにしてます)
main.gs
main()
はトリガーに実行される関数で、このプログラムのエントリーポイントです。
getNewAWSTopics()
(後述)で記事一覧を取得しており、その情報をpostSlack()
(後述)で1つ1つslackに通知してます。
function main() {
let topics = getNewAWSTopics();
topics.forEach(topic => {
postSlack(topic);
});
}
topic_developersio.gs
Developsers.IOの指定サイトページから新着記事を取得するために必要な関数群です。
Developers.IOではhttps://dev.classmethod.jp/tags/<tag-key>
にアクセスすると、<tag-key>
がついてる記事一覧を見ることができます(参考ページ)。
コードの実装では、見たいタグのurl1つ1つにアクセスして記事一覧を取得するという形を取っています。
getNewAWSTopics()
がこの関数群の大元で、AWS_TAGs
が付いてる新着記事の情報を取得してメッセージに整形してから一覧を返します。
_createNewURLs(AWS_TAGs)
は、やってることは単純でhttps://dev.classmethod.jp/tags/<tag-key>
のリストを返します。
_extractYesterdayTopics(url)
は、urlにアクセスして得たhtmlを解析し、昨日投稿された記事情報(タイトル/記事url/投稿日)だけを取得して返します。
Parser.data(html).from('<from-str>').to('<to-str>').iterate();
の部分にだけParserライブラリを使っています。
htmlの<from-str>
から<to-str>
に挟まれた(注意:非貪欲)文字列を抜き出せすことができ、iterate()
はhtmlの中に存在するこの複数のセットをリストで返すことをしています。
例えば以下のhtmlに対して、Parser.data(html).from('<p class="hoge">').to('</p>').iterate();
を実行すると、[aaa, ccc]
が返されます。
<p class="hoge">aaa</p>
<p class="fuga">bbb</p>
<p class="hoge">ccc</p>
DEVELOPERSIO_DOMAIN = "https://dev.classmethod.jp";
DEVELOPERSIO_TAG_BASE_URL = DEVELOPERSIO_DOMAIN + "/tags/";
AWS_TAGs = [
'aws', 'ec2', 'lambda', 'Route53'
]
function getNewAWSTopics() {
let topics = [];
_createTagURLs(AWS_TAGs).forEach(url => {
topics = topics.concat(_extractYesterdayTopics(url));
});
return [...new Set(topics.map(x => x.getMsg()))];
}
class Topic {
constructor(title, url, date) {
this.title = title;
this.url = url;
this.date = date;
}
getMsg() {
return this.title + '\n' + this.url;
}
isYesterday() {
let date = new Date(this.date);
let yesterday = new Date();
yesterday.setDate(yesterday.getDate()-1);
if (yesterday.getFullYear() != date.getFullYear()) return false;
if (yesterday.getMonth() != date.getMonth()) return false;
if (yesterday.getDate() != date.getDate()) return false;
return true;
}
}
function _extractYesterdayTopics(url) {
let res = UrlFetchApp.fetch(url);
let html = res.getContentText('utf-8');
let topic_urls = Parser.data(html).from('<div class="post-container" data-v-0c1f62df><a href="').to('"').iterate();
let topic_dates = Parser.data(html).from('<p class="date" data-v-0c1f62df>').to('</p>').iterate();
let topic_titles = Parser.data(html).from('<h3 class="post-title" data-v-0c1f62df>').to('</h3>').iterate();
let yesterdayTopics = [];
for (let [url, date, title] of zip(topic_urls, topic_dates, topic_titles)) {
let topic = new Topic(title.trim(), DEVELOPERSIO_DOMAIN + url.trim(), date);
if (topic.isYesterday()) yesterdayTopics.push(topic);
}
return yesterdayTopics;
}
function _createTagURLs(tags) {
let urls = tags.map(tag => {
return DEVELOPERSIO_TAG_BASE_URL + tag + '/';
});
return urls;
senders.gs
postSlack(msg)
で、msgをSLACK_CHANNEL_NEWS_AWS
チャンネルに送信しています。
通知するメッセージ内容を変えたい場合はpayload
の中身をいじってみてください。
Reference: Message payloads | Slack
SLACK_CHANNEL_NEWS_AWS = "news_aws";
SLACK_WEBHOOK_URL = "<webhook-url>";
function postSlack(msg) {
let options = {
"method" : "POST",
"headers" : {"Content-type":"application/json"},
"payload" : JSON.stringify({
"text": msg,
"channel": SLACK_CHANNEL_NEWS_AWS,
"username": "news_deliver",
"icon_emoji": ":truck:"
})
};
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}
utils.gs
僕は普段pythonを使っていることからjsでもzip関数を使いたい!となったんですが、標準では存在しなかったためutils.jsとして実装しています。
このコードは以下を丸パクリしております。作者さんありがとうございます。
JavaScript で2つ以上の複数の配列を同時に for 文で回す。 | 民主主義に乾杯
function* zip(...args) {
const length = args[0].length;
// 引数チェック
for (let arr of args) {
if (arr.length !== length){
throw "Lengths of arrays are not eqaul.";
}
}
for (let index = 0; index < length; index++) {
let elms = [];
for (arr of args) {
elms.push(arr[index]);
}
yield elms;
}
}
トリガーで定期起動時間を設定する
GASには作成したプログラムを定期的に実行してくれる「トリガー」という大変便利な仕組みがあります。
これを使って毎日0~1時にmain関数を実行してもらいます。
以下のように追加すれば完了です。
最終的なファイル構成
動作イメージ
関数とトリガーがうまく動いていれば、以下のように毎日0~1時に新着記事のお知らせが届きます。
最後に
自動化ってやる前は絶対価値あると思ってたのに、いざ作ってみたらそんな必要なかったなていう現象よくある気がする。(僕だけ?)
javascript/GASはテンで素人なので、そこのところはご了承ください。