林夕轩

立志于前端的超级菜鸟就是我

手把手教你爬女神的微博

前段时间突然兴起想爬一爬女神的微博,把数据存起来做一个时光轴什么的。当初想到可能工作的重点是如何写一个漂亮的时光轴,至于数据嘛。交给强(xu)力(ruo)的新浪API。但是经过研究发现获取指定用户的微博已经不可用了——获取用户发布的微博。文档中明确说明了只能获取自身用户的微博

看到这里,心想只能写爬虫咯。前段时间又正好写了个PHP脚本爬了咱们首页的所有图片进行流量分析。嗯,应该不难,写好模式匹配就好了。可一看微博,傻眼了,这微博异步加载的啊。一开始只能加载1/3的内容,且在不登录情况下只能看到首页的1/3的内容。对其他人来说,也就走到这里了。但对我们大(xiao)前(cai)端(niao)来说都不是个事!手握selenium和casper两大杀器,没有爬不了的微博!接下来由我为您一一道来...

selenium vs casper

做过自动化测试的同学肯定对selenium比较熟悉,想当初研究生毕业论文里10篇论文有7篇是讲selenium在XX系统中的应用,⊙﹏⊙b汗。它相比于casper就像加特林相比于左轮。一个重型到与浏览器或其它系统集成,支持多种语言的扩展。一个轻量到只运行在phantom环境中,所有操作均在内存中执行。

后来,我选择使用casper,主要是因为写过selenium,想尝试下casper的开发。并且selenium的速度实在让人难以接受,而运行于内存中的casper能让人有飞的感觉。

整个开发过程下来发现,casper确实很快,并且是使用js的语法,上手更快。但执行的过程不像selenium那么直观,只能通过读log的方式。而本身这次开发就是一个轻量型的任务,使用casper更胜一筹。

搭环境

分成两步:安装phantomjs、安装casper。注:本文只说明mac环境下的安装,其它系统请自行脑补O(∩_∩)O

安装phantomjs

使用hombrew安装最新版的phantomjs:brew update && brew install phantomjs

使用npm安装:npm install -g phantomjs

注:虽然phantom能通过npm安装,但不代表它是nodejs的一个工具。它已经明确说明了,它不能当做node执行代码,也不能使用node的插件

安装casper

使用homebrew安装:brew install casper

使用稳定版的casper与最新版phantom(测试时为1.9.2)不兼容,之后选择了beta版casper能正常使用

hello world

以下是一个casper版本的hello world。

var casper = require('casper').create({   
        verbose: true, 
        logLevel: 'debug',
        clientScripts:  [
            'lib/jquery-1.7.1.min.js',      // These two scripts will be injected in remote
        ],
        pageSettings: {
             loadPlugins: true,         
             userAgent: 'Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0'
        }
    });
phantom.outputEncoding="utf-8";
casper.options.viewportSize = {width: 1680, height: 924};
casper.start('http://www.meituan.com')
.waitForSelector('.site-fs', function () {
    console.log('Hello World!');
})
.run();

保存为helloworld.js,使用命令casperjs helloworld.js运行脚本,于是控制台会出现如下log信息。

[info] [phantom] Starting...
[info] [phantom] Running suite: 3 steps
[debug] [phantom] opening url: http://bj.meituan.com/, HTTP GET
[debug] [phantom] Navigation requested: url=http://bj.meituan.com/, type=Other, willNavigate=true, isMainFrame=true
[debug] [phantom] url changed to "http://bj.meituan.com/"
2014-08-07 23:20:24.500 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Arial" and got font with PostScript name "ArialMT". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:24.500 phantomjs[20931:110b] CoreText performance note: Set a breakpoint on CTFontLogSuboptimalRequest to debug.
2014-08-07 23:20:24.502 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Arial" and got font with PostScript name "ArialMT". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:24.512 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Microsoft YaHei" and got font with PostScript name "MicrosoftYaHei". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:24.514 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Microsoft YaHei" and got font with PostScript name "MicrosoftYaHei". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:25.784 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Arial" and got font with PostScript name "ArialMT". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:25.812 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Arial" and got font with PostScript name "ArialMT". For best performance, only use PostScript names when calling this API.
2014-08-07 23:20:25.813 phantomjs[20931:110b] CoreText performance note: Client called CTFontCreateWithName() using name "Arial" and got font with PostScript name "ArialMT". For best performance, only use PostScript names when calling this API.
[debug] [phantom] Automatically injected lib/jquery-1.7.1.min.js client side
[debug] [phantom] Successfully injected Casper client-side utilities
[debug] [phantom] start page is loaded
[info] [phantom] Step _step 3/3 http://bj.meituan.com/ (HTTP 200)
[info] [phantom] Step _step 3/3: done in 6412ms.
[info] [phantom] waitFor() finished in 40ms.
[info] [phantom] Step anonymous 4/4 http://bj.meituan.com/ (HTTP 200)
Hello World!
[info] [phantom] Step anonymous 4/4: done in 6473ms.
[info] [phantom] Done 4 steps in 6492ms

上面的log信息主要就是显示加载资源文件的一些info和debug信息。在倒数第三行可以看到,输出了Hello World!

是的就这么简单!现在一步一步介绍下如何写一个简单的casper脚本。

UA设定及环境配置

因为casper脚本实在Phantom环境中运行的,我们可以通过capser提供的方法创建任意类型的浏览器(UA),如例子中的代码给create函数传入配置项:

    {
        verbose: true, //开启详细日志功能,只有这个设置成true,logLevel才有效
        logLevel: 'debug',    // 日志显示的级别
        clientScripts:  [    // 在UA中可用的js框架
            'lib/jquery-1.7.1.min.js',
        ],  
        pageSettings: {    //UA设置,
             loadPlugins: true,    
             userAgent: 'Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0'
        }
   }

环境配置一般有视口的设置及输出信息编码等

phantom.outputEncoding="utf-8";
casper.options.viewportSize = {width: 1680, height: 924};

获取页面并操作

casper.start('http:bj.meituan.com'); //访问美团北京首页

casper使用阻塞的方式按顺序执行任务,它提供丰富的wait函数帮组开发者在合适的时机进行操作,比如例子中的:

waitForSelector('.site-fs', function () {
    console.log('Hello World!');
})

casper会在“看到”首屏的节点[class=site-fs]打出Hello World!的日志 最后执行一下casper.run()让脚本跑起来。但是casper脚本的最优写法是使用类似于jQuery中操作链接的方式。

这样,一个简单版本的casper就搞定了!是不是超级简单?下面进入正题,教你写一个爬女神微博的casper脚本。

weibo spider

应该要解决哪些问题

  • 动态登录微博
  • 动态触发加载更多微博
  • 加载完全之后如何优雅的结束程序

针对第一点,当我们使用casper访问女神的微博时,右上角有个登录的按钮,点击后会跳转到登录页。这时候,我们需要在登录页输入账号信息并在登录后让casper跳回女神的微博页。这样就复杂很多了。但是,有更好的办法!我们让casper滚动到页面底部或者点击底部加载更多按钮会弹出一个登录对话框,这样就能在不跳转的前提下安心访问女神首页啦!

上面已经介绍了如何登录微博和加载更多微博了,那如何在获取了女神微博后优雅地结束程序呢?因为每个微博页面最多展示50条微博,这时候如果检测到当前页面里的微博数量是50或者大于30条时就可以退出啦!你问我怎么知道,因为每次加载更多微博时都是新增15条

####重难点

  • 动态识别登陆框:使用casper的waitfor机制,监控登录弹窗是否创建了。当casper捕捉到创建了弹窗,会执行回调里的逻辑。而不需要我们自己写循环去取,太棒了,有木有!
  • 动态加载更多微博:微博页面一般会有两个情况,一种是点击按钮请求更多微博,一种是滚动到页面底部,自动触发请求。这两点,使用casper都能很好的实现
  • 如何在casper中和页面内容进行交互,使用casper evaluate。在casper的脚本中有两个执行环境,一个是在casper中,一个是在页面中。后者必须通过casper evaluate把脚本插入到页面中去。回过头去看在创建casper对象的时候content script就是声明在页面中执行的js有哪些。

光说不练假把式

现在祭出微博爬虫脚本。声明:因为是赶忙写的,有些地方写的比较死板,聪明如你肯定能把脚本改的更加优雅滴。


var casper = require('casper').create({   
        verbose: true, 
        logLevel: 'debug',
        clientScripts:  [
            'lib/jquery-1.7.1.min.js',      // These two scripts will be injected in remote
        ],
        pageSettings: {
             loadPlugins: true,         
             userAgent: 'Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0'
        }
    });
casper.resJson = '';
phantom.outputEncoding="utf-8";
casper.options.viewportSize = {width: 1680, height: 924};
casper.start('http://weibo.com/linariel?page=1') //输入女神微博链接,一定要加上page参数强制UA跳转到微博首页
.waitForSelector('.PRF_feed_list_more', function () {
    this.evaluate(function() {
        window.scrollTo(0, 100000);
    });
    this.click('.PRF_feed_list_more');
})
.waitFor(function () {
    return this.evaluate(function () {
        return $('.PRF_feed_list_more').length <= 0;
    })
})
.then(function () {
    this.evaluate(function() {
        window.scrollTo(0, 1000000);
    });
})
.waitForSelector('.layer_login_register', function () {
    this.click('.layer_login_register [action-data="tabname=login"]');
    this.wait(1000, function () {
        this.evaluate(function () {
            $('.layer_login_register .item_login [name="username"]').val('account'); // 填入你的账号
            $('.layer_login_register .item_login [name="password"]').val('password'); // 填入你的密码
        });
        this.click('[node-type="submitBtn"]');
    });
})
.waitFor(function () {
    return this.evaluate(function() {
        return $('.layer_login_register').length <= 0;
    });
})
.then(function () {
    this.echo(this.evaluate(function() {
        return $('.gn_name').html();
    }));
})
.then(function () {
    this.echo(this.evaluate(function() {
        window.scrollTo(0, 1000000);
        return $('.pf_name .name').html();
    }));
})
.then(function () {
    this.wait(500, function () {
        this.echo(this.evaluate(function() {
            window.scrollTo(0, 100000);
            return $('.WB_detail').length;
        }));
    });
})
.then(function () {
    this.wait(3000, function () {
        this.echo(this.evaluate(function() {
            window.scrollTo(0, 100000);
            return $('.WB_detail').length;
        }));
    });
})
.then(function () {
    this.wait(3000, function () {
        this.echo(this.evaluate(function() {
            window.scrollTo(0, 100000);
            return $('.WB_detail').length;
        }));
    });
})
.then(function () {
    this.evaluate(function () {
        window.scrollTo(0, 0);
    });
    this.captureSelector('linyichen.png', '.WB_feed');
})
.run();

好了,这就是一个简单粗暴的微博爬虫脚本。女神的微博截图就拿到了,哟吼吼吼!看到代码,可以发现有很多地方可以改的更完善。而且casper根据时间wait是不够靠谱的,这受网速的影响比较大。所以尽量少用时间作为阻塞的条件转而使用特定条件会更好。

希望这篇文章能给你提供帮助,增加对casper的认识。如果转载,请附上链接:http://zmx.im/blog/?bname=casper

附上女神的微博截图:

<a href="/images/linyichen.png" target="_blank"><img src="/images/linyichen.png" style="zoom:0.05"/></a>

2014-8-7

casper 爬虫 自动化 微博 phontam 自动