模拟淘宝提交订单可行性分析
在已经获取了登录淘宝成功Cookie的前提下,是否能实现模拟淘宝提交订单的功能呢?近期用HttpWatch搜集了一些商品的购买数据,这里我尝试根据这些数据从理论上来分析模拟淘宝提交订单实现的原理并尝试用C#来实现,虽然成功实现了提取提交订单的数据但最终并成功提交订单,在此抛砖引玉,还请各位有共同爱好的同学多多探讨。
以从商品页面的立刻购买提交订单为例,我们购买商品的流程为:立刻购买–>提交订单–>付款。在整个购买过程中,会出现两次方式为POST的请求,第一次POST请求链接为:https://buy.taobao.com/auction/buy_now.jhtml?spm=2013.1.20140002.d1.nlx60c,对应到浏览器的动作为点击浏览器页面的”立刻购买”;第二次POST请求连接为“https://buy.taobao.com/auction/confirm_order.htm?x-itemid=522915048017&x-uid=599283473&spm=a210c.1.1.1.MEeeFR”,对应到浏览器的动作为点击浏览器页面的“提交订单”。那么这两次POST请求需要向服务器发送哪些数据?这些数据又从何而来呢?
1.“立刻购买”抓包数据分析及C#实现数据提取
该请求的POST数据列表如下所示:
这些数据该从哪里获取呢?我们回溯至以GET方式请求商品链接的回复内容,会发现我们想要的都在这里。
“立刻购买”所需要的POST这些信息可以用如下代码来获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<span>//提取表单内容 public void GetProductFormContent() { if (string.IsNullOrEmpty(productPage)) { auctionLog.Log("警告:传递的商品页源代码字符串为空"); return; } //提取问题并要求回答 Match questionMatch = Regex.Match(productPage, "<div\\s*class\\s*=\\s*\"\\s*disqualified\\s*\".*?<\\s*br.*?>(?<question>.*?)\\s*<\\s*/\\s*div>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (questionMatch.Success) { string question = string.Empty; string questionContent = questionMatch.Groups["question"].ToString(); MatchCollection charMatches = Regex.Matches(questionContent, "&#(?<number>\\d+)\\s*;", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (charMatches.Count > 0) { foreach (Match match in charMatches) { int charNumber = int.Parse(match.Groups["number"].ToString()); question += Convert.ToChar(charNumber); } } if (!string.IsNullOrEmpty(question)) { questionForm.lblQuestion.Text = question; questionForm.ShowDialog(); questionAnswer = questionForm.txtAnswer.Text.Trim(); } } string frmContent, inputContent, tempStr; Match tempMatch; MatchCollection matches = Regex.Matches(productPage, "<form\\s+id\\s*=\\s*\"J_FrmBid\"\\s+.*?>.*?<\\s*/\\s*form\\s*>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (matches.Count > 0) { auctionLog.Log("通知:发现了商品页面中表单"); frmContent = matches[0].ToString(); //提取各个input控件的内容存入dict字典 matches = Regex.Matches(frmContent, "<\\s*input.*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (matches.Count > 0) { auctionLog.Log("通知:在商品页面的表单中发现了input控件"); foreach (Match match in matches) { inputContent = match.ToString(); tempMatch = Regex.Match(inputContent, "name\\s*=\\s*[\"\'](?<name>.*?)[\"\']", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (tempMatch.Success) { tempStr = tempMatch.Groups["name"].ToString(); productInputDict.Add(tempStr, ""); tempMatch = Regex.Match(inputContent, "value\\s*=\\s*[\"\'](?<value>.*?)[\"\']", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); productInputDict[tempStr] = tempMatch.Groups["value"].ToString(); } } } } }</span> |
关于POST数据中的skuid,应该是遵循如下规则:如果商品链接中有属性,例如尺码,颜色等等,就会有skuid的相应信息,若此类商品未写入skuid属性值,那么服务器回复的内容中会包含类似“此商品需要写入skuid信息”。例如请求此类商品链接时,我们得到回复内容中就会包含如下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
"skuMap" : { ";20509:28314;1627207:4966037;" : { "skuId" : "3113023353908", "oversold" : "false", "price" : "115.00", "stock" : "2" }, ";20509:28315;1627207:4966037;" : { "skuId" : "3113417941224", "oversold" : "false", "price" : "115.00", "stock" : "2" }, ";20509:28316;1627207:4966037;" : { "skuId" : "3113417941225", "oversold" : "false", "price" : "115.00", "stock" : "2" } } |
如果商品链接中不需要选择任何属性,那么此商品的“立刻购买”的POST数据中skuid直接为空即可。因为我抓取的数据样本还不够多,上述内容是我的推测。
2.“提交订单”抓包数据分析及C#实现数据提取
“提交订单”需要POST数据包含如下内容。
我搜寻这些内容的第一目标自然是上一次POST请求时服务器回复的内容。请看,“提交订单”POST时需要的内容就在请求“立刻购买”时服务器回复的内容里。
我用如下代码来提取我们需要的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
public string GetOrderPostData(string orderPage,bool modifyQuantity, int quantity, bool modifyTransport, string transport, bool modifyMemo, string memo, bool modifyPromotion, bool modifyShopPromotion, bool modifyAnnoy, bool applyPriceBorder, int priceBorder, out bool lessPriceBorder,CookieContainer cc) { lessPriceBorder = true; if (string.IsNullOrEmpty(orderPage)) { auctionLog.Log("警告:传递的订单页面源代码为空"); return string.Empty; } GetOrderFormContent(orderPage); string orderInformationDomain = GetOrderInformationDomain(orderPage); if (string.IsNullOrEmpty(orderInformationDomain)) { return string.Empty; } string selectedTansport = GetOrderTransports(orderInformationDomain, modifyTransport,transport); GetOrderPromotion(orderInformationDomain); GetOrderShopPromotion(orderInformationDomain); if (inputControls.Count < 1) { auctionLog.Log("警告:未发现订单页面中的input控件"); return string.Empty; } string postData = String.Empty; //string selectedTansport = string.Empty, defaultTransport = string.Empty; if (string.IsNullOrEmpty(promotion.Price)) { auctionLog.Log("警告:未从促销选项中获得价格信息,尝试从整个订单表单数据域中获取"); Match priceMatch = Regex.Match(orderInformationDomain, "price\\s*\"\\s*:\\s*\"(?<price>\\d+)\\s*\"", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); if (priceMatch.Success) { actualFee = Single.Parse(priceMatch.Groups["price"].ToString()); } else { auctionLog.Log("警告:未发现任何价格信息,价格边界条件失效"); } } else { actualFee = Single.Parse(promotion.Price); } /* //获取运输方式信息,避免重复计算运费使用 if (modifyTransport) { selectedTansport = GetSelectedTransport(transport); } else { defaultTransport = GetDefaultTransport(); } */ //生成post数据 foreach (InputControl inputControl in inputControls) { if (inputControl.Control.ToLower() == "input") { if (inputControl.Type.ToLower() == "text") { //是否修改数量 if (inputControl.Name.ToLower().Contains("quantity")) { if (modifyQuantity) { postData += inputControl.Name + "=" + quantity.ToString() + "&"; } else { postData += inputControl.Name + "=" + inputControl.Value + "&"; } continue; } //填写验证码 if (inputControl.Name.ToLower() == "checkcode") { if (checkCode.isCheckCode == "true") { //postData = inputControl.Name + "=" + checkCode.checkCode + "&"; postData += inputControl.Name + "=" + checkCode.checkCode + "&"; } else { postData += inputControl.Name + "=" + inputControl.Value + "&"; } continue; } postData += inputControl.Name + "=" + inputControl.Value + "&"; } if (inputControl.Type.ToLower() == "hidden") { //设置运输方式input(codServiceType),注意,如果以后淘宝升级免运费状态下不用列表选择,注意将该控件value设置为0 if (inputControl.Name.ToLower().Contains("codservicetype") && modifyTransport) { // if (modifyTransport) // { postData += inputControl.Name + "=" + selectedTansport + "&"; /* } else { postData += inputControl.Name + "=" + defaultTransport + "&"; } */ } else { //获取验证码并填充newCheckCode表单字段,注意,这里需要添加验证码识别的部分!!!!!!!!!!!!!!!!!!!! if (inputControl.Name.ToLower() == "newcheckcode" && checkCode.isCheckCode == "true") { auctionLog.Log("通知:可能需要输入验证码"); CookieCollection ccReturned; Bitmap image = new Bitmap(auctionOperations.GetCheckCodeImageStream("http://checkcode.taobao.com/auction/checkcode?sessionID=" + checkCode.encrypterString, cc,out ccReturned)); cc.Add(ccReturned); checkCodeForm.checkCodePic.Image = image; checkCodeForm.ShowDialog(); checkCode.checkCode = checkCodeForm.txtCheckCode.Text.Trim(); if (string.IsNullOrEmpty(checkCode.checkCode)) { auctionLog.Log("警告:并未输入验证码"); return string.Empty; } string newCheckCode = auctionOperations.GetNewCheckCode(checkCode, cc, out ccReturned); cc.Add(ccReturned); if (string.IsNullOrEmpty(newCheckCode)) { auctionLog.Log("警告:未能成功获取newCheckCode数据"); return string.Empty; } postData += inputControl.Name + "=" + newCheckCode + "&"; } else if (inputControl.Name.ToLower().Contains("data"))//data {// Regex reg1 = new Regex(@"\""agencyPay_1\""(.*?)\""biz\""},"); string tmpDataStr = "&data={" + reg1.Match(orderInformationDomain).Groups[0].Value; reg1 = new Regex(@"\""anonymous_\d+\""(.*?)\""biz\""},"); tmpDataStr = tmpDataStr + reg1.Match(orderInformationDomain).Groups[0].Value; reg1 = new Regex(@"\""memo_b_\d+\""(.*?)\""biz\""},"); tmpDataStr = tmpDataStr + reg1.Match(orderInformationDomain).Groups[0].Value; reg1 = new Regex(@"\""submitOrder_1\""(.*?)\""biz\""},"); tmpDataStr = tmpDataStr + reg1.Match(orderInformationDomain).Groups[0].Value; postData += tmpDataStr + "}"; /* Match tempMatch = Regex.Match(orderInformationDomain, "\"data\"\\s*:\\s*(?<data>.*?),\"endpoint\"", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); postData += inputControl.Name + "=" + tempMatch.Groups["data"].ToString() + "&"; */ } else if (inputControl.Name.ToLower().Contains("hierarchy"))//hierarchy { Regex reg1 = new Regex(@"\""structure\""(.*?)},"); string tmpDataStr = "&hierarchy={" + reg1.Match(orderInformationDomain).Groups[0].Value + "}"; postData += tmpDataStr; /* Match tempMatch = Regex.Match(orderInformationDomain, "\"structure\"\\s*:\\s*(?<hierarchy>.*?)}}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); postData += inputControl.Name + "=" + tempMatch.Groups["hierarchy"].ToString() + "&"; */ } else if (inputControl.Name.ToLower().Contains("linkage"))//linkage { Regex reg1 = new Regex(@"\""common\""(.*?)},"); string tmpDataStr = "&linkage{" + reg1.Match(orderInformationDomain).Groups[0].Value + "}"; postData += tmpDataStr; /* Match tempMatch = Regex.Match(orderInformationDomain, "\"common\"\\s*:\\s*(?<linkage>.*?)},", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); postData += inputControl.Name + "=" + tempMatch.Groups["linkage"].ToString() + "&"; */ } else if (inputControl.Name.ToLower().Contains("praper_alipay_cashier_domain"))//praper_alipay_cashier_domain { postData += inputControl.Name + "=cashierzth" + "&"; } else { postData += inputControl.Name + "=" + inputControl.Value + "&"; } } } //修改是否匿名 if (inputControl.Type.ToLower().Trim() == "checkbox" && inputControl.Name.ToLower().Contains("anony") && modifyAnnoy) { postData += inputControl.Name + "=" + inputControl.Value + "&"; } } if (inputControl.Control.ToLower() == "textarea") { //修改促销方式 string promotionString = modifyPromotion ? GetPromotionString(true) : GetPromotionString(false); if (!string.IsNullOrEmpty(promotionString)) { postData += promotionString + "&"; } //修改留言 if (modifyMemo && !string.IsNullOrEmpty(memo)) { postData += inputControl.Name + "=" + memo + "&"; } else { postData += inputControl.Name + "=" + inputControl.Value + "&"; } //修改店铺促销方式 string shopPromotionString = modifyShopPromotion ? GetShopPromotionString(true) : GetShopPromotionString(false); if (!string.IsNullOrEmpty(shopPromotionString)) { postData += shopPromotionString + "&"; } } if (inputControl.Control.ToLower() == "select") { //修改运输方式Select if (inputControl.Name.ToLower().Contains("post")) { // if (modifyTransport) //{ postData += inputControl.Name + "=" + selectedTansport + "&"; /* } else { postData += inputControl.Name + "=" + defaultTransport + "&"; } */ } } } if (postData.EndsWith("&")) { postData = postData.Substring(0, postData.Length - 1); } if (applyPriceBorder && actualFee > 0 && actualFee > priceBorder) { lessPriceBorder = false; } return postData; } |
提取时需要注意,并非每一个input控件都有值,部分内容需要到orderdata中去提取,目前貌似不同商品需要从data中提取出的数据不完全一样,但linkage和hierarchy基本一致。
另外需要特别注意的是data数据中submitOrder_1一项包含ua数据,“立刻购买”回复内容中并不包含此项,目前我暂且认为该UA为登录时的用的UA。
3.用C#实现的结果及其原因分析
理论上而言,拥有登录时的cookie与提交订单时完整的数据包,我们应该可以用C#来成功模拟淘宝的购买动作,但实际上,我按照上述思路提交数据后,得到的服务器的回复是“对不起,无法购买”。从抓包数据粗略来看,目前C#模拟的动作与浏览器不一致的点有:
3.1 浏览器实现购买时,商品链接,立刻购买,提交订单三个动作的SPM参数均不一致。但我目前暂不知后续两个动作的spm参数该如何获取,貌似与https://g.alicdn.com/alilog/mlog/??spm.js,spmact.js?v=141204该js文档相关。目前我尝试后两个动作使用的SPM参数与第一个参数一致,或者后两个动作不带spm参数,最终请求的结果均一致。
3.2 浏览器购买时,立刻购买动作提交的数据中包含”sourcetime”这一项,这个应该是服务器的时间,但我偷懒了一下,在模拟淘宝“立刻购买”时并未带上该参数。实验验证,不带该参数,也可以得到貌似完整的“提交订单”的页面。不过又可能该页面中有一些细节数据是有差异的,只是我没有注意到。
3.3 我并没有去核对浏览器提交订单时的数据与模拟提交订单的数据,不能确保数据完全一致。
我自己认为最大的可能性应该是第1点,各位觉得呢?后续会继续尝试,还请各路同学多多指点。
bbs.msdn5.com
楼主应该会对这个论坛感兴趣
bbs.msdn5.com 里面有成熟的C# 模拟Http方案
Great post, I think blog owners should learn a lot from this webloig its real user
friendly. So much superb info on here :D.
不知道 博主 还有没有在研究 最近也遇到了 问题 也是做到这一步 进行不下去了
“金盆洗手”很久了
我也遇到了这个问题,请问您有没有研究出来
已经很久不研究了,祝你研究顺利
请问现在版本的淘宝模拟下单,博主有思路么
已经很久不研究了。祝你研究顺利
“对不起,无法购买”的问题有一个地方楼主没关注到,可以一起讨论。
能给个建议方式一块研究一下吗
这种方案失败的主要原因应该是提交订单时的UA数据不符导致的,我抓取购买动作完整的记录,发现登录时的UA与提交订单时的UA不一致;当然有可能是SPM数据不一致造成的,UA果然名不虚传啊…
这个UA 能破解吗,我也在研究这个
祝你好运:)