项目地址 求个 starhtml
在如今,商家一年不卖货,双11卖出一年的货是你们都知道的事实了,总得来讲调一调蚊子腿的价格,聊胜于无,可是也会有些神价格会出现,这时候买到就是赚到vue
原本是想趁着双11组台电脑,买个 Z370 的板U套装,没想到京东的 8700k 一直是无货的状态,这几天有货了,价格涨到了3999,简直不能忍,看了下板U套装比较划算,可是有些板U套装是不支持自动下单的,因此 gayhub 搜搜看有没有爬虫能够监听到货自动下单的,正好有了这哥们的 jd-autobuy Python 脚本,还有 Go 的,看了下接口已经很齐全了,来个 node 版本的助助兴node
此次用到的 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
size
和 t
能够经过抓包拿到的,这里注意我 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 带上,并加上 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 = `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,下面会说到,解析命令行的参数得到的,在哪里能看到呢,来图
areaId 是对应着区域的信息,毕竟每一个城市的库存都是不同的
京东购物的流程购物车先走一波,而后开始下单付款,有货了咱们加入购物车
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/... 这个就能够了,前面的一个请求是下单页面拿一下订单的信息展现下,这里会有两个注意的点
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