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

電子商務 jQuery 京東 Chrome 毛毛愛科技 毛毛愛科技 2017-11-07

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

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

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

這次用到的 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 size 和 t 可以通過抓包拿到的,這裡注意我 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 = truelet 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 帶上,並加上 host 和 referer 來表明我們從哪裡來要到哪裡去,參數裡面的 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 = `//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: '//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 statuscallback[name = ('jQuery' + getRandomInt(100000, 999999))] = data => {status = data[goodId]} const result = await request({method: 'get',url: '//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 ? '' + 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,下面會說到,解析命令行的參數獲得的,在哪裡能看到呢,來圖

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

areaId 是對應著區域的信息,畢竟每個城市的庫存都是不一樣的

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

京東購物的流程購物車先走一波,然後開始下單付款,有貨了我們加入購物車

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: '//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: '//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 //trade.jd.com/shopping/order/submitOrder.action 這個就可以了,前面的一個請求是下單頁面拿一下訂單的信息展示下,這裡會有兩個注意的點

  1. 商品的數量

    京東下單是把購物車這個商品全部下單,不管數量的,比如你購物車已經有一件這個商品了,那麼前面的流程走完後購物車現在有兩件這個商品,下單後是下單了兩件,當然了這裡是可以更改數量的,但是我沒寫

  2. 訂單的參數

    上面下單的請求可以注意到三個陌生的參數

submitOrderParam.trackID

submitOrderParam.eid

submitOrderParam.fp

  1. ,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.valuebreakcase 'uuid': data.uuid = v.valuebreakcase 'eid': data.eid = v.valuebreakcase 'sessionId': data.fp = v.valuebreak}} 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,都是一樣的

完整的代碼可以去下面的項目地址中查看

項目地址 https://github.com/shaodahong/jd-happy

相關推薦

推薦中...