不知道是饥饿营销仍是真的供不该求,小米的火热真的是没法阻挡。众多产品一一亮相,着实吸引眼球,可是一机难求的局面没有改善,让众多米粉败兴而归。咱们来实现一个简单的小米抢购软件,让抢购之路多上那么一点点但愿。html
首先要说明的是小米抢购过程当中的不少页面和请求地址都是在开放抢购当天时间点到了以后才开放,抢购结束会关闭,因此你在按照博客的内容本身实现的过程当中有请求地址不能访问的,请在抢购开始以后测试,楼主解决不了这个问题。我是在第一次抢购的时候记录请求了哪些地址,作好简单的逻辑以后第二次抢购的时候验证。软件是两个月前完成,当时是可用的。小米抢购的逻辑常常改变,最近这几回没抢,不肯定中间请求的地址是否失效。web
下面开始分析实现的过程吧。json
第一步,模拟登录。在小米首页点击登录以后能够看到登录页面,要求输入邮箱/ID、密码,小米的登录没有验证码,相对简单了不少。点击登录按钮能够看到请求的地址是 https://account.xiaomi.com/pass/serviceLoginAuth2,请设置好对应的Request Header,请求参数见下图:cookie
user 和 pwd 是你的用户名和密码,其余的保持不变应该就能够,返回的结果里 desc 是“成功”说明登录成功,这个时候能拿到一个返回的cookie,这个cookie不是咱们最后须要的cookie,还须要一步请求以后拿到的cookie才能用,从登录页面请求的抓包结果来看确实还有一步。从上一步的返回的结果里得到notificationUrl的值,形如:/auth/service?userId=xxxxxxxx&_sign=xxxxxxxx&nonce=xxxxxxxx,完整地址为 https://account.xiaomi.com/auth/service?userId=xxxxxxxx&_sign=xxxxxxxx&nonce=xxxxxxxx,带着上一步拿到的cookie访问这个页面,拿到返回的cookie保存起来,这个cookie就是可用的,模拟登录到这里就完成了。ide
第二步,得到手机版本选择页面的地址。请求地址形如:http://tp.hd.mi.com/getpath/cn?m=1&jsonpcallback=getpath&_=1416887144988,最后一个参数为当前时间的时间戳,这个地址只有到开放购买的时候才有效,平时访问是不通的。从返回的结果中拿到path对应的值,形如:http://s1.mi.com/open/3612A6B4D011142E4D533DE85CCBA5D1/choosePhone.html?_20141125,这个页面是咱们在抢购的时候看到的手机版本选择的页面,获取页面内容,里面有不少有用的信息。测试
先看一下这个页面里的部分js内容。jsonp
1 init: function() { 2 this.inTheQueue = !1, 3 this.phoneSku = "", 4 this.phoneType = "", 5 this.hdinfoData = null, 6 this.startTime = new Date("2014/12/9 12:00:00").getTime() / 1e3, 7 this.nextDate = "12月16日", 8 this.showMod = !0, 9 this.modType = null, 10 this.fkNum = 0, 11 this.isReg = "true", 12 this.hdget_date_tmp = "{{M}}md{{Y}}y47d15s", 13 this.cookies = { 14 isStart: "XM_Hd_Start", 15 buySucc: "XM_Buy_Succ", 16 userId: "userId", 17 login: "xm_order_btauth" 18 }, 19 this.home = "http://s1.mi.com/open/index.html", 20 this.hdgetUrl = "http://tp.hd.mi.com/hdget/cn?product={{SKU}}&addcart=1&m=1&fk={{FK}}&uagent={{TODAY}}", 21 this.hdinfoUrl = "http://tp.hd.mi.com/hdinfo/cn", 22 this.timestampUrl = "http://tp.hd.mi.com/gettimestamp", 23 this.getmodeUrl = "http://tp.hd.mi.com/getmode/cn/?product=", 24 this.nextBookUrl = "http://a.hd.mi.com/productv2/book/a/18#MIPHONE", 25 this.ordeSite = "http://order.mi.com", 26 this.shopCartUrl = this.ordeSite + "/cart/add/{{SKU}}?source=bigtap&token={{TOKEN}}", 27 this.addCartNext = this.ordeSite + "/event/success?goodsid={{SKU}}", 28 this.loginUrl = "http://s1.mi.com/zt/xm_account/limitfacade.html?third=http%253A%252F%252Forder.mi.com%252Flogin%252Fcallback%253Ffollowup%253Dhttp%25253A%25252F%25252Fs1.mi.com%25252Fopen%25252Findex.html%2526sign%253DNjEzYmU3ZTJkOWRlY2FiZDQ5NDEwNzEyZjNiMjg0NDA0MGYxYWY3Mg%252C%252C%26sid%3Dmi_eshop&sid=mi_eshop&callback=http%253A%252F%252Forder.mi.com%252Flogin%252Fcallback%253Ffollowup%253Dhttp%25253A%25252F%25252Fs1.mi.com%25252Fopen%25252Findex.html%2526sign%253DNjEzYmU3ZTJkOWRlY2FiZDQ5NDEwNzEyZjNiMjg0NDA0MGYxYWY3Mg%252C%252C&sign=dK3nqW%252FKhFM3Tl7Jyt9%252FGt3jOI8%253D", 29 this.noPresaleGoods = [], 30 this.noBookGoods = [], 31 this.isHm = ["2143300001", "2143400005", "2143200006", "2141600007", "2140700031"]; 32 var a = this; 33 return xmCookie(a.cookies.userId) && xmCookie(a.cookies.login) || (location.href = a.loginUrl), 34 xmCookie(a.cookies.isStart) ? a.getHdInfo() : a.checkTime(), 35 $("[data-close-target]").on("click", 36 function() { 37 var b = $(this).attr("data-close-target"); 38 return a.hideBox(b), 39 !1 40 }), 41 $("#submitBtn").on("click", 42 function() { 43 return ! a.phoneSku || $(this).hasClass("btn-disabled") ? (alert("请选择您要购买的手机"), !1) : $.inArray(a.phoneSku, a.isHm) >= 0 ? void(location.href = "http://order.mi.com/event/selectPacket/goodsid/" + a.phoneSku) : "true" !== a.isReg ? (a.showBox("Tip", 44 function() { 45 a.getTipMsg("reg") 46 }), !1) : void(a.showMod ? (a.getmode(), a.showBox("Fk")) : (a.startQueue(), a.getDmSys())) 47 }), 48 $("#boxCacheBtn").on("click", 49 function() { 50 a.hideBox("all"), 51 $("#submitBtn").trigger("click") 52 }), 53 "undefined" != typeof HDOVER && HDOVER === !0 ? (location.href = a.home, !1) : ($(".J_nextDate").html(a.nextDate), $(".J_bookBtn").attr("href", a.nextBookUrl), $(".J_fkLoading").on("click", ".J_reloadFk", 54 function() { 55 $(this).parent().html('<img src="http://img03.mifile.cn/webfile/images/2014/cn/loading.gif">'), 56 a.getmode() 57 }), void $("#fkNum").on("keyup", 58 function() { 59 $(this).val().length; 60 $("#boxFkBtn").removeClass("btn-disabled").off().on("click", 61 function() { 62 a.checkFk() 63 }) 64 })) 65 }
仔细分析一下这段js,发现抢购后续的请求地址和参数格式在这里都能找到,hdgetUrl,hdinfoUrl,getmodeUrl,shopCartUrl 这几个后面都会用到,抓包分析后面几步的过程能够和这里的地址验证,先保存下来。下面要作的工做是计算js中{{TODAY}}对应的值,将hdgetUrl中的值替换,计算逻辑可用下面的,this
Pattern dateTmpPattern=Pattern.compile("hdget_date_tmp=\"(.*?)\""); Matcher dateTmpMatcher=dateTmpPattern.matcher(result); if(dateTmpMatcher.find()) { dateTmp=dateTmpMatcher.group(1); dateTmp=dateTmp.replace("{{Y}}",YEAR+"").replace("{{M}}",MONTH).replace("{{D}}",DAY); hdGetUrl=hdGetUrl.replace("{{TODAY}}",dateTmp); }
YEAR、MONTH、DAY 为当前日期。url
第三步,得到产品状态信息。这里用到了 hdinfoUrl,请求地址为http://tp.hd.mi.com/hdinfo/cn?jsonpcallback=hdinfo&_=1416888070538(下面不说明的时候比较长的数字应该就是指当前的时间戳)。请求返回的数据格式以下:spa
{"stime":1416979023,"pmstart":false,"status":{"2141700014":{"hdstart":false,"hdstop":true,"reg":true},"2143000004":{"hdstart":false,"hdstop":true,"reg":true},"2143400001":{"hdstart":false,"hdstop":true,"reg":true},"2143400004":{"hdstart":false,"hdstop":true,"reg":true},"2143600001":{"hdstart":false,"hdstop":true,"reg":true},"2144000012":{"hdstart":false,"hdstop":true,"reg":true},"2144100013":{"hdstart":false,"hdstop":true,"reg":true},"2144100014":{"hdstart":false,"hdstop":true,"reg":true}},"dbe5a2":false}
包含了每种产品的可售状态,能够根据这个信息判断是否能购买,在这里能够过滤掉不能买的信息。
产品代号和名称的对应关系我作了一个对应,这个信息比较老了,请根据每次购买的状况更新:
2143000004----小米4联通3G版 亮白16GB 1999
2143400001----小米4联通3G版 亮白64GB 2499
2143100007----小米4联通4G版 亮白16GB 1999
2143400004----小米4电信3G版 亮白16GB 1999
2144000012----小米4移动4G版 雅黑16GB 1999
2144100014----小米4移动4G版 雅黑64GB 2499
2144800007----小米4移动4G版 黑色(金属原色框)16GB 1999
2143600001----小米4移动4G版 亮白16GB 1999
2144100013----小米4移动4G版 亮白64GB 2499
2143200006----红米Note 移动4G加强版 899
2143400005----红米Note 联通4G加强版 899
2141600007----红米1S联通3G版 金属灰 799
2140700031----红米1S电信3G版 金属灰 799
2143300001----红米1S移动4G版 金属灰 599
2141700014----小米路由器 mini 129
2143000001----小米手环 79
要购买哪一种产品只须要产品代号信息(应该是sku)就够了。
第四步,获取验证码或验证问题。用到了getmodeUrl地址,请求路径为 http://tp.hd.mi.com/getmode/cn/?product=xxxxxxxxxx&jsonpcallback=getmode&_=1416888070539,将product值换成你要去买的产品的sku值。根据返回response头的Content-Type值判断是验证码图案仍是验证问题,image/jpeg 类型的返回信息应该是验证码,text/html 应该是验证问题。根据不一样类型,输入答案后进入第五步。
第五步,验证答案。地址和第四步中是相似的,多了一个答案参数,http://tp.hd.mi.com/getmode/cn/?product=2144000012&vecode=89&jsonpcallback=getmode&_=1416888070540,返回结果中code值是“1”说明验证成功,能够进行下一步,不然从新输入验证答案。
第六步,最后得到产品信息,得到下一步TOKEN对应的hdurl。这一步用到了hdgetUrl,http://tp.hd.mi.com/hdget/cn?product={{SKU}}&addcart=1&m=1&fk={{FK}}&uagent={{TODAY}},SKU为要购买产品的sku,FK为第四步出现验证码或问题的答案,TODAY已在前面替换成一个和日期有关动态的参数,拿到请求这个地址的返回结果,
hdcontrol({"d22a51":10,"login":true,"pmstart":false,"status":{"2144800007":{"hdstart":false,"hdstop":true,"hdurl":""}}})
其中hdurl有值的时候说明这一步是成功的,拿到这个hdurl进入第七步,若是你获得的结果和我同样没有hdurl的值,那么很差意思,你无法加入购物车,后面会提示出错的,这一步能够多试几回,看看能不能运气好就能买了。
第七步,加入到购物车。用到了shopCartUrl ,地址 http://order.mi.com/cart/add/2144000012?source=bigtap&token={{TOKEN}}&jsonpcallback=getdata,将这里的 TOKEN 替换成第六步得到的 hdurl , 根据这个请求的返回结果判断是否添加成功,
下面是返回数据的格式,
{"code":-1,"message":"\u6dfb\u52a0\u8d2d\u7269\u8f66\u9700\u8981\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\uff01","msg":"\u6dfb\u52a0\u8d2d\u7269\u8f66\u9700\u8981\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\uff01"}
能够根据code的值判断是否成功,后面的为提示信息。
抢购过程当中的关键点都分析完了,再次强调一下,抢购的逻辑常常改变,不保证这个过程还适应如今的逻辑,须要本身在开放购买的时候实测。
上一张截图