Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

趁着双11,写个京东商品自动下单 #13

Open
shaodahong opened this issue Oct 27, 2017 · 13 comments
Open

趁着双11,写个京东商品自动下单 #13

shaodahong opened this issue Oct 27, 2017 · 13 comments

Comments

@shaodahong
Copy link
Owner

shaodahong commented Oct 27, 2017

项目地址 求个 star

在现在,商家一年不卖货,双11卖出一年的货是大家都知道的事实了,总得来说调一调蚊子腿的价格,聊胜于无,但是也会有些神价格会出现,这时候买到就是赚到

本来是想趁着双11组台电脑,买个 Z370 的板U套装,没想到京东的 8700k 一直是无货的状态,这几天有货了,价格涨到了3999,简直不能忍,看了下板U套装比较划算,但是有些板U套装是不支持自动下单的,所以 gayhub 搜搜看有没有爬虫可以监听到货自动下单的,正好有了这哥们的 jd-autobuy Python 脚本,还有 Go 的,看了下接口已经很齐全了,来个 node 版本的助助兴

这次用到的 http 库是 axios,支持客户端和服务端,总得来说语法还是很简洁的,在这之前还有个 superagent 库,看了下也差不多,只不过 superagent 在 response 上多处理了下

因为在 vue 中使用了 axios,这次想试试服务端的能力咋样,还是一如既往的好,滋次一波

先写个 request header ,毕竟是服务端,没有浏览器帮你处理 User-Agent,所以自己去浏览器请求下然后把 header 拿到

const defaultInfo = {
    header: {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
        'Content-Type': 'text/plain;charset=utf-8',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.6,en;q=0.4,en-US;q=0.2',
        'Connection': 'keep-alive',
    },
}

header 拿到我们就可以伪装成浏览器去请求二维码图片了,京东的扫码图片地址 https://qr.m.jd.com/show,没有多余的技巧,直接用 axios 来个get请求即可

async function requestScan() {
    const result = await request({
        method: 'get',
        url: 'https://qr.m.jd.com/show',
        headers: defaultInfo.header,
        params: {
            appid: 133,
            size: 147,
            t: new Date().getTime()
        },
        responseType: 'arraybuffer'
    })
}

参数 appid sizet 可以通过抓包拿到的,这里注意我 responseType 用的 arraybuffer,默认值是 json ,buffer 主要是方便我们来像本地写入图片,我们来处理下 res

defaultInfo.cookies = cookieParser(result.headers['set-cookie'])
defaultInfo.cookieData = result.headers['set-cookie'];
const image_file = result.data;
await writeFile('qr.png', image_file)
async function writeFile(fileName, file) {
    return await new Promise((resolve, reject) => {
        fs.writeFile(fileName, file, 'binary', err => {
            opn('qr.png')
            resolve()
        })
    })
}

这一步 cookie 已经拿到了,这里我做了两步处理,一步是自己写的 cookieParser 把参数进行解析,主要是拿到其中的 wlfstk_smdl,接下来会用到,然后直接 writeFile 写入图片就行了,写好了之后利用 opn 打开图片,sindresorhus 大神的 opn 库还是蛮好用的,可以指定程序打开图片,文件等

在扫码之前我们要监听扫码的状态

async function listenScan() {

    let flag = true
    let ticket

    while (flag) {
        const callback = {}
        let name;
        callback[name = ('jQuery' + getRandomInt(100000, 999999))] = data => {
            console.log(`   ${data.msg || '扫码成功,正在登录'}`)
            if (data.code === 200) {
                flag = false;
                ticket = data.ticket
            }
        }

        const result = await request({
            method: 'get',
            url: 'https://qr.m.jd.com/check',
            headers: Object.assign({
                Host: 'qr.m.jd.com',
                Referer: 'https://passport.jd.com/new/login.aspx',
                Cookie: defaultInfo.cookieData.join(';')
            }, defaultInfo.header),
            params: {
                callback: name,
                appid: 133,
                token: defaultInfo.cookies['wlfstk_smdl'],
                _: new Date().getTime()
            },
        })

        eval('callback.' + result.data);
        await sleep(1000)
    }

    return ticket
}

一开始的想法是开个定时器来轮询下:"好没好呀",没有我1秒后再来问下,这里使用 async/await
的强大功能实现个 sleep,比 setTimeout 的使用更优雅而且对于异步的处理也能够操控自如

function sleep(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, ms)
    })
}

这里我们把 header 组合一下,刚刚拿到的 cookie 带上,并加上 hostreferer 来表明我们从哪里来要到哪里去,参数里面的 token 就是之前解析 cookie 拿到的 wlfstk_smdl ,这个接口应该约定的 jQuery jsonp(京东看了下 jsonp 还是蛮多的),所以我这里使用一个 callback 来模拟一个 jsonp 的执行,看返回的 code 和 msg,code 为 200 的时候说明扫码成功了,这时候 msg 是没有的,所以自定义下,其他状态是有 msg 的,直接输出就 OK 了,扫码成功我们要拿到 ticket,这个从字面上理解就知道了,大兄弟你拿到入场券了,并且 ticket 下单的时候也是需要的,存起来

这时候用你的手机打开京东扫一扫打开的二维码图片,确认后扫码成功,用入场券登录去

async function login(ticket) {
    const result = await request({
        method: 'get',
        url: 'https://passport.jd.com/uc/qrCodeTicketValidation',
        headers: Object.assign({
            Host: 'passport.jd.com',
            Referer: 'https://passport.jd.com/uc/login?ltype=logout',
            Cookie: defaultInfo.cookieData.join('')
        }, defaultInfo.header),
        params: {
            t: ticket
        },
    })

    defaultInfo.header['p3p'] = result.headers['p3p']
    return defaultInfo.cookieData = result.headers['set-cookie']
}

这一步没什么说的,入场券有了,理所应当登录成功了,拿到 p3p 参数并且更新下 cookie 这样一个合法的身份就诞生了

有了身份后就可以去 get 商品页面,这一步需要拿三个请求的信息拼一下

拿到商品页面的 html

function goodInfo(goodId) {

    const stockLink = `http://item.jd.com/${goodId}.html`

    return request({
        method: 'get',
        url: stockLink,
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
        responseType: 'arraybuffer'
    })
}

拿到商品的价格

async function goodPrice(stockId) {
    const callback = {}
    let name;
    let price;

    callback[name = ('jQuery' + getRandomInt(100000, 999999))] = data => {
        price = data
    }

    const result = await request({
        method: 'get',
        url: 'http://p.3.cn/prices/mgets',
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
        params: {
            type: 1,
            pduid: new Date().getTime(),
            skuIds: 'J_' + stockId,
            callback: name,
        },
    })

    eval('callback.' + result.data)

    return price
}

拿到商品的状态

async function goodStatus(goodId, areaId) {
    const callback = {}
    let name;
    let status

    callback[name = ('jQuery' + getRandomInt(100000, 999999))] = data => {
        status = data[goodId]
    }

    const result = await request({
        method: 'get',
        url: 'http://c0.3.cn/stocks',
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
        params: {
            type: 'getstocks',
            area: areaId,
            skuIds: goodId,
            callback: name,
        },
        responseType: 'arraybuffer'
    })

    const data = iconv.decode(result.data, 'gb2312')
    eval('callback.' + data)

    return status
}

最后 Promise.all 一波带走

async function runGoodSearch() {

    let flag = true

    while (flag) {
        const all = await Promise.all([goodPrice(defaultInfo.goodId), goodStatus(defaultInfo.goodId, defaultInfo.areaId), goodInfo(defaultInfo.goodId)])

        const body = $.load(iconv.decode(all[2].data, 'gb2312'))
        outData.name = body('div.sku-name').text().trim()
        const cartLink = body('a#InitCartUrl').attr('href')
        outData.cartLink = cartLink ? 'http:' + cartLink : '无购买链接'
        outData.price = all[0][0].p
        outData.stockStatus = all[1]['StockStateName']
        outData.time = formatDate(new Date(), 'yyyy-MM-dd hh:mm:ss')

        console.log()
        console.log(`   商品详情------------------------------`)
        console.log(`   时间:${outData.time}`)
        console.log(`   商品名:${outData.name}`)
        console.log(`   价格:${outData.price}`)
        console.log(`   状态:${outData.stockStatus}`)
        console.log(`   商品连接:${outData.link}`)
        console.log(`   购买连接:${outData.cartLink}`)

        const statusCode = all[1]['StockState']
        // 如果有货就下单
        // 33 有货  34 无货
        if (+statusCode === 33) {
            flag = false
        } else {
            await sleep(defaultInfo.time)
        }
    }
}

这里要解析 dom,$ 就是有着 Node 版 jQuery 之称的 cheerio,但是如果直接解析会乱码,先转码,转码神器出场 iconv-lite,剩下的就是 jQuery 操作了,很久没写 jQuery 了,写起来还是这么的顺溜

defaultInfo 中的 goodId 是商品的 id,下面会说到,解析命令行的参数获得的,在哪里能看到呢,来图

image

areaId 是对应着区域的信息,毕竟每个城市的库存都是不一样的

image

京东购物的流程购物车先走一波,然后开始下单付款,有货了我们加入购物车

async function addCart() {
    console.log()
    console.log('   开始加入购物车')

    const result = await request({
        method: 'get',
        url: outData.cartLink,
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
    })

    const body = $.load(result.data)

    const addCartResult = body('h3.ftx-02')

    if (addCartResult) {
        console.log(`   ${addCartResult.text()}`)
    } else {
        console.log('   添加购物车失败')
    }
}

没什么可说的,加入后开始下单

async function buy() {
    const orderInfo = await request({
        method: 'get',
        url: 'http://trade.jd.com/shopping/order/getOrderInfo.action',
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
        params: {
            rid: new Date().getTime(),
        },
        responseType: 'arraybuffer'
    })

    const body = $.load(orderInfo.data)
    const payment = body('span#sumPayPriceId').text().trim()
    const sendAddr = body('span#sendAddr').text().trim()
    const sendMobile = body('span#sendMobile').text().trim()

    console.log()
    console.log(`   订单详情------------------------------`)
    console.log(`   订单总金额:${payment}`)
    console.log(`   ${sendAddr}`)
    console.log(`   ${sendMobile}`)
    console.log()

    console.log('   开始下单')

    const result = await request({
        method: 'post',
        url: 'http://trade.jd.com/shopping/order/submitOrder.action',
        headers: Object.assign(defaultInfo.header, {
            cookie: defaultInfo.cookieData.join('')
        }),
        params: {
            'overseaPurchaseCookies': '',
            'submitOrderParam.btSupport': '1',
            'submitOrderParam.ignorePriceChange': '0',
            'submitOrderParam.sopNotPutInvoice': 'false',
            'submitOrderParam.trackID': defaultInfo.ticket,
            'submitOrderParam.eid': defaultInfo.eid,
            'submitOrderParam.fp': defaultInfo.fp,
        },
    })

    if (result.data.success) {
        console.log(`   下单成功,订单号${result.data.orderId}`)
        console.log('请前往京东商城及时付款,以免订单超时取消')
    } else {
        console.log(`   下单失败,${result.data.message}`)
    }
}

其实这里 post http://trade.jd.com/shopping/order/submitOrder.action 这个就可以了,前面的一个请求是下单页面拿一下订单的信息展示下,这里会有两个注意的点

  1. 商品的数量
    京东下单是把购物车这个商品全部下单,不管数量的,比如你购物车已经有一件这个商品了,那么前面的流程走完后购物车现在有两件这个商品,下单后是下单了两件,当然了这里是可以更改数量的,但是我没写

  2. 订单的参数
    上面下单的请求可以注意到三个陌生的参数 submitOrderParam.trackID submitOrderParam.eid submitOrderParam.fp ,trackID 前面有拿到过,这里直接用就行了,那么 eid 和 fp 是从哪来的呢?答案是登录页面,但是这里有个坑是 request 返回的页面拿到的 dom 元素是不行的,只能通过浏览器来,这也很好办,Node 有 phantomjs,但是这里我用了 Chrome 出品的 puppeteer

puppeteer 使用也很简单,它是基于 Node 的 headless Chrome 工具

puppeteer.launch().then(async browser => {
    console.log('   初始化完成,开始抓取页面')
    const page = await browser.newPage();
    await page.goto('https://passport.jd.com/new/login.aspx');
    await sleep(1000)
    console.log('   页面抓取完成,开始分析页面')
    const inputs = await page.evaluate(res => {
        const result = document.querySelectorAll('input')
        const data = {}

        for (let v of result) {
            switch (v.getAttribute('id')) {
                case 'token':
                    data.token = v.value
                    break
                case 'uuid':
                    data.uuid = v.value
                    break
                case 'eid':
                    data.eid = v.value
                    break
                case 'sessionId':
                    data.fp = v.value
                    break
            }
        }

        return data
    })

    Object.assign(defaultInfo, inputs)
    await browser.close();
    console.log('   页面参数到手,关闭浏览器')

    console.log()
    console.log('   -------------------------------------   ')
    console.log('                请求扫码')
    console.log('   -------------------------------------   ')
    console.log()

})

puppeteer 首先要 launch 后来生成一个 browser 的实例,我们用 browser 来新建一个页面运行我们的网址,并且我们可以在它提供的 evaluate 方法中操作 DOM,上面的代码也是很简单的,一目了然

至此基本上一个自动下单的功能就完成了,再扩展下命令行参数

const args = require('yargs').alias('h', 'help')
    .option('a', {
        alias: 'area',
        demand: true,
        describe: '地区编号',
    })
    .option('g', {
        alias: 'good',
        demand: true,
        describe: '商品编号',
    })
    .option('t', {
        alias: 'time',
        describe: '查询间隔ms',
        default: '10000'
    })
    .option('b', {
        alias: 'buy',
        describe: '是否下单',
        default: true
    })
    .usage('Usage: node index.js -a 地区编号 -g 商品编号')
    .example('node index.js -a 2_2830_51810_0 -g 5008395')
    .argv;

这里我给了两个必需的参数和两个可选的参数,-a 必须要的,地区编号,-g 必要要的,商品编号,-t 商品查询的间隔时间,默认是10s,-b是否自动购买,默认是购买的,这里是 boolean,yargs 还是蛮好用的,也可以用 TJ 大神的 commander,都是一样的

完整的代码可以去下面的项目地址中查看

项目地址 求个 star

@shaodahong shaodahong changed the title 写个京东商品到货查询 写个京东商品自动下单 Oct 31, 2017
@shaodahong shaodahong changed the title 写个京东商品自动下单 趁着双11,写个京东商品自动下单 Oct 31, 2017
@hzbd
Copy link

hzbd commented Nov 1, 2017

手动点赞

@shaodahong
Copy link
Owner Author

@sconts 哈哈,谢谢,昨晚已剁手

@FrontToEnd
Copy link

可以可以

@gongfuxiaocai
Copy link

厉害了,你搞个大件更厉害了

@wanghewanghe
Copy link

const isBuy = Boolean(args.b) || true
这里不就永远是true了吗😂😂

@shaodahong
Copy link
Owner Author

@wanghewanghe 谢谢,已更正

@dengnan123
Copy link

厉害了,虽然没看懂

@ogurigin
Copy link

ogurigin commented Nov 3, 2017

哈哈哈哈有没有tb抢券的,抢的我都憔悴了

@shaodahong
Copy link
Owner Author

@ogurigin 没有,不怎么关注淘宝,基本上东西都在京东买,看重的就是物流和售后哈哈

@LiangRenDev
Copy link

@shaodahong 你好,我在windows上跑了你的脚本完全没有问题,但是在Ubuntu 16.4上运行的时候,login 这一步完全得不到p3p和set-cookie的信息, 不知道你有没有什么解决方案,谢谢。我的Node和NPM版本如下
v9.2.1
5.5.1
image
image

@boolean93
Copy link

这几天在玩 https://c0.3.cn/stocks 这个接口,神奇的发现这个接口获取到的商品状态并不能完全依赖StockState字段。StockState为33的时候,我手动去商品页面查询发现依旧是没货。StockStateName也是写的"现货" (反复确认过area_id和sku_id没填错)

不知道是否有啥其他字段也在限制商品的是否可买状态?

@shaodahong
Copy link
Owner Author

这几天在玩 https://c0.3.cn/stocks 这个接口,神奇的发现这个接口获取到的商品状态并不能完全依赖StockState字段。StockState为33的时候,我手动去商品页面查询发现依旧是没货。StockStateName也是写的"现货" (反复确认过area_id和sku_id没填错)

不知道是否有啥其他字段也在限制商品的是否可买状态?

不清楚,这个项目很老了,我也很久没有更新了,从 issue 来看属于半废状态,可以自己去京东下单抓下接口看看

@boolean93
Copy link

这几天在玩 https://c0.3.cn/stocks 这个接口,神奇的发现这个接口获取到的商品状态并不能完全依赖StockState字段。StockState为33的时候,我手动去商品页面查询发现依旧是没货。StockStateName也是写的"现货" (反复确认过area_id和sku_id没填错)
不知道是否有啥其他字段也在限制商品的是否可买状态?

不清楚,这个项目很老了,我也很久没有更新了,从 issue 来看属于半废状态,可以自己去京东下单抓下接口看看

嗯, 现在最新是 /stock 接口. 能获取单一商品的状态值.
而上面的 /stocks 接口的好处是能够批量获取商品的状态.(参数名叫做skuIds, 用逗号分隔skuId)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants