cloud functions・pub/sub・puppetterで夜22時前後のテレビ番組情報を取得して生活を少し良くした話


夜の報道番組が結構好きです。
最近、テレビ東京のニュース番組であるWBSが22時台になったり、大谷翔平が想像以上に活躍して日本のプロ野球情報よりもMLB情報の方が欲しくなったり、緊急事態宣言やまんぼうで夜のカフェで作業できなくなったり、としたことで、何か生活のリズムが崩れて来た感覚を持ち始めていました。

そんななか、夜22時前後のテレビ番組情報をさくっとcloud functions・pub/sub・puppetterで取得して自分のslackに通知するだけでその辺の感覚が結構正常化されたので、ざっくり共有です:

結果

情報の取得

あまり使ったことなかったですが、こういう情報は動的なサイトが多いイメージがあったので、puppetterで取得してみました。
こんな感じのhtmlがあったとすると、

<table class="aaa">
<tbody>
<tr>
<td>
<div class="bbb">TITLE<div class="ccc">CONTENT</div></div>
</td>
<td></td>
<td></td>
</tr>
<tr><tr/>
<tr><tr/>
</tbody>
</table>

puppeterではこんな感じでしょうか。

import * as puppeteer from 'puppeteer';

// https://github.com/GoogleChrome/puppeteer/issues/3120
const browser = await puppeteer.launch({
    args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '-–disable-dev-shm-usage',
        '--disable-gpu',
        '--no-first-run',
        '--no-zygote',
        '--single-process',
    ],
    headless: true
});
const page = (await browser.pages())[0];
await page.goto(URL);

let listSelector = "table.aaa > tbody > tr > td";
const datas = await page.evaluate((selector) => {
    const list = Array.from(document.querySelectorAll(selector));
    var contents = [];
    for (let i = 0; i < list.length; i++) {
        if (list[i].querySelector("div.bbb") != null) {
            let title = list[i].querySelector("div.bbb").innerText;
            // 自分のみたい内容があるときは詳細取得
            if (title.includes("XXX")) {
                let programType;
                if (title.includes("XXX")) {
                    programType = "XXX"
                }
                let textContent = list[i].querySelector("div.bbb > div.ccc").innerText;
                var content = {
                    programType: programType,
                    textContent: textContent,
                };
                contents.push(content);
            }
        }
    }
    return contents;
}, listSelector);

page.evaluate()内では、page.click()が使えなかったのですが、性能面含めてもう少し良い書き方はある気がしています。。。
https://stackoverflow.com/questions/59867111/puppeteer-how-to-use-page-click-inside-page-evaluate

slackへの投稿

取得した内容は、slack側で権限を設定したりトークンを取得した後に、下記のようにslackにも投稿しておきます。slackに投稿されてから20分ぐらい放置しておくとメールが飛んできます。

const {WebClient} = require('@slack/web-api');
const token = "xabcd-123451234512-123451234512345-xXXXX~~~";
const channelId = "C012345~~~";
const web = new WebClient(token);
web.chat.postMessage({
    text: textContent + "\n" + url,
    channel: channelId,
}).catch((e: any) => {
    functions.logger.info(`end slack error:${e}`);
})

cloud functionsとpub/subで定期的に動かす

上記のスクリプトをcloud functionsとpub/subで下記のようにして定期的に動かします。メモリは512MBぐらいにしておいた方があまりエラーはないと思います。

import * as functions from "firebase-functions";
const runtimeOpts: RuntimeOptions = {
    // timeoutSeconds: 300,
    memory: '512MB'
}
exports.scheduledFunctionGetData = functions
    .runWith(runtimeOpts)
    .pubsub
    .schedule('0 */x * * *') //x時間ごとに動かす
    .timeZone('Asia/Tokyo') 
    .onRun(async (context) => {
         ※上記のスクリプト※
}

firestoreへの保存

何となくfirestoreにも入れておきます。データが溜まったらbig queryで何かしてみたいと思います。
ちなみに、 https://www.bravesoft.co.jp/seblog/archives/2213 を参考にさせて頂きました。

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const firestore = admin.firestore();

var docRef = firestore.collection("route22").doc(YYYYMMDD());

const now: Date = new Date();

docRef.collection(programType).doc(now.toDateString()).set({ 
        text: textContent,
        programType: programType,
        url: url,
        created: now
    }
);

function YYYYMMDD() {
    var dt = new Date();
    return dt.getFullYear()
        + (('00' + (dt.getMonth() + 1)).slice(-2))
        + (('00' + dt.getDate()).slice(-2));
}