3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Puppeteerにはまだまだ負けられないcheerio-httpcliの底力を見せる時が来た

Posted at

昨今のWEBスクレイピングといえばSPAなどの動的更新WEBサイトの増加もあってPuppeteerが主流になっていますが、かつて一世を風靡した(言いすぎ)cheerio-httpcliというスクレイピングライブラリもあるのをご存知でしょうか。

静的HTMLを解析してjQueryのように要素を検索・操作するいわゆる古いタイプのライブラリではありますが、まだまだ現役でやれるところを見せるべく、目玉機能をいくつか引っさげてバージョン0.8.0へと進化したのでこの場を借りて紹介させていただきます。

Puppeteer使いの方にも役立つ情報もあるのでぜひぜひ最後までご覧ください。

ちなみに、cheerio-httpcliについての紹介記事は過去に何度かQiitaで紹介しているので「cheerio-httpcli?なにそれおいしいの?🤔」的な人はそちらもご覧になるとより分かりやすいと思います。

過去の紹介記事

それでは行ってみましょう。今回のバージョンアップの目玉機能のラインナップはこちら😆。

ワクワクするような機能が目白押しですね。では、ひとつずつじっくりしっぽり紹介していきます。

マルチインスタンス対応

今までのcheerio-httpcliはシングルインスタンスで動作するライブラリのため、クッキー記憶領域が1つしかありませんでした。
そのため、ログインが必要な同一サイトに複数アカウントで同時にログインした場合、クッキーに残るのは後からログインしたアカウントのものとなってしまっていたわけです。

同じサイトに同時に別々のアカウントでログインしようとして失敗する例

const client = require('cheerio-httpcli');

// ユーザーfooでログイン
client.fetch('https://example.com/login', (err, $, res, body) => {
  // ログインフォーム送信
  $('form').submit({
    username: 'foo',
    password: 'password_for_foo'
  }, (err, $, res, body) => {
    // ログイン後のユーザーページでユーザー名を表示
    console.log($('.username').text());  // => barと表示されてしまう😟
  });
});

// ユーザーbarでログイン
client.fetch('https://example.com/login', (err, $, res, body) => {
  // ログインフォーム送信
  $('form').submit({
    username: 'bar',
    password: 'password_for_bar'
  }, (err, $, res, body) => {
    // ログイン後のユーザーページでユーザー名を表示
    console.log($('.username').text());  // => barと表示される😉
  });
});

サーバーからのレスポンスのタイミングによってはユーザーbarではなくfooの方の情報が有効になる可能性もありますが、いずれにしても今まではこういった用途では使い物にならない状態でした。

しかし、今回のバージョンアップで追加されたfork()メソッドを使うことによって、cheerio-httpcliをマルチインスタンスで動作させることが可能になりました。

fork()メソッドで複数アカウントでの同時ログインが可能になった例

const client = require('cheerio-httpcli');

// 🚩fork()で子インスタンスを作成
const child = client.fork();

// メインインスタンスを使用してユーザーfooでログイン
client.fetch('https://example.com/login', (err, $, res, body) => {
  // ログインフォーム送信
  $('form').submit({
    username: 'foo',
    password: 'password_for_foo'
  }, (err, $, res, body) => {
    // ログイン後のユーザーページでユーザー名を表示
    console.log($('.username').text());  // => fooと表示される😉
  });
});

// 子インスタンスを使用してユーザーbarでログイン
child.fetch('https://example.com/login', (err, $, res, body) => {
  // ログインフォーム送信
  $('form').submit({
    username: 'bar',
    password: 'password_for_bar'
  }, (err, $, res, body) => {
    // ログイン後のユーザーページでユーザー名を表示
    console.log($('.username').text());  // => barと表示される😉
  });
});

最初にrequire('cheerio-httpcli')でロードしたclientがメインインスタンスで、そのメインインスタンスのfork()メソッドで生み出したchildが子インスタンスになります。

各インスタンスはそれぞれ個別にクッキー領域や設定を保持するので、同一サイトで複数アカウントによる同時ログインや、特定のインスタンスだけリクエストヘッダを加工して送信するといったフリーダムなことも可能になったわけです。ちなみに子インスタンスは何個でも生み出せます。すごい😲。

なお、fork()を実行できるのはメインインスタンスのみで、子インスタンスからさらに孫インスタンスをfork()することはできません。

また、ダウンロードマネージャーを持っているのはメインインスタンスのみなので、子インスタンスのスクレイピング中で$(...).download()メソッドが実行されるとメインインスタンスのダウンロードマネージャーに送られます。

ざっくりと表現すると、メインインスタンスは女王アリで子インスタンスは働きアリのようなもので、図にするとこんな感じです。

今までのcheerio-httpcli

これからのcheerio-httpcli

※上の図では女王アリはふんぞり返ってますが、女王アリ自身をスクレイピングに駆り出すことも可能です。

今回のマルチインスタンス対応によってサーバーサイドでcheerio-httpcliを稼働させたり、タイミングを気にせずクッキーに影響されるサイトを並列スクレイピングすることができるようになったかと思います。

※サイト側に過度な負担がかからないよう、お気をつけください。

クッキーの読み書き対応

あるサイトにログインした状態のクッキーをエクスポートしておいて、次にスクレイピングを行う際にそのクッキーをインポートすることにより、いきなりログイン後のページにアクセスできるようになりました(セッションIDやトークンなどのログイン情報をクッキーに保存していないサイトでは無理ですが)。

エクスポート例

const fs = require('fs');
const client = require('cheerio-httpcli');

// ユーザーfooでログイン
client.fetch('https://example.com/login', (err, $, res, body) => {
  // ログインフォーム送信
  $('form').submit({
    username: 'foo',
    password: 'password_for_foo'
  }, (err, $, res, body) => {
    console.log($('.username').text());  // => fooと表示される

    // 🚩クッキー情報をファイルに書き出し
    const cookies = JSON.stringify(client.exportCookies());
    fs.writeFileSync('example.com.cookies.json', cookies, 'utf-8');
  });
});

インポート例

const fs = require('fs');
const client = require('cheerio-httpcli');

// 🚩クッキー情報をファイルから読み込んでログイン状態を復元
const cookies = JSON.parse(fs.readFileSync('example.com.cookies.json', 'utf-8'));
client.importCookies(cookies);

// ログイン後にしかアクセスできないページにいきなりアクセス
client.fetch('https://example.com/mypage', (err, $, res, body) => {
  console.log($('.username').text());  // => fooと表示される
});

ちなみに、このクッキー情報はPuppeteerのpage.cookies()page.setCookie()で読み書きされるものと互換性があるので、Puppeteerから書き出したクッキーをcheerio-httpcliから読み込んだり、その逆も可能だったりします😎。

途中までcheerio-httpcliで潜って、その先からPuppeteerにバトンタッチするといった胸熱で実用性皆無なコラボレーションもできちゃったりします。

※クッキーにはWEBサイトのセッションIDなど、セキュリティ上非常に重要な情報が書き込まれている可能性があるので、ファイルやDBなど外部に保存する際には十分注意してください(セッションIDが漏洩するとアカウントの乗っ取りなどされる恐れがあります)。

ファイルアップロード対応

$('input[type=file]')の要素にローカルのファイルのパスを指定することにより、フォーム送信時にファイルアップロードができるようになりました。

<form action="/upload.php" enctype="multipart/form-data" method="post">
  <input type="text" name="title">
  <input type="file" name="upload_file">
  <input type="submit">
</form>

上記のようなフォームでファイルをアップロードするには以下のように指定します。

$('form').submit({
  title: 'お宝画像',
  upload_file: '/path/to/secret/yabai.jpg'
});

// もしくは
$('form').find('input[name=title]').val('お宝画像');
$('form').find('input[name=upload_file]').val('/path/to/secret/yabai.jpg');
$('form').submit();

対象のinput[type=file]要素が複数ファイル(multiple)に対応している場合は配列で指定可能です。

$('form').submit({
  title: 'お宝画像',
  upload_file: [
    '/path/to/secret/yabai.jpg',
    '/path/to/secret/sugee.jpg'
  ]
});

// もしくは
$('form').find('input[name=title]').val('お宝画像');
$('form').find('input[name=upload_file]').val([
  '/path/to/secret/yabai.jpg',
  '/path/to/secret/sugee.jpg'
]);
$('form').submit();

ダウンロード機能がより便利に

cheerio-httpcliには画像ファイルのダウンロード機能というものがあり、$('img').download()とすることで、画像ファイルをまとめてダウンロードすることができます。

// ダウンロードマネージャー設定
client.download
  .on('ready', (stream) => {
    // 画像のストリームが送られてくるのでファイルに保存するなりなんなり
    console.log('download start', stream.url.href);
  });

client.fetch('https://example.com/', (err, $, res, body) => {
  // サムネイル画像をまとめてダウンロードマネージャーに登録
  $('img.thumbnail').download();
});

※ダウンロード機能についての詳細は以前書いた記事で解説していますので、よろしければそちらも是非。

便利機能① ファイルへの保存処理を簡単に

今まではstream.on('data')とかstream.pipe()などでストリームをファイルに保存する処理を自前で書く必要がありましたが、stream.saveAs()というメソッドで一発でファイルに保存できるようになりました。

client.download
  .on('ready', (stream) => {
    stream.saveAs(<保存先ファイルパス>, (err) => {
      console.log(stream.url.href, 'saved');
    });
  });

ちなみに、上記の例ではコールバックで結果を受け取っていますが、コールバックを省略するとプロミス形式で結果を受け取れます。

client.download
  .on('ready', (stream) => {
    stream.saveAs(<保存先ファイルパス>)
      .then(() => console.log(stream.url.href, 'saved'))
      .catch(console.error);
  });

便利機能② <a href="~">のリンク先もダウンロードできるようになった

ダウンロード機能は今までは画像要素のみで実行できるものでしたが、いわゆるリンク先のファイルもダウンロードできるようになりました。まるでブラウザみたいですね😲。

// ページ内のzipファイルを丸ごとダウンロード
$('a[href$=".zip"]').download();

おまけ:Puppeteerやcheerio-httpcliをより便利に使うブラウザ拡張機能の紹介

少し上でcheerio-httpcliのクッキー読み書きについて紹介しましたが、今回はなんとさらに!(TVショッピング風)

現在開いているページのクッキー情報をPuppeteerやcheerio-httpcliで読み込める形のクッキーJSONファイルとしてエクスポートすることができるブラウザ拡張機能を用意しました😍。

こちらからインストールできます。

ソースファイルはこちら

これで何ができるかというと、例えば2段階認証が必要なサイトにChromeやFirefoxでログインし、ログインした状態のクッキーJSONをこの拡張機能でエクスポートして、それをPuppeteerやcheerio-httpcliから読み込むことにより、あら不思議、2段階認証が必要なサイトにログインした状態でスクレイピングができるようになるわけです😲。

ただ、上でも少し書きましたが、ログイン状態をクッキー以外(ローカルストレージなど)で管理しているようなサイトではこの方法を使用してもスクレイピングは無理なので、そこはおとなしく泣き寝入りしましょう😢。

最後に

ここまでお付き合いくださり、ありがとうございます😚。

大体のケースにおいてはPuppeteerで事足りるし、Puppeteerの方が高性能であることは事実ですが、Puppeteerをインストールできない環境だったり、マルチインスタンスを駆使して同時進行で並列スクレイピングしたい場合など、ニッチな条件下ではcheerio-httpcliの方が適しているケースもあるかと思いますので、そういった時の選択肢としていかがでしょうか。

今回紹介した機能はGitHubで詳しく説明しているので、ご利用の際はそちらもご確認いただければ幸いです(クッソ長いですが)。

最後に、WEBスクレイピングはルールとマナーとモラルを守って健全な範囲で行いましょう。スクレイピングおじさんとの約束だぞ🤓。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?