ktty1220 / cheerio-httpcli

iconvによる文字コード変換とcheerioによるHTMLパースを組み込んだNode.js用HTTPクライアントモジュール
MIT License
262 stars 28 forks source link

スクレイピング時にETIMEDOUTが発生する #33

Closed ganyariya closed 5 years ago

ganyariya commented 5 years ago

背景

現在スクレピングを非同期で行っており、可能であれば 複数のURLを同時にスクレイピングー>全部終わるまで待つー>全部終わったらまた複数のURLを同時にスクレピング の繰り返しを行いたいと考えています。

つまり、最初 link1, link2, link3のURLたちをスクレピングするなら これら3つの結果が帰ってくるまで待ち、全部終わったらまた別の link4, link5, link6のようにしたいと考えています。

上記の実装のために links = [link1, link2, link3]のような配列を作り

await Promise.all(links.map(scrape)));
console.log("linksすべてのスクレピングが終わるまでここは実行されない");

のようにして、非同期処理を行いながら、linksのすべての非同期なスクレピングが終わるまで待てるようになりました。

発生している問題

ここで問題が発生していて、 これら複数のlinksすべてが終わるまでスクレピングを待つ際に 以下のようなエラーが一部のリンクで発生します。

Error: ETIMEDOUT
    at Timeout._onTimeout (/Users/students/Desktop/electron-example/node_modules/request/request.js:849:19)
    at listOnTimeout (internal/timers.js:531:17)
    at processTimers (internal/timers.js:475:7) {
  code: 'ETIMEDOUT',
  connect: true,
  url: 'http://games.wildtangent.com/fate/'
}

このサイト自体にはブラウザからはアクセスできるため、海外からのスクレピングのようなアクセスを遮断しているか、もしくは、なにかスクレピング時に要求が多すぎて実行されない? ようなエラーが起きていると考えています。

ここで、複数のlinksをPromise.allで待っている際に、上記のエラーが発生すると、永遠に時間が経っても、要求が帰ってこないため、ここで止まってしまいます。

httpcheerio側で、このように長時間返信待ちに強制的に通信を止めるパラメータや手段はありますでしょうか?

ktty1220 commented 5 years ago

ここで、複数のlinksをPromise.allで待っている際に、上記のエラーが発生すると、永遠に時間が経っても、要求が帰ってこないため、ここで止まってしまいます。

Promise.allは指定した処理の中のどれか1つでも失敗するとcatchの方に流れてしまうので、要求が返ってこないというより、どこかにすっ飛んで行ってしまっているのではないかと思います。 以下のようにtrycatchで囲めばおそらく処理は継続できると思います。

try {
  await Promise.all(links.map(scrape)));
  console.log("linksすべてのスクレピングが終わるまでここは実行されない");
} catch (e) {
  // エラー処理
}

ただ、これだといくつか指定したスクレイピングの内、1つでも失敗したものがあると他の成功したスクレイピング結果を取得できません。

そこで、Promise.allではなくPromise.allSettledを使用してみてはどうでしょうか。

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled https://blog.jxck.io/entries/2019-08-20/promise-allsettled-any.html#allsettled

なお、エラーログを見るとElectron環境で実行しているようですが、ElectronのベースとなっているChromiumのJavaScriptエンジンがPromise.allSettledを実装しているかどうか分かりません(たぶん対応していると思いますが)。

もし、対応していない場合でもnpmモジュールのpromise.allSettledをインストールすれば使用できます。

cheerio-httpcliと組み合わせた簡単なサンプル

以下のスクリプトを実行してokと共にresultに各スクレイピング結果が入っているのを確認できました。

const allSettled = require('promise.allsettled');
const client = require('cheerio-httpcli');

(async () => {
  // タイムアウトするまで時間がかかるようならここで調整(ミリ秒)
  client.set('timeout', 10000);

  try {
    const result = await allSettled([
      client.fetch('https://www.yahoo.co.jp/'),  // 成功
      client.fetch('http://localhost:5000/'),    // 自前で立てたダミーサーバー(タイムアウトさせる)
      client.fetch('https://www.yahoo.co.jo/')   // 存在しないドメイン
    ]);
    console.log('ok', result);
  } catch (e) {
    console.log('error', e);
  }
  console.log('done');
})();

というような形で目的は達成できるかと思いますがどうでしょうか。

ganyariya commented 5 years ago

ありがとうございます! Promise.allSettledを追加し、 client.set(timeout)(指定時間建ったら、TLEとしてスクレイピング用フェッチをやめる) 処理を追加することで動かすことが出来ました、ありがとうございます!