如何爬取外卖平台商家订单

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


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

想必不少人都在美团,饿了么,百度上点过外卖吧,每家平台都不按期的发力进行各类疯狂打折活动,好多人都是 三个app都安装的一块儿比价的策略。而做为大的餐饮企业为了扩大本身的订单量,也是三家都会上本身的商户,可是这 三家平台由于竞争的缘由都不支持订单批量导出功能。这个爬虫程序就是这个缘由而开发出来的。java

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

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

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

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

三家后台采用的页面技术web

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

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

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

如何抓取数据

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

人工操做流程

图片描述

抽象出软件执行流程

三家外卖平台抓取的细节都不同,但整体上能够用下面的方式表示
图片描述微信

更细化一下的表示restful

图片描述

核心代码为

/*  爬虫任务的父类
*   定义抓取流程,各步骤的内容
*   抽取出统一的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

相关文章
相关标签/搜索