2

标签:餐饮外卖,美团,饿了么,百度,爬虫,数据挖掘


爬虫定时抓取外卖平台订单的解决方案

想必很多人都在美团,饿了么,百度上点过外卖吧,每家平台都不定期的发力进行各种疯狂打折活动,好多人都是 三个app都安装的一起比价的策略。而作为大的餐饮企业为了扩大自己的订单量,也是三家都会上自己的商户,但是这 三家平台因为竞争的原因都不支持订单批量导出功能。这个爬虫程序就是这个原因而开发出来的。

想了解客户就要收集销售数据

定位客户,了解客户有很多种渠道,其中收集订单信息是比较客观的数据,我们能从中知道客户的年龄分布,地理位 置分布,喜欢的口味,消费的层次,购买套餐后还喜欢哪些单点等等问题都能逐渐积累的订单数据中挖掘出来, 刚开 始这项艰巨的工作是由运营的童鞋们开始的, 她们每天兢兢业业的Ctrl+C , Ctrl+V的拷贝下来百度,美团,饿了么 后台数据,然后Excel大神生成各种报表,供我们做分析。 但平淡的日子总是渐渐枯燥起来,随着订单越来越来,公 司配送点也越来越多, (三个外卖平台 +自有微信商城) X 配送点 X 每个配送点的订单的数据就是运营童鞋们的 噩梦

重复劳动就应该让机器去做

当运维童鞋正在苦逼复制各种订单数据时, 我已经想到用爬虫技术爬取外卖平台上的订单了, 这件事并不能,之前 学习Nodejs时候,还写过一个爬虫在@煎蛋爬取无聊图和美女图呢:>于是开始调研这三家外 卖平台的后台系统。

三家后台采用的页面技术

平台 后台展现 页面使用的数据接口 可能的抓取方案
美团外卖 网页 and 桌面程序 restful api 请求获取json 或者抓取网页
百度外卖 桌面程序内嵌webkit 动态页面 抓取网页
饿了么 桌面程序内嵌webkit restful api 请求获取json 或者抓取网页

其中百度外卖后台页面非常变态,采用动态页面生成页面还能接受, 订单部分数据特意生成 一大段js代码,

由页面执行渲染后才显示出来,这也是后来在抓取时一个坑。

如何抓取数据

爬虫技术简单说就是用程序模拟人在上网,浏览需要的网页,然后把网页上需要的内容下载提取出来, 转换成结构 化的数据保存起来。这些外卖后台也是一样,基本上都如下面的流程。

人工操作流程

图片描述

抽象出软件执行流程

三家外卖平台抓取的细节都不一样,但总体上可以用下面的方式表示
图片描述

更细化一下的表示

图片描述

核心代码为

/*  爬虫任务的父类
*   定义抓取流程,各步骤的内容
*   抽取出统一的json to csv生成代码
*/
class FetchTask {
    /*  account:{username:String,password:String}
        option:{beginTime:moment,endTime:moment}
    */
    constructor(account,option) {
        this.account = account;
        let end = moment().subtract(1,'days').endOf('day');
        let begin = moment().subtract(option.beforeDays, 'days').startOf('day');
        logger.info(`Start fetch ${account.name} from ${begin.format('YYYY-MM-DD')} to ${end.format('YYYY-MM-DD')} orders`);
        this.option = {
            beginTime: begin,
            endTime: end
        };
        this.columns = {};
    }
    //  任务执行主方法 
    run() {
        return this.preFetch().then(this.fetch.bind(this)).then(this.postFetch.bind(this));
    }
    // 抓取前的准备工作
    preFetch() {
        logger.info(`preFetch ${this.account.name}`);
        return this.login();
    }
    // 保存登录凭证
    setToken(token){
        this.token = token;
        logger.info(`${this.account.name} gets token :${JSON.stringify(token)}`);
    }
    //  执行抓取
    fetch() {
        logger.info(`fetch ${this.account.name}`);
        return this.fetchPageAmount().then(this.fetchPages.bind(this));
    }
    //  登录步骤需要子类实现
    login() {
        return;
    }
    //  抓取分页总数
    fetchPageAmount(){
        return 0;
    }
    //  抓取所有分页上的数据
    fetchPages(pageAmount) {
        let tasks = [];
        for (let pageNum = 1; pageNum <= pageAmount; pageNum++) {
            tasks.push(this.fetchPage(pageNum));
        }
        return promise.all(tasks).then((result)=> {
            return _.flatten(result);
        });
    }
    //  抓取之后的操作,主要是对原始数据转换,格式转换,数据输出
    postFetch(orders){
        logger.info(`postFetch ${this.account.name}`);
        return this.convertToReport(orders).then(this.convertToCSV.bind(this));
    }
    //  原始数据格式转换
    convertToReport(orders){
        return orders;
    }
    //  在postFetch中将数据转换成csv格式并生成文件
    convertToCSV(orders) {
        logger.info(`convertToCSV ${this.account.name}`);
        let option = {
            header: true,
            columns: this.columns,
            quotedString: true
        };
        var begin = this.option.beginTime.format('YYYY-MM-DD');
        var end = this.option.endTime.format('YYYY-MM-DD');
        let reportFile = this.account.name + begin + '_' + end + '_' + uuid.v4().substr(-4, 4) + '.csv';
        let reportPath = path.resolve(__dirname, '../temp', reportFile);
        return new promise(function (resolve, reject) {
            stringify(orders, option, function (err, output) {
                if (err) {
                    reject(err);
                }
                fs.appendFile(reportPath, output, {
                    encoding: 'utf8',
                    flag: 'w+'
                }, function (err) {
                    if (err) return reject(err);
                    logger.info('Generate a report names ' + reportPath);
                    resolve(reportPath);
                });
            });
        });
    }
}
module.exports = FetchTask;

每天凌晨6点钟自动执行抓取任务,定时执行是由later定时库实现的

const ElemeTask = require('./lib/eleme_task');
const BaiduTask = require('./lib/baidu_task');
const MeituanTask = require('./lib/meituan_task');
const mail = require('./lib/mail');
const logger = require('./lib/logger');
const promise = require('bluebird');
const moment = require('moment');
const config = require('config');
const accounts = config.get('account');
const later = require('later');

function startFetch() {
    let option = {beforeDays: 1};
    let tasks = [];
    accounts.forEach((account)=> {
        switch (account.type) {
            case 'meituan':
                tasks.push(new MeituanTask(account, option).run());
                break;
            case 'eleme':
                tasks.push(new ElemeTask(account,option).run());
                break;
            case "baidu":
                tasks.push(new BaiduTask(account,option).run());
                break;
        }
    });
    promise.all(tasks).then((files)=> {
        logger.info('Will send files :' + files);
        mail.sendMail(option, files);
    }).catch((err)=> {
        logger.error(err);
    });
}
later.date.localTime();
let schedule = later.parse.recur().on(6).hour();
later.setInterval(startFetch,schedule);
logger.info('Waimai Crawler is running');

按这个结构就是可以实现各个平台上的抓取任务了,因为不想把文章写成代码review,细节可以直接
访问waimai-crawler


mudiyouyou
94 声望22 粉丝

你好