🤖

Cloudflare Workers (Unboundモード)で Webクローラー&スクレイパーを作り、ドキュメントのアップデートを把握する

2023/03/20に公開

Cloudflareは2023年3月20日現時点で、AWSでいうところのWhat's newがなく、中の人でもアップデートの把握に少し苦労しています。
無いときは作りましょう!ということで日曜大工的にWeb Crawler & Scrapperを作ってみました。

処理すべき量が予想より多かったので、通常のWorkersプランでは動作せず、最上位のUnboundedモードを使いました。プラン毎の制限はこちらにまとまっています

今回ドキュメントのトップ階層HTML(https://developers.cloudflare.com/workers/)を読み込み、そこからリンクが張られているHTMLを再度読み込み同じ処理を繰り返す、というルーチンを書いたのですが、単一のWorkers処理では「50/request」という制限がありひっかかったため、プランをUnboundedに変更し、上限を「1000/request (Unbound)」まで緩和し実行しました。
この辺りやはり中の人の役得でしょうか。尚、We are Hiring!中です。

ソースは以下です。

export async function kvupdatesub(url, datetime, env) {

	const response = await fetch(url);
	const html = await response.text();

	let regex = /<time[^>]*>(.*?)<\/time>/g;
	let match = regex.exec(html);
	let datetimesub = match[0];

	const getCache = await env.CFUPDATE.get(url);

	if (getCache === null) {
		console.log(`new`, url)
		env.CFUPDATE.put(url, datetimesub);
		//通知を出す
	}
	else if (getCache === datetimesub) {
		console.log(`same`, url)
		//通知を出さない
	}
	else {
		console.log(`updated`, url)
		env.CFUPDATE.put(url, datetimesub);
		//通知を出す
	}
}

export default {
	async fetch(request, env, ctx) {
		// 特定のURLからHTMLを取得
		const url = "https://developers.cloudflare.com/workers/";
		const response = await fetch(url);
		const html = await response.text();

		//HTMLからtimeタグの情報のみを抜き出す
		let regex = /<time[^>]*>(.*?)<\/time>/g;
		let match = regex.exec(html);
		let datetime = match[0];

		//KVへ新規URLか過去と同じかUpdateされているかの判断を行う。
		//新規やUpdateの場合、KVのアイテムをUpdateして通知を出す。
		await kvupdatesub(url, datetime, env);

		//読み込んだHTMLからリンク一覧(/workers配下のみ)を取得
		// htmlからaタグ(リンク)を抽出
		let links = html.match(/<a[^>]*>/g);

		// aタグからhref属性値(URL)を抽出
		let urls = links.map(function (link) {
			let href = link.match(/href=([^ >]*)/);
			if (href) {
				if ((href[0].indexOf('http') === -1) && (href[0].indexOf('#') === -1) && (href[0].indexOf('href=/workers') !== -1)) {
					let str = href[0].replace('href=/workers/', '');
					if (str.length !== 0 || str !== null) {
						return url + str;
					} else return ("test")
				} else return ("test")
			} else return ("test")
		});

		for (let url of urls) {
			if (url !== "test") {
				//KVへ新規URLか過去と同じかUpdateされているかの判断を行う。
				//新規やUpdateの場合、KVのアイテムをUpdateして通知を出す。
				await kvupdatesub(url, datetime, env);
			}
		}
		return new Response("finished successfully");
	},
};

先日WorkersでConnpassのAPIをたたき、新規イベントを検知して、Discordに通知を行うBotの記事を上げたのですが、基本的にはその延長です。
Cloudflare Workers から Connpassのイベント一覧を取得してDiscordに通知を出すBotを作成した
だた、前回のBotは30分に1回、cron起動させているため、

async scheduled(controller, env, ctx)

を用いましたが、今回は自分のタイミングで更新がかかっているドキュメントを把握したいため、都度ブラウザから実行するため以下としました。夜中や別件に集中しているときに自分が読むべきドキュメントのアップデート通知を受け取りたくないためです💦

async fetch(request, env, ctx)

やっていることは単純です。

const url = "https://developers.cloudflare.com/workers/";
const response = await fetch(url);
const html = await response.text();

この部分で
https://developers.cloudflare.com/workers/
からまずindex.htmlを読み込みます。
当初このhtmlのハッシュ値を取得して一意性を確認しようとしていましたが、動的に生成される部分があるため、ハッシュ値が毎回異なっていたため、HTMLの<time>タグを取得します。

let regex = /<time[^>]*>(.*?)<\/time>/g;
let match = regex.exec(html);
let datetime = match[0];

注意;Cloudflareは公式にこの<time>タグをドキュメント一意性担保に使えるとは宣言していませんが、とりあえず動きそうです。
その後以下のルーチンでKVへデータの突合せ
1.URLが存在しているかいないか
2.URLが存在していない場合新規保存→通知
3.URLが存在している場合→タイムスタンプ確認→差分があれば上書き→通知
を行います。今回は通知処理は書いていないですがDiscordに飛ばす場合はこの記事を参考にしてください。
当面は自分専用のアドホック実行にしますのでconsole.logで運用します。

await kvupdatesub(url, datetime, env);

この際KVの値などBinding情報が含まれているenvを明示的にfunctionで渡さないと動作しませんでした。勝手にGlobalオブジェクトと認識していましたが違ったようです。

let links = html.match(/<a[^>]*>/g);
		let urls = links.map(function (link) {
			let href = link.match(/href=([^ >]*)/);
			if (href) {
				if ((href[0].indexOf('http') === -1) && (href[0].indexOf('#') === -1) && (href[0].indexOf('href=/workers') !== -1)) {
					let str = href[0].replace('href=/workers/', '');
					if (str.length !== 0 || str !== null) {
						return url + str;
					} else return ("test")
				} else return ("test")
			} else return ("test")
		});

その後、HTMLの中から<a>タグ項目をまず抜き出しさらに"href"部分を抜き出しリンクを特定します。https://developers.cloudflare.com/workers/ 配下のURLのみに後続の処理を限定させるため、色々文字列を除外したり、nullをtestという文字列で置き換えたりごちゃごちゃしました。今後宿題1として綺麗にします。(nullのままだとなぜか[str !== null]を通り抜けてきました・・・要調査)

あとはURL文字列を組み立て、URL単位で以下を呼び出しindex.htmlと同じ作業を繰り返します。

await kvupdatesub(url, datetime, env);

この際URL毎にfetchのsubrequestが発生することになりますが、100弱となったため、Unboundedモードが必要となりました。

https://developers.cloudflare.com/workers/
を変更すればその他サービスのドキュメントでも動作します。ただし<time>タグの書き方次第ですが少なくともCloudflareドキュメントは大丈夫そうです。

さて個人備忘録の宿題です。
1.if文やforループなどきれいにできると思うので、後日、日を改めて格闘する
2.await/async/Promiseを雰囲気で使っているので処理量の制限ができていない。この辺りは要学習です。

Discussion