Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

とらのあなラボTechConferenceVol.2におけるLT大会「Webページの更新を検知するツールを作った話」の登壇資料です。

■イベント情報
https://yumenosora.connpass.com/event/241175/

■今後のイベントについてはこちら
https://yumenosora.connpass.com/

■虎の穴ラボ 採用サイト
https://yumenosora.co.jp/tora-lab/

More Decks by 虎の穴ラボ株式会社

Other Decks in Programming

Transcript

  1. あじぇんだ! 1. なぜ作成したか? 2. 各種フレームワーク/ライブラリの選定 3. 処理の流れ 4. 実際の表示 5.

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

    → 画像をアップロード Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  3. ライブラリ LINE Bot SDK → LINE 通知に Playwright → Web

    サイトのスクショを撮る pngjs → 過去のスクショ(PNG)画像の読み込み pixelmatch → 過去のスクショと最新画像を比較する sharp → LINE 通知用に画像をリサイズ Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  4. RaspberryPi Cloud Storage 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 Web サイト等 ① スクショ取得 &保存 ② 画像の差分をとる ③ スクショと差分を アップロード (公開) ④ スクショのURL と メッセージをAPI に送 信 ⑤ スクショのURL の 内容を取得 システム構成イメージ Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  5. 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"); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  6. 3-2. スクショを撮る await page.waitForTimeout(3000); // たまにロード完了してないときがある await page.screenshot({ fullPage: true,

    path: `${id}.new.png`, }); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  7. 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 }) Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  8. 3-4. 比較して、比較結果を画像ファイルで書き出 す const result2 = pixelmatch(img1.data, img2.data, diff2.data, width,

    height); fs.writeFileSync(`${id}.diff2.png`, PNG.sync.write(diff2)); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  9. 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) }) Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  10. 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`, }); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  11. 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, }); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  12. 5. さいごに! LINE Messaging API は、登録すると無料で使えます。 また、友達や家族との共有も QR コードや URL

    でできます。 他にも、LINE Messaging API を使って作りたい。 Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  13. 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`, }); }; Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  14. 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', }) Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  15. 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()); Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  16. 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) } Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  17. 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)) Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  18. 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() }) Appendix:ソースコード全録 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.