🍋

DenoのFreshを使用したスクレピングAPI作成

2022/10/03に公開

最近Deno+TypeScriptの勉強がてらFreshを使ってサイトスクレイピングを作っていました。
https://github.com/naoyukik/deno-ffxiv-scraping
(動作に不要なファイルもけっこう入ってます)

Final Fantasy XIVのロードストーンというサイトの自キャラのリテイナーの所持品を取得が主なスクレイピング内容です。
ゲームやってない人は動作のHTMLが見れないので実装の参考資料ということで……。

※なお、今回はDenoの基本的な動作はあまり説明してないです

使用モジュール

今回使用している主なモジュールはこんな感じ

  • Fresh
    • サーバーサイドレンダリング実装のフレームワーク
  • deno-cheerio
    • jQuery Coreなどを使って構築したサーバー特化のjQuery実装
  • axiod
    • axiosのDeno向けHTTPクライアント
  • denoのstdライブラリー
    • dotenv
      • dotenvを使えるようにする
    • testing
      • Deno向けテストフレームワーク
    • deepMerge
      • 配列をDeepMerge可能にするモジュール

npmパッケージの直接読み込みにも対応したDeno1.25ですが、動作しないモジュールもあったので今回は全てDeno用に用意されたモジュールを使用しています。

Freshとは

JS/TS向けのフルスタックWebフレームワークです。中身はPreact(Reactの軽量実装的な)+JSXが採用されています。
特徴としては、サーバーでのジャスト・イン・タイム(JIT)レンダリングを行います。
最近では先にHTMLをビルドしておく方法がJS界隈では主流でしたが、Freshはリクエストごとにレンダリングを行う手法です。
インタラクティブなJSなどはアイランドという手法を採用しています。これはサーバーサイドでレンダリングされつつも、JSとして独立したコンポーネントとして読み込まれます。
※今回はサーバーサイドのスクレイピングツールなのでアイランドは扱いませんでした……

Freshについては、すでにチュートリアルが用意されており、シンプルな内容なので一度触っておくことをお勧めします。

Freshインストール

https://fresh.deno.dev/docs/getting-started/create-a-project

これは何も考えずに公式通りに。

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実装のようです。
詳細は公式サイト参照。
https://twind.style/

今回作成したのはスクレイピング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としました。
実際のところは分かりやすければなんでもいいんですが、サーバーサイドでやっていた経験から。

それぞれの役どころと流れを記載すると、

  1. Controllerはルーティングからリクエストを受け取り、
  2. なんやかんや処理してくれるServiceモジュールへ渡し、
  3. Repositoryはデータ(DBやストレージ)をCRUDし
  4. 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