ほんじゃらねっと

ダイエット中プログラマのブログ

Node.jsでテレビの映画放映情報をWebスクレイピングしてSlackに通知する

最近素晴らしいことに家族内でSlackを使ってやりとりするようになった。

せっかくなので何かボット的なものを作ろう、ということで

昔Clojureで作った、

「映画情報をスクレイピングして通知する」スクリプトのNode.js版を作って、

定期的にSlackに通知するようにしてみたい。

blog.honjala.net

仕様

下記のページで今後1週間のテレビ映画放映予定を掲載してくれているので、

今週TV放映予定の映画

この内容を週2回程度スクレイピングして家族用Slackのgeneralチャンネルに通知する。

あらかじめ家族用Slackに「Bots」Appを追加して、

APIトークンを取得しておき、

generalチャンネルに作成したボットをAppとして追加しておく。

このスクリプトを実行すると、下記のような投稿が追加されるイメージ。

f:id:piro_suke:20180913002614p:plain

つくる

構成としては、

「メイン部分」「映画情報スクレイピング部分」「Slack投稿部分」に分けて作成する。

まずは映画情報スクレイピング部分。

「cheerio-httpcli」ライブラリを使う。

movie_scrape.js

const httpClient = require('cheerio-httpcli');

async function fetch() {
    const baseUrl = 'http://cinema.pia.co.jp/title/tvlist/';
    const scheduleList = [];

    const result = await httpClient.fetch(baseUrl, 'sjis');
    const $ = result.$;
    $('tr', '#mainTvSchedule').each(function () {
        const row = $(this);
        const captions = $('.commonCaption', row).map(function () { return $(this).text().replace(/([\s\t\n]| )+/g, ' '); }).get();
        scheduleList.push({
            title: $('h3 a', row).text().trim(),
            date: $('.date', row).text().trim().replace(/([\s\t\n]| )+/g, ' '),
            start: $('.time .start', row).text().trim(),
            end: $('.time .end', row).text().trim(),
            ch: $('.star strong', row).text().trim(),
            caps: captions
        });
    });

    return scheduleList;
}

function stringifyList(scheduleList) {
    return scheduleList.map(schedule => stringify(schedule)).join('\n\n');
}

function stringify(schedule) {
    let text = schedule.date + " " + schedule.start + "-" + schedule.end + "(" + schedule.ch + ")\n";
    text += schedule.title + "\n";
    text += schedule.caps.join('\n');
    return text;
}

module.exports = {
    fetch,
    stringifyList,
    stringify
}

fetch関数でスクレイピングして、stringify系関数でテキストとして整形してる。

スクレイピング結果を取得するところでthisを使っているのだけど、

アロー関数がthisを束縛しないことを忘れてて全然取得できず、しばらくハマった。

thisを使う時はちゃんと「function () {}」を使おう。

続いて、Slack投稿部分。

slack.js

const request = require('request');

module.exports = {
    postMessage: async (token, channel, text) => {
        const options = {
            headers: {
                'Authorization': 'Bearer ' + token
            },
            form: {
                channel: channel,
                text: text,
                as_user: true
            },
            json: true
        };
        return new Promise((resolve, reject) => {
            request.post('https://slack.com/api/chat.postMessage', options, (error, res, body) => {
                if (error) {
                    reject(error);
                }

                resolve({
                    response: res, 
                    body: body
                });
            });
        });
    }
};

postMessage関数にトークンとチャンネルと投稿内容を渡したら投稿できる。

最後にメイン部分:

index.js

const slack = require('./slack');
const movieScrape = require('./movie_scrape');

async function main() {

    const scheduleList = await movieScrape.fetch();
    const scheduleListText = movieScrape.stringifyList(scheduleList);
    const slackMessageText = "<映画情報>\n\n" + scheduleListText;
    
    const token = "<APIトークン>";
    await slack.postMessage(token, "#general", slackMessageText);
}

main();

スクレイピング用モジュールとSlack投稿モジュールを呼び出し。

割とシンプルにできた。

おわり

これで子どもたちがジブリ映画や細田守監督映画を見逃すこともなくなるだろう。

Slack通知系は今回作ったSlack投稿モジュールで使いまわせそう。

時をかける少女

時をかける少女