クローリングをシュッとやるのに、Crawleeが便利だった

スクレイピングしたいときって、あると思います。 Crawlee という OSS が便利だったので、共有します。

背景

スクレイピングしようと思うと、得意な言語でクローリングプログラムを書いて、html をスクレイピングすると思います。 私は、Node.js が得意なので、fetch + jsdom で書くことが多いです。ブラウザレンダリングが必要な場合、ヘッドレスブラウザを使うこともあります。 毎回これを組み立てるのが、ちょっと面倒だなと思います。そういうときに、Crawle という OSS が便利でした。

Crawle

https://crawlee.dev/ より引用します。

Crawlee is a web scraping and browser automation library. It helps you build reliable crawlers. Fast. Crawlee won't fix broken selectors for you (yet), but it helps you build and maintain your crawlers faster.

Crawlee は壊れたセレクタを直せませんが、クローラーを素早く作ることができます。

Crawle の良いところ

Crawle の良いなとおもった特徴を挙げます。

crawlee のテンプレートがある

crawlee は、npx crawlee create でコード生成できます。

$ npx crawlee create my-crawler
? Please select the template for your new Crawlee project (Use arrow keys)
❯ Getting started example [TypeScript]
  Getting started example [JavaScript]
  CheerioCrawler template project [TypeScript]
  PlaywrightCrawler template project [TypeScript]
  PuppeteerCrawler template project [TypeScript]
  CheerioCrawler template project [JavaScript]
  PlaywrightCrawler template project [JavaScript]

TypeScript のサポートがあります。 また、Crawler はデフォルトで plain HTTP crawler である Cherrio を採用しています。 必要に応じて、Playwright や Puppeteer を使うことができますし、Crawler の切り替えもインターフェースが揃っているため、簡単にできます。

RequestQueue という仕組み

クローラで、復数の URL にアクセスすることは、よくあると思います。 リクエストは、RequestQueue というキューで管理され、自動的にクローラがアクセスしていきます。 キューはユニークな URL で管理されるため、重複したアクセスはありません。

この仕組みは、次のような簡単なコードで実現できます。

import { RequestQueue } from "crawlee";
const requestQueue = await RequestQueue.open();
await requestQueue.addRequest({ url: "https://crawlee.dev" });

さらに、enqueueLinks という機能があります。これは、アクセスしているページの anchor の URL を RequestQueue に追加します。 次のコードが、enqueueLinks の例です。

import { CheerioCrawler } from "crawlee";
const crawler = new CheerioCrawler({
  async requestHandler({ enqueueLinks }) {
    await enqueueLinks();
  },
});
await crawler.run(["https://crawlee.dev"]);

enqueueLinks には、様々なオプションがあります。

例えば、リンクを globs でフィルタリングしたり、anchor のセレクタを指定できたりします。

データは JSON で保存される

スクレイピングで手に入れたデータは、json で保存できます。

例えば、リクエストした URL を集めたいときは、次のようなコードです。

import { CheerioCrawler, Dataset } from "crawlee";

const crawler = new CheerioCrawler({
  async requestHandler({ request }) {
    await Dataset.pushData({ url: request.url });
  },
});
await crawler.run(["https://crawlee.dev"]);

保存先は、{PROJECT_FOLDER}/storage/datasets/default/ になります。 めちゃくちゃく簡単にデータが保存できます。

終わりに

Crawlee の SaaS として、Apify があります。これで気軽に試してみるのもありかもしれません。

turborepo-remote-cache でキャッシュサーバをセルフホストした

vercel 製の turborepo という ビルドシステムが爆速なモノレポツールがあります。 爆速にする機能の 1 つに、リモートキャッシュというものがあります。 この機能は vercel のキャッシュサーバを使うのですが、キャッシュサーバをセルフホストする方法もあります。 今回は、それを紹介します。

なぜ、セルフホストしたいのか

vercel のキャッシュサーバを使う場合、vercel のアカウントが必要です。 vercel の pricingを見ると、個人利用(Hobby)では無料ですが、会社(Pro)で使うとすると、$20 per user / month という価格になります。費用対効果に見合うならそれで良いかもしれませんが、まだそれがわからない段階でコストをかけられない場面もあると思います。そこで、公式にも書いてあるとおり、キャッシュサーバをセルフホストする方法があります。

ローカルで、やってみた

実際に試してみました。ソースコードは、次のリンクにあります。

手元に Git clone して、README に従って動作確認できると思います。必要なソフトウェアは、Docker と Yarn です。

キャッシュサーバの準備

セルフホストする場合、キャッシュサーバを建てる必要があります。 キャッシュサーバは、https://github.com/fox1t/turborepo-remote-cache を使うと良いです。 Docker イメージが公開されているので、それを使っても良いですし、自前で docker build しても良いです。

キャッシュサーバには、最低でも次の 2 つを環境変数を設定する必要があります。

  • TURBO_TOKEN
    • turborepo と api を繋げるための TOKEN
  • STORAGE_PATH
    • キャッシュオブジェクトを保存するパス
    • STORAGE_PROVIDER が s3 を指定する場合は、バケット

簡単にするため、次の.env ファイルを用意しました。

# .env
TURBO_TOKEN=mytoken
STORAGE_PATH=/storage/

あとは、キャッシュサーバを起動するために、docker-compose を書きます。

# docker-compose.yml
services:
  remote-cache:
    image: fox1t/turborepo-remote-cache:latest
    env_file:
      - .env
    ports:
      - "3000:3000"

次のコマンドで、キャッシュサーバを起動しましょう。

$ docker-compose up -d

これで、キャッシュサーバは PORT:3000 番 で起動します。

turbo build

では、実際に turborepo からつながるか、試してみます。

turborepo は、npx create-turbo@latest で作成できます。 作成後、作成したフォルダで次のコマンドを実行します。

$ yarn
$ yarn turbo run build --team="team_myteam" --token="mytoken" --api="http://localhost:3000"

turbo コマンドのオプションで、3 つ指定します。

  • team
    • キャッシュを保存するときの名前空間の役割
  • token
  • api
    • キャッシュサーバの URL

実行すると次のログが表示されるはずです。

yarn run v1.22.19
turbo run build --team=team_myteam --token=mytoken --api=http://localhost:3000
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote computation caching enabled
web:build: cache miss, executing 082bae5de9b1745f
docs:build: cache miss, executing 5a55c6367c8caf01
...

Remote computation caching enabled で、リモートキャッシュが有効となりました。 初回の場合、cache miss となります。ハッシュ値は、web: 082bae5de9b1745fdocs:5a55c6367c8caf01 になります。 キャッシュがローカルに保存されるため、削除します。

$ rm -rf node_modules/.cache/turbo

ではもう一度、turbo build してみましょう。

$ yarn turbo run build --team="team_myteam" --token="mytoken" --api="http://localhost:3000"
yarn run v1.22.19
$ /Users/silverbirder/docker/node/turborepo-with-selfhost-remote-cache/node_modules/.bin/turbo run build --team=team_myteam --token=mytoken --api=http://localhost:3000
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote computation caching enabled
docs:build: cache hit, replaying output 5a55c6367c8caf01
web:build: cache hit, replaying output 082bae5de9b1745f

どうでしょうか、cache hit と表示されています。手元にキャッシュがないのにも関わらず、リモートのキャッシュサーバにキャッシュがあるため、cache hit となります!

キャッシュオブジェクト

キャッシュのオブジェクトは、ハッシュ値名で、アウトプット(file やログ)のバイナリになります。 Docker コンテナ内で見ると、次のようなファイルが置かれています。

$ ls -hl storage/team_myteam/
total 5392
-rw-r--r--  1 silverbirder  staff   1.3M Sep 11 16:20 082bae5de9b1745f
-rw-r--r--  1 silverbirder  staff   1.3M Sep 11 16:20 5a55c6367c8caf01

--team オプションで指定した名前で、フォルダが作成されています。 そのため、team 毎にキャッシュが作成されます。

キャッシュとは

turborepo のキャッシュについては、公式 を読むと良いでしょう。

ざっくりいうと、次の流れで cache miss,cache hit になります。

  1. turbo build を実行
  2. turbo.jsonbuildタスクの inputs(ソースコードなど)や環境変数をハッシュ化
  3. キャッシュが既にローカルまたはリモートに存在していなければ、cache miss
  4. turbo.jsonbuildタスクの outputs(dist フォルダ、標準出力など)をバイナリ化し、ハッシュ名で保存

3 の手順で、キャッシュが存在していれば、cache hit となり、outputs が復元します。

クラウドで、やってみた

キャッシュサーバは、AWSGCP などのクラウドベンダーにあるコンピューティングリソースへデプロイしましょう。 Docker イメージがあるので、AppRunner や CloudRun が楽にできそうです。

キャッシュストレージは、いまのところ AWS S3 のみ対応とのことです。 AWS S3 のクライアントは、S3Client を使っているため、GCS にも対応可能です。まあ README に従うなら、S3 に配置するのがベターでしょう。コンピューティングリソースを動かす IAM は、ストレージリソースへの READ/WRITE 権限を足しましょう。

おわりに

セルフホストして、リモートキャッシュが使えるようになりました。 まだ運用したことがないので、課題を実感していません。引き続き、利用してみようと思います。

Stable Diffusion API 開発

Stable Diffusion は、文章を渡すと画像を生成してくれる AI で OSS です。 これを自分の PC で動かそうとすると、GPU が必要になります。 (CPU で動かせるstable_diffusion.openvino というのもあります)

できれば、どの PC でも使えるように、かつ、Slack などサービスと連携できるよう API がほしいなと思いました。 そこで、Stable Diffusion の API を開発しました。

結論

beta.dreamstudio.aiSDKstability-sdkを使いました。

成果物は、次のリポジトリに置いています。

ローカル環境でも、Docker コンテナでも、動きます。

動かすには、beta.dreamstudio.aiAPI Key が必要になります。 Docker で動くので、Docker をデプロイできるサービスなら、どこでも動きます。(GPU は不要です)

私は、GCP が好きなので、CloudRun というサービスにデプロイしました。 API は、とりあえず、<url>/?prompt=<text> というパラメータを受け取り、画像を返却します。

Slack で使ってみると、こんな感じになりました。

stable-diffusion-api-on-slack

ひとまず、API で Stable Diffusion を動かせました。

GPU と設計

stability-sdkを使う前までは、自前で Stable Diffusion を動かす環境を用意しようと設計を考えました。設計の調査メモは、次のリンクにメモを残しています。

具体的に、次のようなパターンを考えました。

  1. Google Colaboratory の GPU を使って Stable Diffusion を動かし、簡易な API で公開する
  2. サーバー(GCE や CloudRun など) で GPU を使って Stable Diffusion を動かし、簡易な API で公開する
  3. バッチ(Cloud Batch)で GPU を使って Stable Diffusion を動かし、必要なときに動かす。(API からバッチ処理をキックする)

1 番目は、Google Colaboratory の利用は 12 時間制限というのがあり、そこを回避する何かが必要なります。ただし、本来の用途と外れていると思うので、却下しました。

2 番目は、金銭的に数万~数十万円以上のランニングコストが発生するので却下です。

3 番目は、一番最初の構想したものです。2 番目のような GPU のサーバを常時起動しているとめちゃくちゃもったいないので、 バッチ処理として 3 番目の案を考えていました。3 番目で実際に構築してみると、(何が原因か深く調べていないですが) 起動に 30 分以上かかってしまい、使い物にならなさそうでした。

で、悩んだ結果、stability-sdk がメンテナンスやランニングコストも不要で、シュッとできそうだったことに気づきました。

もちろん、デメリットはあります。

  • SDK に依存するので、自身がコントロールできない(img2img できない)
  • 課金制

しかし、個人レベルで利用するという前提でしたので、デメリットよりもメリットの方が大きいと判断しました。

stability-sdk

beta.dreamstudio.ai は、Stable Diffusion を使っています。 API として、stability-sdk を公開しています。 使うには、Python で書く必要があります。 ソースコードを読むと、gRPC を使っているため、別言語で SDK を書くのは比較的簡単だと思います。 私は、Python でシュッと書けるので、flask と stability-sdk を使いました。

ひとまず、Prompt だけを受け付ける超絶シンプルな API を書きました。 stability-sdkは、様々パラメータがあるので、それも受け付けられるようにしようかなと思ったり、Midjourney の discord のボットのようなモノを書いても面白そうだなと思いました。

終わりに

マークダウンで、画像を読み込むときに、今回開発した API を指定すると、マークダウンを開いたタイミングで画像が毎回変わります。 prompt と seed を指定すれば固定できるんですけど、こういうのも面白いなと思っています。

ERNIE-ViLG を Google Colaboratory で動かしてみた

ERNIE-ViLG というのが、"二次元キャラ" に強いという記事を目にしました。

実際に使ってみようと、次のページで試したんですが、レスポンスがイマイチでした。

そこで、公式ページを参考にして、ERNIE-ViLG を Google Colaboratory を書こうと思いました。

Google Colaboratory で動かす

実際に作ったものは、次のモノです。

中身については、正直良くわかっていないですが(公式ページ 通りに試しただけ)、簡単に紹介しようと思います。

準備

次のコマンドを叩いて、ERNIE-ViLG の準備をします。(GPU 環境でないと動作しません)

$ pip install paddlepaddle-gpu -U
$ pip install paddlehub==2.1.0
import paddlehub
paddlehub.server_check()
$ hub install ernie_vilg

ERNIE-ViLG を使う

使うのは、2 つのパターンがあります。

  1. CLI で実行する(hub コマンド)
  2. Python で実行する(hub ライブラリ)

CLI の場合は、次のとおりです。

$ hub run ernie_vilg --text_prompts "宁静的小镇" --style "油画" --output_dir ernie_vilg_out

Python の場合は、次のとおりです。

import paddlehub as hub

module = hub.Module(name="ernie_vilg")
text_prompts = ["宁静的小镇"]
images = module.generate_image(text_prompts=text_prompts, style='油画', output_dir='./ernie_vilg_out/')

オプションは、次の説明の通りです。

  • text_prompts
    • 生成したい画像の内容を記述した入力文
  • style
    • スタイルで画像を生成することが可能
      • 油画 (油絵)
      • 水彩 (水彩画)
      • 粉笔画 (パステル)
      • 卡通 (漫画, カートゥーン)
      • 儿童画 (子供向け)
      • 蜡笔画 (クレヨン)
      • 探索无限 (無限大を探る)
  • topk
    • 生成する画像数(最大 6 枚)
  • output_dir

ERNIE-ViLG#API

text_prompts や style は、中国語で書く必要があります。

Google Colaboratory で 画像を簡単に見たい

ERNIE-ViLG を動かすと、出力ファイルが Google Colaboratory のフォルダに入ります。 画像を見るためには、画像をダウンロードして、開くという手間があります。

download_image_on_browser

そこで、フォルダを Google Drive と同期するという機能があります。 これを使えば、保存先を Google Drive にしておけば、Google Drive の UI 上から画像を見ることができます。

mount_google_drive

めちゃくちゃ便利なので、ぜひ使ってみてください。

Midjourney, StableDiffusion で役立つPrompt フレーズ集

Midjourney や StableDiffusion を使っていると、どういうフレーズを使えばよいかわからなくなります。 そこで、フレーズ集を作って、Prompt で役立てたいなと思っています。

練習場

どこで Prompt の練習したら良いか悩むので、まとめておきました。 お勧めは、DreamStudio.ai です。

Prompt で入力する文章構成

Prompt で入力する文章は、AI に理解しやすい構成である方が良いです。その方が欲しい画像を手に入れやすくなります。 文章の構成は、次のフォーマットです。

<全体のフォーマット>
<主題>
<主題の補足>
<作者>
<全体の補足>
<フレーバー>

例:
<全体フォーマット>Detailing oil painting of
<主題>The great white castle on deep forest landscape
<英霊>by CASPAR DAVID FRIEDRICH and CLAUDE LORRAIN,
<全体の補足> perfect lighting, golden hour,
<フレーバー> taken with Canon 5D Mk4

魔術として理解するお絵描き AI 講座 より引用

各項目についてのフレーズを、まとめておきました。

全体のフォーマット

  • Ancient of
  • Beautiful concept art of
  • Cartoon of
  • Concept art of
  • Detailed illustration of
  • Detailed water painting of
  • Detailing oil painting of
  • Futuristic of
  • Illustration of
  • Image of nightmare of
  • Logo about
  • Pencil sketch of
  • Photo of
  • Pop art of
  • Portrait of
  • Scene of graphic novel that
  • Scene of the movie that
  • Screenshot of UE5 of
  • Side profile of
  • Sketch of

作者

  • by <作者>
  • in <作者> style

全体の補足

  • 8k
  • art
  • beautiful shadow
  • collection sheet
  • comic
  • golden hour
  • grid
  • kawaii
  • manga
  • perfect lighting
  • pixiv
  • realistic photo
  • unreal engine

フレーバー

  • 11mm
  • by Canon EOS 5D Mark4 and SIGMA Art Lens 35mm F1.4 DG HSM, F1.4, ISO 200 Shutter Speed 2000
  • EF11-24mm F4L USM
  • no background
  • taken with Canon 5D Mk4
  • white background

終わりに

フレーズをまとめておくと、Prompt で文章入力するときに、参考にできて便利でした。 別件で、そもそも私には英語力がないため、Deepl を欠かせないことに気づきました...。 では、よい お絵かきライフを!