趁着雙11,寫個京東商品自動下單

項目地址 求個 starhtml

在如今,商家一年不賣貨,雙11賣出一年的貨是你們都知道的事實了,總得來講調一調蚊子腿的價格,聊勝於無,可是也會有些神價格會出現,這時候買到就是賺到vue

原本是想趁着雙11組臺電腦,買個 Z370 的板U套裝,沒想到京東的 8700k 一直是無貨的狀態,這幾天有貨了,價格漲到了3999,簡直不能忍,看了下板U套裝比較划算,可是有些板U套裝是不支持自動下單的,因此 gayhub 搜搜看有沒有爬蟲能夠監聽到貨自動下單的,正好有了這哥們的 jd-autobuy Python 腳本,還有 Go 的,看了下接口已經很齊全了,來個 node 版本的助助興node

趁着雙11,寫個京東商品自動下單

此次用到的 http 庫是 axios,支持客戶端和服務端,總得來講語法仍是很簡潔的,在這以前還有個 superagent 庫,看了下也差很少,只不過 superagent 在 response 上多處理了下ios

由於在 vue 中使用了 axios,此次想試試服務端的能力咋樣,仍是一如既往的好,滋次一波git

先寫個 request header ,畢竟是服務端,沒有瀏覽器幫你處理 User-Agent,因此本身去瀏覽器請求下而後把 header 拿到github

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請求便可json

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 主要是方便咱們來像本地寫入圖片,咱們來處理下 resaxios

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 庫仍是蠻好用的,能夠指定程序打開圖片,文件等瀏覽器

在掃碼以前咱們要監聽掃碼的狀態cookie

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/... 這個就能夠了,前面的一個請求是下單頁面拿一下訂單的信息展現下,這裏會有兩個注意的點

  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

相關文章
相關標籤/搜索