4

私は Node.js の内部の仕組みにあまり詳しくありませんが、私の知る限り、関数呼び出しが多すぎると「最大呼び出しスタック サイズを超えました」というエラーが発生します。

リンクをたどるスパイダーを作成していますが、ランダムな数の URL をクロールした後にこれらのエラーが発生し始めました。これが発生した場合、ノードはスタック トレースを提供しませんが、再帰エラーがないことは確かです。

requestを使用して URL をフェッチし、 cheerio使用してフェッチした HTML を解析し、新しいリンクを検出しました。スタック オーバーフローは常に Cheerio 内で発生します。Cheerio をhtmlparser2に交換すると、エラーが消えました。Htmlparser2 は、ドキュメント全体を解析してツリーを構築するのではなく、開いているタグごとにイベントを発行するだけなので、はるかに軽量です。

私の理論は、チェリオがスタック内のすべてのメモリを使い果たしたということですが、これが可能かどうかはわかりませんか?

これが私のコードの簡略化されたバージョンです(読み取り専用で、実行されません):

var _       = require('underscore');
var fs      = require('fs');
var urllib  = require('url');
var request = require('request');
var cheerio = require('cheerio');

var mongo   = "This is a global connection to mongodb.";
var maxConc = 7;

var crawler = {
  concurrent: 0,
  queue:      [],
  fetched:    {},

  fetch: function(url) {
    var self = this;

    self.concurrent += 1;
    self.fetched[url] = 0;

    request.get(url, { timeout: 10000, pool: { maxSockets: maxConc } }, function(err, response, body){
      self.concurrent  -= 1;
      self.fetched[url] = 1;
      self.extract(url, body);
    });
  },

  extract: function(referrer, data) {
    var self = this;
    var urls = [];

    mongo.pages.insert({ _id: referrer, html: data, time: +(new Date) });

    /**
     * THE ERROR HAPPENS HERE, AFTER A RANDOM NUMBER OF FETCHED PAGES
    **/
    cheerio.load(data)('a').each(function(){
      var href = resolve(this.attribs.href, referer); // resolves relative urls, not important

      // Save the href only if it hasn't been fetched, it's not already in the queue and it's not already on this page
      if(href && !_.has(self.fetched, href) && !_.contains(self.queue, href) && !_.contains(urls, href))
        urls.push(href);
    });

    // Check the database to see if we already visited some urls.
    mongo.pages.find({ _id: { $in: urls } }, { _id: 1 }).toArray(function(err, results){
      if(err) results = [];
      else    results = _.pluck(results, '_id');

      urls = urls.filter(function(url){ return !_.contains(results, url); });
      self.push(urls);
    });
  },

  push: function(urls) {
    Array.prototype.push.apply( this.queue, urls );
    var url, self = this;

    while((url = self.queue.shift()) && this.concurrent < maxConc) {
      self.fetch( url );
    }
  }

};

crawler.fetch( 'http://some.test.url.com/' );
4

2 に答える 2

0

そこで再帰が行われているようです。これは関数ポインターが格納される場所であるため、再帰関数呼び出しは最終的にスタックを超えます。

それで、これがどのように起こるかです:

  1. fetch 呼び出しは request.get コールバックで抽出します
  2. 呼び出しを抽出し、mongo.pages.find コールバックをプッシュします。
  3. while ループ内でフェッチ呼び出しをプッシュする

スタックがなくなるまで、このサイクルが繰り返されるようです。

あなたの場合、呼び出すまでにスタックが非常に少なくcheerio.loadなっているため、すぐにスタックが不足します。

それがバグなのか意図したものなのかを調べたいと思うかもしれませんが、単純再帰を使用せずにnodejsで同じ効果を得るには、次を使用します。

process.nextTick(functionToCall).

囲まれた関数を離れ、スタックからポインターをポップしますがfunctionToCall、次のティックで を呼び出します。

noderepl で試すことができます:

process.nextTick(function () { console.log('hello'); })

すぐに「こんにちは」を出力します。

に似てsetTimeout(functionToCall, 0)いますが、それよりも優先されます。

コードに関連して、置き換えることができ、スタックが不足することself.fetch(url)process.nextTick(function () { self.fetch(url); })ありません。

そうは言っても、前述のように、コードにバグがある可能性が高いため、最初にそれを調べてください。

于 2012-09-25T01:32:38.937 に答える