astral を使用して deno からブラウザを操作する

deno は使っていますか?使っていますよね? そして、 Puppeteer のようにスクリプトからブラウザ操作をしたいということが良くあります。ありませんか?

本エントリでは、 deno 版の Puppeteer である astral を使用したブラウザの操作について紹介します。

Note

  • このエントリでは 2025年5月時点での最新版である astral@v0.5.2 で解説しています。
  • deno 2.2.8 / v8 13.5.212.10-rusty / typescript 5.7.3 を使用しています。
  • このエントリは生成AIは使用していません。

Warning

サイトに負荷をかけると最悪犯罪になるためお気をつけください。

astral とは

説明に Astral is a high-level puppeteer/playwright-like library that allows for control over a web browser とあるように Puppeteer やPlaywright ライクなブラウザを操作するパッケージです。

基本的な操作はほぼ Puppeteer などと似ており直感的な操作が可能です。 JSR のパッケージとして配布されているので deno から利用する場合は、

deno add jsr:@astral/astral

とするだけで利用可能です。JSR のインストール方法などは Using JSR with Deno - Docs - JSR を参照してください。

そして操作するブラウザは基本的に明示的な設定をしない限り、astral 用の Chrome が自動でインストールされるようです。

一応、Firefoxを使用することも可能っぽい記述はあるのですがうまく動かなかったのでここでは解説はしません。

Tip

astral ではデバッグモードで起動することができます。 DEBUG=true deno run --allow-env your_script.ts のように実行することで起動するブラウザのコマンドライン引数やAPIのやり取りなどのログを確認できます。

スクリーンショットを撮る

Note

この記事を含めた具体的なスクリプト例を GitHub - cateiru/astral-playground にて紹介しています。合わせて参考にしてみてください。

さて、では実際に astral を実際に動かして例を見ていきましょう。最初はブラウザ操作も醍醐味と勝手に思っているスクリーンショットをしていきます。

まずは、 astral を import しましょう。

import { launch } from "jsr:@astral/astral";

launch() を呼び出すことでブラウザ操作の準備ができます。戻り値は Promise<Browser> 型が返ります。そのため、初めに以下のようにします。

const browser = await lunch();

Puppeteer も const browser = await puppeteer.launch(); なので同じような機能です。 さらに引数として渡せる LunchOptionsastral/src/browser.ts at main · lino-levan/astral · GitHub によると以下のような項目が設定可能のようです。

  • path?: string
    • ブラウザのパスのようです。未指定の場合は自動で見つけてくれるので基本的には未指定で良さそうです。
  • args?: string[]
  • cache?: string
    • ブラウザが無い場合、自動でブラウザがインストールされますが、そのキャッシュディレクトリのパスを指定します。
    • デフォルトは mac などでは /Library/Caches/astral などが使われるようです。詳しくは astral/src/cache.ts at main · lino-levan/astral · GitHub を参照してください。
  • headless?: boolean
    • ヘッドレスモードで起動するかのフラグです。 未指定の場合は true で、 false にすると実際にブラウザを起動した状態で実行がきでます。
  • product?: "chrome" | "firefox"
    • 使用するブラウザを選べるようです。デフォルトは chrome です。
  • userAgent?: string
    • 使用できる UserAgent を設定します。後述する newPage でも設定できますが、その場合は上書きされるようで、こちらはデフォルトの値になるようです。

lunch ができ、 Browser のインスタンス変数が作成できたら次は URL を指定して Page を作ります。これによりブラウザがページにアクセスできます。

const page = await browser.newPage("https://example.test");

newPage にも第二引数でオプションを指定でき、以下のような項目が設定可能です。

  • waitUntil?: "none" | "load" | "networkidle0" | "networkidle2"
  • sandbox?: boolean
  • userAgent?: string
    • アクセスする UserAgent を指定できます。 lunch で設定していても上書きすることができます。

もし、 Basic認証やビューポートサイズを変えてアクセスしたい場合は以下のように goto メソッドを呼び出します。

// ここでは引数をなにも設定しない
const page = await browser.newPage();

await page.authenticate({ username: "postman", password: "password" });

// ここで URL や Options を設定する
await page.goto("https://example.test");

goto の第二引数で指定できるオプションも newPage で指定できるオプションと同じです。

次にビューポートサイズの設定を行います。スクリーンショットを取得する際には、このビューポートサイズが画像のサイズとなるため必ず設定しておきます。 page のインスタンスに setViewportSize メソッドがあるのでこちらを使用します。

await page.setViewportSize({
  width: 1280,
  height: 800,
});

最後にスクリーンショットを取ります。スクリーンショットは screenshot メソッドを使用します。

const image = await page.screenshot();
Deno.writeFileSync(`${import.meta.dirname}/image.png`, image);

戻り値は Uint8Array<ArrayBufferLike> が返るので Deno.writeFileSync などでファイルに書き出しをすると良いです。

Tip

import.meta.dirname を使用することで実行する ts ファイル直下に画像を書き出しすることが可能です。

screenshot メソッドには以下のようなオプションが指定できます。

  • format?: "jpeg" | "png" | "webp"
    • 書き出す画像の形式を指定します。デフォルトは png です。
  • quality?: number
    • jpeg のクオリティを指定します。0~100 の値で設定します。
  • clip?: Page_Viewport
    • スクリーンショットをする位置を座標で設定します。設定する座標は x, y, width, height, scale です。
    • このオプションは ScreenshotClip interface | Puppeteer と同じようです。
  • fromSurface?: boolean*1
    • 値を true にすると View ではなく Surface からスクリーンショットを取得するオプションです。デフォルトは true です。
    • Surface というのは Surfaces のことのようです。
  • captureBeyondViewport?: boolean
    • true にすることでビューポート外の要素もキャプチャできるようにします。デフォルトは false です。
  • optimizeForSpeed?: boolean
    • true にするとキャプチャスピードの速度を優先してエンコードするフラグのようです。デフォルトは false です。

指定した要素のスクリーンショットを撮る

さて、先程までは画面全体(ビューポート全体)のスクリーンショットについてを解説しましたが、次に指定した要素のスクリーンショットを撮る例について解説します。

astral では以下のようにして要素を指定して、スクリーンショットメソッドでスクリーンショットを撮る機能が一応実装されています。

// `.target-select` にスクリーンショットを撮りたい要素の CSSセレクタ を指定
const targetElement = page.$(".target-selector");
if( targetElement == null) {
  throw new Error("Element not found");
}
const image = await targetElement.screenshot();
Deno.writeFileSync(`${import.meta.dirname}/image.png`, image);

しかし、これではスクリーンショットが正しく撮れないようです。要素のインスタンス変数 ElementHandle で定義されている screenshot メソッドは以下のように実装されています。

  /**
   * This method scrolls element into view if needed, and then uses `Page.screenshot()` to take a screenshot of the element.
   */
  async screenshot(
    opts?: Omit<ScreenshotOptions, "clip">,
  ): Promise<Uint8Array> {
    await this.scrollIntoView();
    const box = await this.boundingBox();

    if (!box) {
      throw new Error(
        "No bounding box found when trying to screenshot element",
      );
    }

    return await this.#page.screenshot({
      ...opts,
      clip: {
        ...box,
        scale: 1,
      },
    });
  }

引用元: astral/src/element_handle.ts at e647b13da6b4511aea1ee5d73e29c03cbc4a4358 · lino-levan/astral · GitHub

最初に await this.scrollIntoView(); をしているのがポイントです。これにより、指定した位置までページをスクロールします。その後に boundingBox メソッドで位置の絶対位置を取得します。しかし、 boundingBox はスクロールを含まない画面からの絶対位置です。そうすると、スクロール分 y 座標がズレてしまうことで正しく撮ることができません。

Tip

JavaScript で要素の絶対位置を取得する場合、 window.pageYOffset + element.getBoundingClientRect().top とします。

そのため、 1. スクロールする前の初期位置の状態で絶対座標を取得する方法 か、 2. スクロール量を取得する方法 の2種類の案が考えられます。

1. スクロールする前の初期位置の状態で絶対座標を取得する方法

この方法では、 scrollIntoView を実行する前に boundingBox を実行します。というか、 scrollIntoView は必要ありません。代わりに captureBeyondViewport: true としてViewPort 外でもスクリーンショットを撮れるようにします。

  const boundingBox = await targetElement.boundingBox();
  if (boundingBox == null) {
    throw new Error("Element bounding box not found");
  }
  const image = await page.screenshot({
    captureBeyondViewport: true,
    clip: {
      x: boundingBox.x,
      y: boundingBox.y,
      width: boundingBox.width,
      height: boundingBox.height,
      scale: 1,
    },
  });
  Deno.writeFileSync(`${import.meta.dirname}/image.png`, image);

2. スクロール量を取得する方法

1の方法はシンプルです。しかし、事前に scrollIntoView をしていると壊れてしまう可能性があります。これらを解決するため window.pageYOffset を JavaScript 経由で取得して y 座標に足すことで正しい座標を指定します。

  const scrollY = await targetElement.evaluate(() => {
    return (window as any).pageYOffset;
  });
  const boundingBox = await targetElement.boundingBox();
  if (boundingBox == null) {
    throw new Error("Element bounding box not found");
  }
  const image = await page.screenshot({
    captureBeyondViewport: true,
    clip: {
      x: boundingBox.x,
      y: boundingBox.y + scrollY,
      width: boundingBox.width,
      height: boundingBox.height,
      scale: 1,
    },
  });
  Deno.writeFileSync(`${import.meta.dirname}/image.png`, image);

JavaScript の API を呼び出すには evaluate を使用します。引数に関数を指定することでその関数を実行できます。注意点として、渡す関数は内部で toString() *2 の後ブラウザのAPIに渡されます。そのため関数内部で変数を外から渡して使用したい場合は evaluate の引数で渡します。

const returnValue = await targetElement.evalute((element, value1, value2, value3) => {
  ...
  return value;
}, {args: [value1, value2, value3]});

フォームを自動で入力する

次にフォームの自動入力について解説していきましょう。ブラウザ操作では、動作確認などで自動でログインをしたいケースなどがあるかもしれません。そういった場合に利用可能です。

1. 文字入力型

type="input"type="password"textarea などの文字を入力するタイプのフォーム要素では type メソッドが astral に用意されていてこちらが利用できます。

  const textElement = await page.$("input[type='text']");
  if (textElement == null) {
    throw new Error("textElement is null");
  }
  await textElement.type("こんにちは", { delay: 100 });

type では第二引数に delay を含めることができ、これに値を設定すると指定したミリ秒分タイプ感覚を遅延させます。上記の場合は こ [100ms] ん [100ms] に [100ms] ち [100ms] は となります。change イベントなどで入力中にバリデートなどをしている場合はこの遅延を入れると便利です。

2. クリック型

type="button"type="checkbox"type="radio" などは click メソッドを使用して擬似的にクリックすることが可能です。

  const checkboxElement = await page.$("input[type='checkbox']");
  if (checkboxElement == null) {
    throw new Error("checkboxElement is null");
  }
  await checkboxElement.click();

3. value に値が入るケース

type="date"type="weektype="color" などの value 属性に値が入るケースでは専用のメソッドが用意されていないため evaluate を使用して JavaScript 経由で value に値を入れてあげる必要があります。

  const datetimeElement = await page.$(
    "input[type='datetime-local']"
  );
  if (datetimeElement == null) {
    throw new Error("datetimeElement is null");
  }
  await datetimeElement.evaluate(
    (element: any, date) => {
      element.value = date;
    },
    { args: ["2023-10-01T12:00"] }
  );

4. select

select も .select() といったメソッドが astral にはない*3ので evaluate を使用する必要があります。

  const selectElement = await page.$("select");
  if (selectElement == null) {
    throw new Error("selectElement is null");
  }
  const selectedValue = "option2";
  await selectElement.evaluate(
    (element: any, v) => {
      const optionElement = element.querySelector(`option[value='${v}']`);
      if (optionElement != null) {
        optionElement.selected = true;
      }
    },
    { args: [selectedValue] }
  );

おわりに

いかがだったでしょうか? deno は手軽にスクリプトを書けるのが魅力で astral も deno に最適化されているためすぐ使い始めることができ非常に便利なライブラリです。ぜひこの機会に試してみてはいかがでしょうか?