cheerio find找不到的debug记录

最近在兼职做爬虫,同事在做越南的需求时,说起用cheerio的find找不到子元素。

页面请戳此处。一开始看到这个页面时,最明显的特征就是好多注释,又有同事说之前抓取facebook的数据的时候也常遇到find不到的情况,正则去掉注释后就可以了,所以当时把注意力集中在注释。

尝试在全文通过class查找之前find要查找的元素,是找的到的。
简化问题如下:

$parent.find(elem) --> not found
$(elem) --> found

查找cheerio的find方法:

exports.find = function(selectorOrHaystack) {
  var elems = _.reduce(this, function(memo, elem) {
    return memo.concat(_.filter(elem.children, isTag));
  }, []);
  var contains = this.constructor.contains;
  var haystack;

  if (selectorOrHaystack && typeof selectorOrHaystack !== 'string') {
    if (selectorOrHaystack.cheerio) {
      haystack = selectorOrHaystack.get();
    } else {
      haystack = [selectorOrHaystack];
    }

    return this._make(haystack.filter(function(elem) {
      var idx, len;
      for (idx = 0, len = this.length; idx < len; ++idx) {
        if (contains(this[idx], elem)) {
          return true;
        }
      }
    }, this));
  }

  return this._make(select(selectorOrHaystack, elems, this.options));
};

发现经过重重的if,最后落在了this._make(select(selectorOrHaystack, elems, this.options)); ,_make只是制作cheerio object的一个函数,而select则是css-select 这个模块的方法,到此处我就疑惑了,难道elem不是$parent的子元素,因为cheerio本身$(elem)也是通过select来查找的。继续追踪select方法:

var selectAll = getSelectorFunc(function selectAll(query, elems){
  return (query === falseFunc || !elems || elems.length === 0) ? [] :         findAll(query, elems);
});

 function CSSselect(query, elems, options){
   return selectAll(query, elems, options);
}

findAll又是什么来历?原来是css-select的依赖domUtil的方法

function findAll(test, elems){
  var result = [];
  for(var i = 0, j = elems.length; i < j; i++){
      if(!isTag(elems[i])) continue;
      if(test(elems[i])) result.push(elems[i]);

      if(elems[i].children.length > 0){
        result = result.concat(findAll(test, elems[i].children));
    }
  }
  return result;
}

到此处,之前的疑惑越来越大,由于它是遍历children来递归查找的,意味着elem可能在此处判断为不是$parent的子元素。

为了证实这个猜想,机(yu)智(chun)的我于是用util.inspect() 方法将解析出来的dom obj打印了出来:

Anh Lương đi làm xa nhà tháng mới về một lần, hôm nay nghe tin anh Lương về lòng chị Ví thấy vui vui lạ. Chiều nay nghe tin anh Lương về, lòng chị Ví bỗng thấy vui lạ. Hai…


many hentry under
many hentry under
many hentry under

以上是出了问题的html,略长== 以下是我通过util.inspect() 打印出的部分,注意看.entry-summary是个注释,它的前一个兄弟元素居然是#main

{ data: ' .entry-summary ',
  type: 'comment',
  next: [Circular],
  prev:
   { type: 'tag',
     name: 'div',
     attribs: { id: 'main' },
     children:
      [ [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [length]: 6 ],
     next: [Circular],
     prev:
      { data: '\n\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t',
        type: 'text',
        next: [Circular],
        prev: [Object],
        parent: [Circular] },
     parent: [Circular] },
  parent: [Circular] }

因为解析html是在htmlparser2这个库里进行的,于是我就去向作者提了个issue,当时的想法是:注释怎么可能是#main的兄弟节点,一定是bug== , 后来作者回复如下:

Screenshot from 2015-12-01 22:43:45.png

虽然回复如此,但是我看了以下domhandler,发现它只是组装dom object而已,并不涉及具体的parser过程,带着疑惑,我继续追查下去。

这次我学聪明了,既然定位到问题出在htmlparser2上,我索性把网站源码wget下来,借助htmlparser2的接口,在openTag和closeTag的时候打印出节点情况,结果发现:

Anh Lương đi làm xa nhà tháng mới về một lần, hôm nay nghe tin anh Lương về lòng chị Ví thấy vui vui lạ. Chiều nay nghe tin anh Lương về, lòng chị Ví bỗng thấy vui lạ. Hai…

注意p包住了很多div节点,如下是其中一部分结果:

script start; attribs: {"type":"text/javascript","src":"https://apis.google.com/js/platform.js"}
script   end;
div  .defualt-button-gplus end;
div  .ssb-share ssb-share-9977 defualt end;
p   end;
-->

追踪到这里,才发现原来在p标签闭合时,div标签还没有完全闭合,但是因为Parser.js中的onclosetag

Parser.prototype.onclosetag = function(name){
    this._updatePosition(1);

    if(this._lowerCaseTagNames){
        name = name.toLowerCase();
    }

    if(this._stack.length && (!(name in voidElements) || this._options.xmlMode)){
        var pos = this._stack.lastIndexOf(name);
        if(pos !== -1){
            if(this._cbs.onclosetag){
                pos = this._stack.length - pos;
                while(pos--) this._cbs.onclosetag(this._stack.pop());
            }
            else this._stack.length = pos;
        } else if(name === "p" && !this._options.xmlMode){
            this.onopentagname(name);
            this._closeCurrentTag();
        }
    } else if(!this._options.xmlMode && (name === "br" || name === "p")){
        this.onopentagname(name);
        this._closeCurrentTag();
    }
};

主要在第一个if上,当在stack中找的到p这个name时,就将p内的所有都pop出来,或者this._stack.length = pos,也就是直接删掉了。

到这里就清楚了,就是html页面本身不规范,在闭合p标签时并没有闭合完p标签内的所有子元素标签,导致在解析时,以为p标签内元素已经完全结束,从而导致整个文档后面的解析出现错误。

和注释半毛钱关系都没有啊。

这样一场追踪下来,笨方法虽然最后找到了问题,但是觉得最开始就应该排除所有的看上去不相关的因素,直接抓取源码,对源码进行观察解析,而不是通过浏览器观察(浏览器的容错性太强==);另外定位也定的不是很准确,最开始测试时就发现了cheerio并没有解析到elem是$parsent的子元素,此时就应该去查看在何处进行html的parse的。

吃一堑长一智,查bug要拿代码说话,千万不要主观臆想和猜测,先入为主==

另外,和别人讨论了一下,关于类似htmlparser2这种库是不是应该做强大的容错性处理,要是不容错的话,很容易出现解析和网页本意结果不同的情况,但是又感觉容错这种东西,让一个解析库来做实在是太复杂,难不成要做成一个浏览器那样的解析能力?

你可能感兴趣的:(cheerio find找不到的debug记录)