DenoのFreshを使用したスクレピングAPI作成
最近Deno+TypeScriptの勉強がてらFreshを使ってサイトスクレイピングを作っていました。
(動作に不要なファイルもけっこう入ってます)Final Fantasy XIVのロードストーンというサイトの自キャラのリテイナーの所持品を取得が主なスクレイピング内容です。
ゲームやってない人は動作のHTMLが見れないので実装の参考資料ということで……。
※なお、今回はDenoの基本的な動作はあまり説明してないです
使用モジュール
今回使用している主なモジュールはこんな感じ
- Fresh
- サーバーサイドレンダリング実装のフレームワーク
- deno-cheerio
- jQuery Coreなどを使って構築したサーバー特化のjQuery実装
- axiod
- axiosのDeno向けHTTPクライアント
- denoのstdライブラリー
- dotenv
- dotenvを使えるようにする
- testing
- Deno向けテストフレームワーク
- deepMerge
- 配列をDeepMerge可能にするモジュール
- dotenv
npmパッケージの直接読み込みにも対応したDeno1.25ですが、動作しないモジュールもあったので今回は全てDeno用に用意されたモジュールを使用しています。
Freshとは
JS/TS向けのフルスタックWebフレームワークです。中身はPreact(Reactの軽量実装的な)+JSXが採用されています。
特徴としては、サーバーでのジャスト・イン・タイム(JIT)レンダリングを行います。
最近では先にHTMLをビルドしておく方法がJS界隈では主流でしたが、Freshはリクエストごとにレンダリングを行う手法です。
インタラクティブなJSなどはアイランド
という手法を採用しています。これはサーバーサイドでレンダリングされつつも、JSとして独立したコンポーネントとして読み込まれます。
※今回はサーバーサイドのスクレイピングツールなのでアイランドは扱いませんでした……
Freshについては、すでにチュートリアルが用意されており、シンプルな内容なので一度触っておくことをお勧めします。
Freshインストール
これは何も考えずに公式通りに。
deno run -A -r https://fresh.deno.dev my-project
cd my-project
deno task start
最後のコマンドは、Denoでのnpmスクリプト的なものでdeno.json
に記載可能です。
{
"tasks": {
"fresh:start": "deno run -A --watch=./routes/ ./dev.ts",
"fresh:debug": "deno run -A ./dev.ts"
},
"importMap": "./import_map.json"
}
※リモートデバッグを行う際watchオプションは使用できないので別途taskを用意してます。
importMapは読み込むモジュールの一覧を記載します。
package.jsonみたいなものです。
今回使ったもの抜粋(フレームーワークのFrash分除く)
{
"imports": {
"axiod": "https://deno.land/x/axiod@0.26.1/mod.ts",
"cheerio": "https://deno.land/x/cheerio@1.0.6/mod.ts",
"dotenv/": "https://deno.land/std@0.157.0/dotenv/",
"config": "https://deno.land/std@0.157.0/dotenv/mod.ts",
"testing/": "https://deno.land/std@0.157.0/testing/",
"deepMerge": "https://deno.land/std@0.157.0/collections/deep_merge.ts"
}
}
Freshスタート
Freshのブートストラップとなるのは、dev.tsです。このファイルは特段弄りません。
index.ts
dev.tsは内部でindex.tsを呼びだすようになっているので、index.tsを見てみます。
インストール直後はrender
関数でtwind
というモジュールを呼び出しています。これはtailwindのin JS実装のようです。
詳細は公式サイト参照。
今回作成したのはスクレイピングAPIで返す値はJSONだったので、HTML+CSSはノータッチです。
ポートの変更
Freshのプロジェクト作成コマンドを使用した場合、ポート変更は main.ts
の最終行のstart関数で行えます(2022/07/13現在)。
// portを8001に変更した例
await start(manifest, { render, port: 8001 });
Freshのルーティング
Freshのルーティングはシンプルです。
routes/
api/
retainers.ts
routes/配下の構造がほぼそのままURLになります。
上記の場合だと https://localhost:8001/api/retainers
というURLになります。
作成直後のFreshのコードの場合、ルーティングファイルの中で直接HTML等の出力します。
ただ、そうなるとルーティングファイルが肥大化します。
ルーティングファイルはユーザーが触れる最初のFreshファイルです。
ここはFreshと密接に紐付くのであまり触らずに、受け取ったリクエストをさっさと別のモジュールに受け渡してしまいます。
Controller -> Service -> Repository
ルーティングが渡すモジュールはControllerとしました。
実際のところは分かりやすければなんでもいいんですが、サーバーサイドでやっていた経験から。
それぞれの役どころと流れを記載すると、
- Controllerはルーティングからリクエストを受け取り、
- なんやかんや処理してくれるServiceモジュールへ渡し、
- Repositoryはデータ(DBやストレージ)をCRUDし
- Controllerへ戻ってきた値をルーティングへ返す
こんな感じになってます。
Service
大半のモジュールはここで呼び出されています。そしてコードが長くなりがちな層(勢いで書くとよくあるController肥大化問題がServiceに移る)。
import 'dotenv/load.ts'
import axiod from 'axiod'
import { cheerio } from 'cheerio'
というわけで、Serviceにはスクレイピング処理を記載しました。
実際のスクレピング処理はaxiodで取得したデータをcheerioで解析し、ローカルストレージへデータを保存しています。
今回のスクリプトの場合、基本的には自分のローカル環境でサーバーを立ち上げて、そのままデータをローカルへ保存するという流れになっています。
そのため永続的なデータも特別にDBを用意することなく、ローカルストレージへデータ保存をしています。
cheerioはjQuery互換みたいなところがあるので、こんな感じでDOM取得します。
const $ = cheerio.load(data)
const dom = $('body > ...')
ちょっと使うくらいならjQueryと書き味は一緒なので、jQueryを扱ったことがあるならすんなり扱えるでしょう。
Repository
ローカルストレージへの保存・取り出しを行います。
Repositoryは組み込み関数しか使用しておらず、Denoに限らずどのエンジンでも動作するはずです。
続く
今回はDeno+Freshでスクレイピングしたので、今後保存したデータをブラウザーから呼び出せるようにNext.jsかFreshで作っていこうと思ってます。
Discussion