1. 网络爬虫 #

网络爬虫是一种自动获取网页内容的程序,功能如下

  1. 发出HTTP请求获取指定URL中的内容
  2. 使用jQuery的语法操作网页元素,提取需要的元素
  3. 将数据保存到mysql数据库中
  4. 建立web服务器显示这些数据
  5. 使用计划任务自动执行更新任务
  6. 布署项目到阿里云中并配置反向代理

2. 预备知识 #

2.1 request #

一个简单的HTTP请求工具,用来获取网页内容

request

var request = require('request');
var iconv = require('iconv-lite');
request({url: 'http://top.baidu.com/category?c=10&fr=topindex'
, encoding: null},function(err,response,body){
    if(err)
        console.error(err);
    body = iconv.decode(body, 'gbk').toString();
    var regex = /<a href=".\/buzz\?b=\d+&c=\d+">.+<\/a>/g;
    console.log(body.match(regex));
})

2.2 cheerio #

在服务器端实现了jQuery中的DOM操作API

cheerio

var cheerio = require('cheerio');
var $ = cheerio.load('<ul>
<li><a href="./buzz?b=353&c=10">玄幻奇幻</a></li>
\<li><a href="./buzz?b=354&c=10">爱情</a></li></ul>');
$('ul li a').each(function () {
    var $me = $(this);
    var item = {
        name: $me.text().trim(),
        url: $me.attr('href').slice(2)
    }
    var result = item.url.match(/buzz\?b=(\d+)/);
    if (Array.isArray(result)) {
        item.id = result[1];
    }
    console.log(item);
});

2.3 cron #

用来周期性的执行某种任务或等待处理某些事件的一个守护进程

cron

符号 含义
星号(*) 代表所有可能的值
逗号(,) 可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”
中杠(-) 可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”
正斜线(/) 可以用正斜线指定时间的间隔频率,*/10,如果用在minute字段,表示每十分钟执行一次
var cronJob = require('cron').CronJob;
var job1 = new cronJob("* * * * * *",function(){
  console.log('每秒');
});
job1.start();

2.4 debug #

根据环境变量的有选择向控制台输出调试信息

debug

var debug = require('debug')('crawler:main');
//windows set DEBUG=crawler:*
//linux export DEBUG=crawler:*
debug('welcome to zhufengpeixun');

2.5 child_process #

child_process即子进程可以创建一个系统子进程并执行shell命令

spawn语法

child_process.spawn(cmd, args=[], [options])

示例

var url = require('url');
var fs = require('fs');
var DOWNLOAD_DIR = './';
var spawn = require('child_process').spawn;
// 使用curl下载文件的函数
var download_file_curl = function(file_url) {
    // 提取文件名
    var file_name = url.parse(file_url).pathname.split('/').pop();
    // 创建一个可写流的实例
    var file = fs.createWriteStream(DOWNLOAD_DIR + file_name);
    // 使用spawn运行curl
    var curl = spawn('curl', [file_url]);
    // 为spawn实例添加了一个data事件
    curl.stdout.on('data', function(data) { file.write(data); });
    // 添加一个end监听器来关闭文件流
    curl.stdout.on('end', function(data) {
        file.end();
        console.log(file_name + ' downloaded to ' + DOWNLOAD_DIR);
    });
    // 当子进程退出时,检查是否有错误,同时关闭文件流
    curl.on('exit', function(code) {
        if (code != 0) {
            console.log('Failed: ' + code);
        }
    });
};

download_file_curl('http://pic.baidu.jpg');

exec语法

child_process.exec(cmd, [options], callback)

示例

var url = require('url');
var fs = require('fs');
var DOWNLOAD_DIR = './';
var exec = require('child_process').exec;
var download_file_curl = function(file_url) {
    // 提取文件名
    var file_name = url.parse(file_url).pathname.split('/').pop();

    // 使用exec执行curl命令
    var child = exec('curl '+file_url+' -o '
    +DOWNLOAD_DIR+file_name, function(err, stdout, stderr) {
        if (err) throw err;
        else console.log(file_name + ' downloaded to ' + DOWNLOAD_DIR);
    });
};
download_file_curl('http://pic.baidu.jpg');

2.6 async #

async是一个流程控制库,为我们带来了丰富的嵌套解决方案

async

2.6.1 串行执行 #

串行执行一个函数数组中的每个函数 series(tasks,callback);

async.series([function(callback){
        callback(null, "tv is over");
    },function(callback){
        callback(null, 'homework is down');
    }],function(err, results) {
    console.log(results);
});

2.6.2 并行执行 #

parallel函数是并行执行多个函数,每个函数都是立即执行,不需要等待其它函数先执行 parallel(tasks, [callback])

console.time('start');
async.parallel([
        function (callback) {
            setTimeout(function () {
                callback(null, 'one');
            },2000);
        },
        function (callback) {
            setTimeout(function () {
                callback(null, 'two');
            },3000);
        }
    ],
    function (err, results) {
        console.log(results);
        console.timeEnd('start');
    });

2.6.3 waterfall(瀑布) #

waterfall和series函数有很多相似之处,都是按顺序依次执行一组函数,不同之处是waterfall每个函数产生的值,都将传给下一个函数 waterfall(tasks, [callback])

async.waterfall([function(callback){
        callback(null, "水");
    },function(data,callback){
        callback(null, data+'+咖啡');
    },function(data,callback){
    callback(null, data+'+牛奶');
}],function(err, results) {
    console.log(results);//水+咖啡+牛奶
});

2.6.4 自动依赖 #

用来处理有依赖关系的多个任务的执行 auto(tasks, [callback]);

async.auto({
    getWater: function(callback){
        callback(null, 'Water');
    },
    getFlour: function(callback){
        callback(null, 'Flour');
    },
    mixFlour: ['getWater', 'getFlour', function(callback, results){
        callback(null, results['getWater']+','
        +results['getFlour']+','+'mixFlour');
    }],
    steam: ['mixFlour', function(callback, results){
        callback(null, results['mixFlour']+',steam');
    }]
}, function(err, results) {
    console.log('err = ', err);
    console.log('results = ', results);
});

注意callback, results的顺序在不同的async版本中不一样

2.6.5 迭代多个异步任务 #

所有的任务都迭代完成后才执行后续任务

var arr = [{name:'zfpx1'},{name:'zfpx2'}];
async.forEach(arr, function(item, callback) {
    console.log('1.1 enter: ' + item.name);
    setTimeout(function(){
        console.log('1.1 handle: ' + item.name);
        callback(null);
    }, 1000);
}, function(err,result) {
    console.log('1.1 err: ' + err);
});

3.实现爬虫 #

3.1 抓取内容 #

3.1.1 读取内容 #

tasks/read.js

var request = require('request');
var cheerio = require('cheerio');
var debug = require('debug')('crawler:read');
var iconv = require('iconv-lite');
module.exports =  function (url, callback) {
    var items = [];
    debug('读取电影列表');
    request({url:url,encoding:null},function(err,response,body){
        body = iconv.decode(body,'gbk');
        var $ = cheerio.load(body);
        $('.keyword .list-title').each(function(){
            var that = $(this);
            var item = {
                name:that.text().trim(),
                url:that.attr('href')
            }
            items.push(item);
            debug('读取电影 ',item);
        });
        callback(null,items);
        debug('读取电影列表完毕');
    });
}

3.1.2 数据库操作 #

model/index.js

var mongoose = require('mongoose');
mongoose.connect('mongodb://123.57.143.189/zhufengcrawl');
exports.Movie =  mongoose.model('Movie',new mongoose.Schema({
    name:String,
    url:String
}));

3.1.3 保存内容 #

tasks/save.js

var async = require('async');
var Movie = require('../model').Movie;
var debug = require('debug')('crawler:save');

module.exports = function(items,callback){
    async.series([
       function(callback){
           debug('清空电影列表');
           Movie.remove({},callback);
       },
       function(callback){
           debug('保存电影列表');
           async.forEach(items, function (item, next) {
               debug('保存电影 ',item);
               Movie.create(item,next);
           }, callback);
       }
    ],function(err,result){
        debug('保存电影完毕');
        callback();
    })
}

3.1.4 入口文件 #

tasks/main.js

var read = require('./read');
var save = require('./save');
var async = require('async');
var debug = require('debug')('crawler:main');
debug('开始计划任务');
var movies = [];
async.series([
    function(done){
        read('http://top.baidu.com/buzz?b=26&c=1&fr=topcategory_c1',function(err,items){
            movies = items;
            done();
        });
    },
    function(done){
        save(movies,done);
    }
],function(err,result){
    debug('结束计划任务');
    process.exit(0);
})

3.1.5 计划任务 #

app.js

var spawn = require('child_process').spawn;
var cronJob = require('cron').CronJob;
var path = require('path');
var job = new cronJob('* * * * * ',function(){
    var update = spawn(process.execPath,[path.join(__dirname,'tasks/main.js')]);
    update.stdout.pipe(process.stdout);
    update.stderr.pipe(process.stderr);
    update.on('close',function(code){
        console.log(code);
    });
    update.on('error',function(code){
        console.log(code);
    });
});
job.start();

3.2 页面展示 #

3.2.1 启动应用 #

app.js

var express = require('express');
var app = express();
app.set('view engine', 'jade');
app.set('views', 'views');
var Movie = require('./model').Movie;
app.get('/', function (req, res) {
    Movie.find({}, function (err, movies) {
        res.render('index', {
            movies: movies
        });
    });
});
app.listen(9090);

var spawn = require('child_process').spawn;
var cronJob = require('cron').CronJob;
var path = require('path');
var job = new cronJob('* * * * * ',function(){
    var update = spawn(process.execPath,[path.join(__dirname,'tasks/main.js')]);
    update.stdout.pipe(process.stdout);
    update.stderr.pipe(process.stderr);
    update.on('close',function(code){
        console.log(code);
    });
    update.on('error',function(code){
        console.log(code);
    });
});
job.start();

3.2.2 编写模板 #

doctype html
html
    head
        title 电影风云榜
        link(rel="stylesheet",href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css")
    body
        .container
            .panel.panel-primary
                .panel-heading.text-center 电影风云榜
                .panel-body
                    ul.list-group
                        each movie in movies
                            li.list-group-item
                                a(href="#{movie.url}")=movie.name

4. 资源 #

项目源码