Webページの更新を検知するツールを作った話

170 Views

December 12, 24

スライド概要

とらのあなラボTechConferenceVol.2におけるLT大会「Webページの更新を検知するツールを作った話」の登壇資料です。
■イベント情報
https://yumenosora.connpass.com/event/241175/
■今後のイベントについてはこちら
https://yumenosora.connpass.com/
■虎の穴ラボ 採用サイト
https://yumenosora.co.jp/tora-lab/

profile-image

虎の穴ラボ株式会社は、主にとらのあな関連サービスのシステム開発を専門に担う、エンジニアの会社です。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

- Web ページの更新を検知するツール を作った話 〜Web ページが更新されたら LINE でお知らせ!!〜 虎の穴ラボ株式会社 古賀広隆 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

2.

自己紹介 所属 虎の穴ラボ株式会社 とらのあな通販コスト削減チーム 主な担当 フロントエンド、サーバサイド 推し 南條愛乃さんです! Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

3.

Webページの更新を検知するツールを作った話 子供の声が入るかも 100% フルリモートワーク で、今日も三重県の自宅から配信して います。 ご了承ください Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

4.

Webページの更新を検知するツールを作った話 あじぇんだ! 1. なぜ作成したか? 2. 各種フレームワーク/ライブラリの選定 3. 処理の流れ 4. 最後に Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

5.

Webページの更新を検知するツールを作った話 1. なぜ作成したか? Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

6.

Webページの更新を検知するツールを作った話 Web ページに変化があったら差分を画像で知りたいことがあった ので、個人的に作りました。 いきなり、個人 Web サイトに異常があったりとか Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

7.

Webページの更新を検知するツールを作った話 2. 各種フレームワーク/ライブラリの選定 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

8.

Webページの更新を検知するツールを作った話 言語 Node.js & TypeScript → 得意なので Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

9.

Webページの更新を検知するツールを作った話 クラウド環境 Cloud Storage for Firebase → 得意なので Firebase Admin SDK → 画像をアップロード Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

10.

Webページの更新を検知するツールを作った話 ライブラリ LINE Bot SDK → LINE 通知に Playwright → Web サイトのスクショを撮る pngjs → 過去のスクショ(PNG)画像の読み込み pixelmatch → 過去のスクショと最新画像を比較する sharp → LINE 通知用に画像をリサイズ Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

11.

システム構成イメージ ⑤ スクショのURLの 内容を取得 Saturday 10:12 AM Hey man, got a sec? Hi Tim, of course, just give me a couple minutes to finish breakfast. Read Friday LINE Messaging API Cloud Storage ③ スクショと差分を アップロード (公開) ④ スクショのURLと メッセージをAPIに送 信 ① スクショ取得 &保存 Web サイト等 RaspberryPi ② 画像の差分をとる Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

12.

Webページの更新を検知するツールを作った話 3. 処理の流れ Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

13.
[beta]
Webページの更新を検知するツールを作った話

3-1. Playwright で対象の Web ページを開く
const browser = await playwright.chromium.launch({
args: ["--ignore-certificate-errors", "--lang=ja,en-US,en"],
executablePath: "/usr/bin/chromium-browser",
});
// Basic
const context = await browser.newContext({
httpCredentials: {
username: "xxxxxxxx",
password: "xxxxxxxx",
},
});
const page = await context.newPage();
await page.goto(new URL(url).href); // Web
URL
await page.waitForLoadState("domcontentloaded");

認証がある場合

サイトの を指定する

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

14.
[beta]
Webページの更新を検知するツールを作った話

3-2. スクショを撮る
await page.waitForTimeout(3000); //
await page.screenshot({
fullPage: true,
path: `${id}.new.png`,
});

たまにロード完了してないときがある

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

15.

Webページの更新を検知するツールを作った話 3-3. 前回実行時の画像ファイルがあったら PNG を読み込む if (fs.existsSync(`${id}.old.png`) && fs.existsSync(`${id}.new.png`)) { // const file1 = fs.readFileSync(`${id}.old.png`) const file2 = fs.readFileSync(`${id}.new.png`) const img1 = PNG.sync.read(file1) const img2 = PNG.sync.read(file2) const { width, height } = img2 const diff2 = new PNG({ width, height }) 新旧ファイルが揃っているときは、比較する Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

16.

Webページの更新を検知するツールを作った話 3-4. 比較して、比較結果を画像ファイルで書き出 す const result2 = pixelmatch(img1.data, img2.data, diff2.data, width, height); fs.writeFileSync(`${id}.diff2.png`, PNG.sync.write(diff2)); Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

17.

比較結果画像のサンプル Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

18.

Webページの更新を検知するツールを作った話 3-5. 閾値以上の差分があったら、サムネイル画像 作成 if (result2 > threshold) { // sharp(`${id}.new.png`) .resize(512) // 512px .toFile(`${id}.new.512.png`, (err, info) => { if (err) { throw err } console.log(info) }) サムネイル向けの画像を作る 横幅 にリサイズ Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

19.
[beta]
Webページの更新を検知するツールを作った話

3-6. 差分ファイルと最新のファイルを Firebase
にアップロード

最新のスクショ画像をアップロード

//
const respNew = await getStorage()
.bucket()
.upload(`${id}.new.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-new.png`,
});
//
const respNew512 = await getStorage()
.bucket()
.upload(`${id}.new.512.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-new-512.png`,
});

最新のスクショ画像のサムネイルをアップロード

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

20.
[beta]
Webページの更新を検知するツールを作った話

3-7. LINE に通知を送る
const client = new line.Client(clientConfig.default);
const lineRespNew = await client.broadcast({
type: "image",
originalContentUrl: respNew[0].publicUrl(),
previewImageUrl: respNew512[0].publicUrl(),
});
const lineRespText = await client.broadcast({
type: "text",
text: url,
});

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

21.

Webページの更新を検知するツールを作った話 3-8. 旧ファイルを削除、最新ファイルをリネーム して保存する fs.unlinkSync(`${id}.old.png`); fs.renameSync(`${id}.new.png`, `${id}.old.png`); Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

22.

Webページの更新を検知するツールを作った話 4. さいごに! LINE Messaging API は、登録すると無料で使えます。 また、友達や家族との共有も QR コードや URL でできます。 他にも、LINE Messaging API を使って作りたい。 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

23.

Webページの更新を検知するツールを作った話 4. さいごに! LINE 通知は他にも、とらのあな通販の告知とかセール、新発売、 配送状況、購入通知の通知などの機能開発に使えるかも?と思い ました。 画像の差分もわかりやすくて、家族には好評でした。 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

24.

Webページの更新を検知するツールを作った話 ご清聴、ありがとうございました! Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

25.
[beta]
Appendix:ソースコード全録
import * as playwright from "playwright-chromium";
import * as fs from "fs";
import { PNG } from "pngjs";
import * as pixelmatch from "pixelmatch";
import { exit } from "process";
import { initializeApp, applicationDefault } from "firebase-admin/app";
import { getStorage } from "firebase-admin/storage";
import * as line from "@line/bot-sdk";
import * as clientConfig from "./channel-access-token";
import * as sharp from "sharp";
const getNewScreenShot = async (id: string, url: string) => {
const browser = await playwright.chromium.launch({
args: ["--ignore-certificate-errors", "--lang=ja,en-US,en"],
executablePath: "/usr/bin/chromium-browser",
});
const context = await browser.newContext({
httpCredentials: {
username: "xxxxxxx",
password: "xxxxxxx",
},
});
const page = await context.newPage();
await page.goto(new URL(url).href);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(3000);
await page.screenshot({
fullPage: true,
path: `${id}.new.png`,
});
};

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

26.
[beta]
Appendix:ソースコード全録
const sendLineMessage = async (id: string, url: string) => {
sharp(`${id}.new.png`)
.resize(512)
.toFile(`${id}.new.512.png`, (err, info) => {
if (err) {
throw err
}
console.log(info)
})
sharp(`${id}.diff2.png`)
.resize(512)
.toFile(`${id}.diff2.512.png`, (err, info) => {
if (err) {
throw err
}
console.log(info)
})
const YYYYMMDDHHmmss = new Date()
.toISOString()
.replace(/[^\d]/g, '')
.slice(0, 14)
initializeApp({
credential: applicationDefault(),
storageBucket: 'change-web-page-reminder.appspot.com',
})
Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

27.
[beta]
Appendix:ソースコード全録
const respOld = await getStorage()
.bucket()
.upload(`${id}.old.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-old.png`,
});
console.log("old publicUrl", respOld[0].publicUrl());
const respNew = await getStorage()
.bucket()
.upload(`${id}.new.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-new.png`,
});
console.log("new publicUrl", respNew[0].publicUrl());
const respNew512 = await getStorage()
.bucket()
.upload(`${id}.new.512.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-new-512.png`,
});
console.log("new publicUrl", respNew512[0].publicUrl());
const respDiff = await getStorage()
.bucket()
.upload(`${id}.diff2.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-diff2.png`,
});
console.log("diff publicUrl", respDiff[0].publicUrl());
const respDiff512 = await getStorage()
.bucket()
.upload(`${id}.diff2.512.png`, {
destination: `${id}-${YYYYMMDDHHmmss}-diff2-512.png`,
});
console.log("diff publicUrl", respDiff512[0].publicUrl());

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

28.

Appendix:ソースコード全録 } const client = new line.Client(clientConfig.default) const lineRespNew = await client.broadcast({ type: 'image', originalContentUrl: respNew[0].publicUrl(), previewImageUrl: respNew512[0].publicUrl(), }) console.log(lineRespNew) const lineRespDiff = await client.broadcast({ type: 'image', originalContentUrl: respDiff[0].publicUrl(), previewImageUrl: respDiff512[0].publicUrl(), }) console.log(lineRespDiff) const lineRespText = await client.broadcast({ type: 'text', text: url, }) console.log(lineRespText) Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

29.
[beta]
Appendix:ソースコード全録
const main = async (id: string, url: string, threshold = 0) => {
// url
await getNewScreenShot(id, url)
if (fs.existsSync(`${id}.old.png`) && fs.existsSync(`${id}.new.png`)) {
//
const file1 = fs.readFileSync(`${id}.old.png`)
const file2 = fs.readFileSync(`${id}.new.png`)
const img1 = PNG.sync.read(file1)
const img2 = PNG.sync.read(file2)
const { width, height } = img2
const diff2 = new PNG({ width, height })
console.log('pixelmatch2', img1.width, img1.height, img2.width, img2.height)
const result2 = pixelmatch(
img1.data,
img2.data,
diff2.data,
width,
height,
{
// threshold: 0.1,
// includeAA: true,
// alpha: 0.5,
// aaColor: [255, 255, 0],
// diffColor: [255, 0, 0],
// diffColorAlt: [0, 0, 255],
// diffMask: false,
},
)
console.log('pixelmatch2', result2)
fs.writeFileSync(`${id}.diff2.png`, PNG.sync.write(diff2))

をヘッドレスブラウザで開いて、新しくスクリーンショットを取る

新旧ファイルが揃っているときは、比較する

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

30.
[beta]
Appendix:ソースコード全録
if (result2 > threshold) {
//
LINE
await sendLineMessage(id, url)
}
//
fs.unlinkSync(`${id}.old.png`)

画像をクラウドにアップロードして、

にメッセージ送信する

ファイル名を変更

}
fs.renameSync(`${id}.new.png`, `${id}.old.png`)

}
const all = async () => {
await main(
'id1',
'url1',
)
await main(
'id2',
'url2',
)
await main('id3', 'url3', 8)
}

Promise.all([all()])
.then((resutls) => {
console.log('OK!', resutls)
})
.catch((errors) => {
console.error('ERROR.', errors)
exit()
})
.finally(() => {
console.log('EXIT')
exit()
})

Copyright (C) 2022 Toranoana Inc. All Rights Reserved.