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打印了出来:

<div id="main">
  <div class="hfeed">
    <div id="post-9977" class="hentry post publish post-1 odd author-qt category-truyen-cuoi-hang-ngay">                    
        <div class="sticky-header">
            <h2 class="post-title entry-title"><a href="xxx">Chuyện vợ chồng anh Lương chị Ví</a></h2>
            <div class="byline">
                <time class="published">August 25, 2015</time> · by <span class="author vcard">
                <a class="url fn n" rel="author" href="xxxx" title="Trùm Cười">Trùm Cười</a></span> · in <span class="category"><a href="http://cuoivuive.com/truyen-cuoi/truyen-cuoi-hang-ngay" rel="tag">Truyện cười hàng ngày</a></span>
            </div>                                      
        </div>
        <!-- .sticky-header -->
        <div class="entry-summary">
            <p>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 class="ssb-share ssb-share-9977 defualt" post_id="9977">
                <div class="defualt-button-fb">
                    <iframe src="xxx" ></iframe>
                </div>
                <div class="defualt-button-twitter">
                    <iframe id="twitter-widget-0"  src="xxx"></iframe>
                    <br>
                    <script>
                        !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?"http":"https";if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document, "script", "twitter-wjs");
                    </script>
                </div>
                <div class="defualt-button-gplus">
                    <script type="text/javascript" src="xxx" gapi_processed="true"></script>
                    <p></p>
                    <div id="___plusone_0" >
                        <iframe src="xxx"></iframe>
                    </div>
                    <p></p>
                </div>
            </div>                                                              
        </div>
    <!-- .entry-summary -->
    </div>
    <!-- .hentry -->
    <div class="hentry"> many hentry under</div>
    <!-- .hentry -->
    <div class="hentry"> many hentry under</div>
    <!-- .hentry -->
    <div class="hentry"> many hentry under</div>
    <!-- .hentry -->
  </div>
</div>

以上是出了问题的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的时候打印出节点情况,结果发现:

<div class="entry-summary">

  <p>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…
    <div class='ssb-share ssb-share-9977 defualt' post_id='9977'>
      <div class="defualt-button-fb">
        <iframe src="xxx"></iframe>
      </div>
      <div class="defualt-button-twitter">
        <a href="https://twitter.com/share" class="twitter-share-button" data-url="http://cuoivuive.com/chuyen-vo-chong-anh-luong-chi-vi">Tweet</a>
        <br />
        <script>
        </script>
      </div>
      <div class="defualt-button-gplus">
        <script type="text/javascript" src="https://apis.google.com/js/platform.js"></script>
  </p>
  <div class="g-plusone" data-size="medium" data-href="http://cuoivuive.com/chuyen-vo-chong-anh-luong-chi-vi"></div>
  </p>
  </div>
  </div>

</div>
<!-- .entry-summary -->

</div>
<!-- .hentry -->

注意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这种库是不是应该做强大的容错性处理,要是不容错的话,很容易出现解析和网页本意结果不同的情况,但是又感觉容错这种东西,让一个解析库来做实在是太复杂,难不成要做成一个浏览器那样的解析能力?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容