スクレイピングライブラリ crawlee の紹介

JS, Python で使えるスクレイピングライブラリ crawlee の紹介です．ちょっとしたスクレイプや本格的なアプリ開発まで使えるライブラリになっています．

[公式ドキュメント](https://crawlee.dev/)


# 対象読者

-   スクレイピングをして，データ分析などをしてみたい方
-   普段 puppeteer や playwright でスクレイピングを行うアプリ開発をしている方


# crawlee とはなんぞ？

> Crawlee is a web scraping library for JavaScript and Python. It handles blocking, crawling, proxies, and browsers for you.

公式サイトの文言にもあるとおり，ブロッキング（ボット判定を回避する仕組み）やページ内のリンクをどんどん辿っていく巡回型クロールを簡単に実現できるライブラリです．
また，データセットという仕組みにより取ってきたデータを json 形式にして永続化することも得意です．
普段 puppeteer や playwright を使われている方向けのイメージとしては，これらのライブラリのラッパーと捉えてもらえればよいと思います．
ブロッキング技術については結構色々あるので，記事の最後の方に載せておきます．


# インストール方法


## JS

いつもの（今回は puppeteer を使っています）

```sh
npm install crawlee puppeteer
```

crawlee が提供しているテンプレートもあります．

```sh
npx crawlee create my-crawler
```


## Python

いつもの．uv, poetry を使っている方は適宜．

```sh
python -m pip install 'crawlee[all]'
```


# とりあえずクロールを開始する

簡単な使い方としては，

1.  `PuppeteerCrawler` , `PlaywrightCrawler` などのクローラを作成する
2.  `requestHandler` に処理内容を書いていく

という流れです．とりあえず **requestHandler** に処理を書いていくということだけ理解していれば，問題ないと思います．
以下は公式サイトのサンプルコードです．requestHandler の中身は普通の puppeteer のコードになっています．

```js
import { PuppeteerCrawler } from 'crawlee';

const crawler = new PuppeteerCrawler({
    async requestHandler({ request, page, enqueueLinks, pushData, log }) {
        const title = await page.title();
        log.info(`Title of ${request.loadedUrl} is '${title}'`);

        await pushData({ title, url: request.loadedUrl });
        await enqueueLinks();
    },
});

await crawler.run(['https://crawlee.dev']);
```

ここで，↑のコードに出てきている `pushData` と `enqueueLinks` について補足します．`pushData` はスクレイプした情報を永続化するための処理で，引数として渡したデータが json 形式でファイルとして保存されます．

`enqueueLinks` はページ内のリンクを探し出して，再帰的にクロールをかけていくための処理です．
この関数を呼び出すだけでページ内のリンクを探し出し，自動的にフェッチをしに行ってくれます．いちいちリンク抽出のコードを書く必要がないのがありがたポイント．


# 簡単なデータ分析プロジェクト

個人レベルならここまでの知識で簡単なプロジェクトを開始できるのでやってみます．
自分はプロ野球の順位の推移について知りたかったので，順位一覧が載っているサイトをスクレイプしてグラフ化することを目指してみます．

今回スクレイプしたサイトは[こちら](https://nipponbaseball.web.fc2.com/standings_pa.html)です．
chatGPT の助けを借りて，早速スクレイピングのコードを書いてみました．`requestHandler` の処理はサイトごとに書かないといけないので，AI ツールにやってもらうのがラク．

```js:main.ts
import { PuppeteerCrawler } from '@crawlee/puppeteer';
import { Dataset } from '@crawlee/core';

const startUrls = ['https://nipponbaseball.web.fc2.com/standings_pa.html'];
const teamNames = ["ソフトバンク", "日本ハム", "ロッテ", "楽天", "オリックス", "西武"];

(async () => {
    const crawler = new PuppeteerCrawler({
    requestHandler: async ({
        page,
    }) => {
        await page.waitForSelector("#table1");
        const title = await page.title();
        console.log(title);

        // テーブルデータを取得
        const years = await page.$$eval('#table1 tbody tr', rows => {
                return rows.map(row => {
                    const cells = row.querySelectorAll('td');
                    const year = cells[0].textContent?.trim() ?? "";
                    const standings = Array.from(cells)
                        .slice(1, 7) // 1位から6位のデータを取得
                        .map(cell => cell.querySelector('.team')?.textContent?.trim() ?? "値なし");
                    return standings;
            });
        });
        console.log(years);

        // 取得したデータから各チームの順位一覧を作成
        const teamRankings: { [team: string]: number[] } = {};
        years.forEach(standings => {
            let rank = 1;
            for (const teamName of standings) {
                if (!teamRankings[teamName]) { teamRankings[teamName] = []; }
                teamRankings[teamName].push(rank);
                rank += 1;
            }
        });
        await Dataset.pushData(teamRankings);
    }
    });
    await crawler.run(startUrls);
})();
```

このコードを実行すると storage というディレクトリが作成され， `storage/datasets/default/000000001.json` というファイルが作成されます．これが `Dataset.pushData(teamRankings);` のところでやっている永続化されたファイルです．中身を見てみましょう．

```json-ts:000000001.json
{
    "値なし": [
        1,
        2,
        3,
...
    ],
    "ソフトバンク": [
        1,
        3,
        2,
...
    ],
    "日本ハム": [
        2,
        6,
        6,
...
```

いい感じに各チームの順位が格納されています．（値なしが入っちゃっていますが，，，）

json としてデータが手に入ったので，あとは notebook などで可視化しちゃいましょう．ここは本題ではないので gist (https://gist.github.com/Hirata-Kodai/34a0f2fd1c2bc25be2ddb9532811e27b )に雑ですがコードをあげておきました．結果だけ載せるとこんな感じです．
（前年 A クラスのチームが翌年もそのまま A クラス入りするケースって 2015 -> 2016 で結構珍しいという発見👀）


![スクリーンショット 2025-04-21 17.21.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542476/d35adbc2-fc61-40e0-9d36-202ed09754c9.png)


# まとめ

こんな感じで crawlee を使うとスクレイプからデータセット作成まで簡単にできるので，興味を持った方はぜひ使ってみてください．ω・)つ  <https://crawlee.dev/js/docs/quick-start>


## （補足）ブロッキング回避技術

大きく分けて２つあります．

1.  ブロッキングされにくい IP アドレスを使う
2.  人間らしい fingerprint を設定する

それぞれ見ていきます．


### ブロッキングされにくい IP アドレスを使う

サイト運営者側から見て，クローラーボットを弾く際にまず実施しやすいのは特定の IP アドレスからのアクセスをブロックすることです．このためボット対策が強固なサイトをスクレイプする場合，ブロックされにくい IP アドレスを使うことが必要になることがあります．

crawlee ではプロキシサーバの管理も行うことができ，フェッチが成功するプロキシを優先的に使う，プロキシ間で優先度付けをするといったことができます．ボット対策が強固なサイトを本格的にクロールしたい場合，Apify などが提供している有料のプロキシサーバを導入することも必要かもしれません．（<https://apify.com/proxy>）

-   crawlee のプロキシに関するドキュメント：<https://crawlee.dev/js/docs/guides/proxy-management>


### 人間らしい fingerprint を設定する

ブラウザが送るユーザエージェントや使っているブラウザの種類など，サイトにアクセスするときにサーバに送られるあらゆる情報をまとめて fingerprint と呼びます．（ブラウザにおける指紋みたいな意味合い？）
自分の fingerprint がユニークかどうかを調べられるサイトがあるので，見てみるとイメージが付きやすいかもしれません．（<https://amiunique.org/>）

例えば素直にユーザエージェントに `HeadlessChrome` という文字列を入れているとそれだけでブロックされるようなサイトもあります．そのため crawlee ではデフォルトでより人間らしい fingerprint を設定するような仕様になっています．簡単に言ってしまえば偽造できるところは偽造してしまえということです．

このように crawlee では

-   プロキシサーバ
-   ブラウザ
-   fingerprint
-   セッション

などを管理することで，素で puppeteer などを使うよりもブロッキングされにくいクロールが簡単にできるようになっています．