资讯专栏INFORMATION COLUMN

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

Labradors / 2485人阅读

摘要:项目地址求个在现在,商家一年不卖货,双卖出一年的货是大家都知道的事实了,总得来说调一调蚊子腿的价格,聊胜于无,但是也会有些神价格会出现,这时候买到就是赚到本来是想趁着双组台电脑,买个的板套装,没想到京东的一直是无货的状态,这几天有货了,价格

项目地址 求个 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,下面会说到,解析命令行的参数获得的,在哪里能看到呢,来图

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89420.html

相关文章

  • #11.11#腾讯云11智惠云集:爆款云服务器首年48元,海量代金券,续费2.5折起

    摘要:腾讯云双活动地址点击进入腾讯云年双十一活动优惠活动参与腾讯云活动,多重优惠享不停一爆品秒杀爆款核云服务器首年元,每日场秒杀,全年冰点价。 腾讯云正式开启2021年双11智惠云集促销活动,多重优惠享不停,包括首购服务器低至0.4折直击底价,爆款1核2G云服务器首年48元;新老用户同享,领今年最大额度代金券;折上再享优惠iPad Pr,额外10%返券等你拿,具体活动内容如下。 ...

    clasnake 评论0 收藏0
  • 架构 - 收藏集 - 掘金

    摘要:浅谈秒杀系统架构设计后端掘金秒杀是电子商务网站常见的一种营销手段。这两个项目白话网站架构演进后端掘金这是白话系列的文章。 浅谈秒杀系统架构设计 - 后端 - 掘金秒杀是电子商务网站常见的一种营销手段。 不要整个系统宕机。 即使系统故障,也不要将错误数据展示出来。 尽量保持公平公正。 实现效果 秒杀开始前,抢购按钮为活动未开始。 秒杀开始时,抢购按钮可以点击下单。 秒杀结束后,按钮按钮变...

    Riddler 评论0 收藏0
  • 京东CTO张晨:用技术迎接第四次零售革命

    摘要:张晨表示,第四次零售革命必然是颠覆性的,因为它改变了零售的基础设施。张晨表示,零售基础设施是第四次零售革命的核心,其社会化更是推动整个零售行业变革的动力。 从用户画像个性化的描摹,到与供应商结合需求进行动态定价,再到无人运输与云服务在基础设施上的升级,围绕零售的整条产业链,京东已经在底层进行了长期的建设和夯实。 showImg(https://segmentfault.com/img/...

    evin2016 评论0 收藏0
  • #11.11#腾讯云:企业高配大带宽云服务器,4核8G内存/10M,三年仅需768元

    摘要:年腾讯云双十一活动力针对企业用户推出专享的高配置大带宽云服务器优惠核年元,核年元,核年元,适合各种企业建站使用。轻量云服务器腾讯云轻量应用服务器优惠,可选上海广州北京成都机房,我们可以自定义配置,其中核年元核年元核年元。 腾讯云2021年双十一活动力度非常不错,双11智惠云集不管是个人还是企业都比较优惠,这里小编来推荐下企业专区下面的轻量应用服务器,Lighthouse具有配置高、带宽...

    mozillazg 评论0 收藏0

发表评论

0条评论

Labradors

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<