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. Web ページの更新を検知するツール を作った話 〜Web ページが更新されたら LINE でお知らせ!!〜 虎の穴ラボ株式会社 古賀広隆 -

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

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

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

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

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

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

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

    Toranoana Inc. All Rights Reserved.
  9. クラウド環境 Cloud Storage for Firebase → 得意なので Firebase Admin SDK

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

    サイトのスクショを撮る pngjs → 過去のスクショ(PNG)画像の読み込み pixelmatch → 過去のスクショと最新画像を比較する sharp → LINE 通知用に画像をリサイズ Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  11. 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.
  12. 3. 処理の流れ Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights

    Reserved.
  13. 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.
  14. 3-2. スクショを撮る await page.waitForTimeout(3000); // たまにロード完了してないときがある await page.screenshot({ fullPage: true,

    path: `${id}.new.png`, }); Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.
  15. 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.
  16. 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.
  17. 比較結果画像のサンプル Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

  18. 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.
  19. 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.
  20. 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.
  21. 3-8. 旧ファイルを削除、最新ファイルをリネーム して保存する fs.unlinkSync(`${id}.old.png`); fs.renameSync(`${id}.new.png`, `${id}.old.png`); Webページの更新を検知するツールを作った話 Copyright (C) 2022

    Toranoana Inc. All Rights Reserved.
  22. 4. 実際の表示 Webページの更新を検知するツールを作った話 Copyright (C) 2022 Toranoana Inc. All Rights

    Reserved.
  23. 4. サンプルサイト:変更前 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

  24. 4. サンプルサイト:変更後 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

  25. 4. サンプルサイト:実際の表示 Copyright (C) 2022 Toranoana Inc. All Rights Reserved.

  26. 5. さいごに! LINE Messaging API は、登録すると無料で使えます。 また、友達や家族との共有も QR コードや URL

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

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

  29. 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.
  30. 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.
  31. 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.
  32. 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.
  33. 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.
  34. 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.