今回はタイトルにもある通り、puppeteerのスクレイピング(とついでにブログの投稿検知)についてやっていきます。なぜなら最近プライベートで触ったからという、とても個人的な理由です。
puppeteerとは?
puppeteerというのは、Node.jsを用いてスクレイピングをするライブラリです。はい、死ぬほど雑な説明をしました。違うわ!とまではいかないまでもお叱りを受けそうですね。詳しくは公式サイトをご覧ください。公式の英語サイトに投げる暴挙。とにかくNode.jsでスクレイピングのできるライブラリです。しかもChromeを作っているGoogle製。使わない理由はありませんね。
どうやって使うの?
- Node.jsの最新版をインストールします
- 好きな場所にフォルダを作り、
npm init -y
と打ち、npm i puppeteer
します
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'example.png' }); await browser.close(); })();同じフォルダにexample.pngというファイルが作成されたと思います。ファイルには見知らぬサイトのスクリーンショットが。puppeteerがブラウザを画面に出したりせずに撮ってきてくれたスクリーンショットです。実際にこのページにアクセスしてみると、画像そのままのサイトが表示されたと思います。
これを応用することで、ブログの投稿を検知することが可能となります。RSSがあればこういうことをしなくても大丈夫だったりしますが、そもそもRSSが下火だったり、RSSを提供するようなサイトじゃなかったりもしますからね。
プログラムの流れ
前提条件:昨日や一週間前に取ってきたタイトル一覧を手元に持っている(持っていなければ、下記手順の1~2、5だけを行います)- ブログにアクセスする
- タイトル一覧を取得する
- 手元で持っているタイトル一覧と照合する
- 手元にないタイトルがあれば、そのタイトルを通知する
- 最新のタイトル一覧を手元に保存する
- おわり
import puppeteer, { Page } from "puppeteer"; import { readFileSync, writeFileSync } from "fs"; import csvParse = require("csv-parse/lib/sync") import csvStringify = require("csv-stringify/lib/sync") import { difference } from "./setUtils"; const CURRENT_FILE_NAME = 'current.csv'; const TITLE_SELECTOR = '.entry-title'; const readCurrentCsv = (): Set<string> | null => { try { const file = readFileSync(CURRENT_FILE_NAME); return new Set((csvParse(file) as Array<Array<string>>).flat()); } catch (error) { // Error NO ENTryらしいです if (error.code === 'ENOENT') { return null; } else { throw new Error("知らんエラー。君誰?"); } } } const fetchTitles = async (page: Page): Promise<Set<string>> => { const elements = await page.$$(TITLE_SELECTOR); return new Set(await Promise.all(elements.map(async ele => (await ele.getProperty('textContent'))!.jsonValue())) as Array<string>); } const writeCsv = (newTitles: Set<string>) => { writeFileSync(CURRENT_FILE_NAME, csvStringify([...newTitles].map(title => [title]))); } const notify = (diffTitles: Set<string>) => { console.info(` 「${[...diffTitles].join('、')}」が開発者ブログに投稿されました。見てみましょう! https://techblog.imagemagic.jp/ `.trim()); // みたいのを通知したりしたいよね } const main = async (page: Page) => { const currentTitles = readCurrentCsv(); const newTitles = await fetchTitles(page); if (currentTitles === null) { writeCsv(newTitles); return; } const diffTitles = difference(newTitles, currentTitles); if (!diffTitles.size) { return; } notify(diffTitles); writeCsv(newTitles); } (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://techblog.imagemagic.jp/'); try { await main(page); } finally { await browser.close(); } })();main関数の中身を見れば、先程のプログラムの流れと大体同じなことがわかると思います。このプログラムをたとえばAWSのLambdaに乗せて、CloudWatchで定期的に叩くなどすれば、差分検知通知システムの完成です。
細かいコードの解説は省きます。皆さんも興味のあるサイト(たとえば本テックブログ!)をスクレイピングしたりして、実際に触ってみてください。
※スクレイピングは少し油断するとアクセス負荷を相手のサーバーにかけてしまったり、そもそもスクレイピングを許可していないサイトもあります。使い方を間違えれば違法行為です。この辺りのサイトを見つつ、用法用量を守って正しくお使いください。