本文记录对某网站A的秒杀活动编写秒杀器的经历和技术重点。
故事回顾
某日早上,朋友给我说最近A网站在开展秒杀活动,有IPad、IPhone,让大家一起去秒杀。结果我们四个人一起秒,都没有别人快,没有一个人秒到。然后下午我就开始尝试分析它网站的秒杀流程,并尝试使用自动提交数据的方案来进行秒杀。结果,在晚上的时候,成功做出了第一个版本的秒杀器,然后我们一起秒杀了几个IPad(大家都想要IPad,而对IPhone没兴趣,汗)。
当时就用网银付了帐,等待它发货。接下来我们每个人一个接一个地接到了A网站打来的电话,确定我们是不是作弊了,哈哈,我们当然打死不会承认了~
过了半个来月,该网站又发起了新一轮的秒杀活动,但是由于之前发现有许多人作弊,所以这次全面更改了网站的流程,随机出现各种题目让会员回答,回答成功才能继续秒杀。这样,难度就大了些,一开始以为它们是题库,后来发现原来所有的题目都是自动生成的。元旦也没闲着,花了几天时间,改出了第二个版本的秒杀器,智能解题。经测试,目前没有失败过。
第一版本
以下简明扼要地描述所有的分析流程:
分析网站秒杀流程,得出“入口页面”的地址。但是尝试登录此页面失败,返回活动等待页面,并提示:“活动未开始”。
写了一个简单的控制台程序,在活动开始时立刻运行此程序,快速地打开了20-40个入口页面。此时,发现有一半左右的页面进入成功,到达“提交页面”。提交页面中需要填写一些必要的个人信息,最下面是一个提交按钮。估计这是A网站秒杀的最后一道关。
把提交页面的客户端源代码全部保存下来,尝试进行分析。发现表单中需要填写的是:一些固定信息、一些隐藏域(HiddenField)、图片验证码。
隐藏域中需要提交一些如:当时秒杀活动Id、用户Id等的信息。这些信息只要在网站中多分析一下就能得出。
验证码:这个目前并没有什么好的办法能自动识别验证码,网上虽然有此类程序,不过我懒得去下载了,直接把验证码的图片显示在程序中,人工录入就好了。这样做的原因是,以我的经验,他们的验证码十有八九存储在服务端Session中,也有可能是客户端Cookie中,也就是说,验证码是可以提前获取的,并不一定需要等等活动开始后再获取。所以只要在临近活动开始的前2分钟获取并录入验证码就行了。
这样,所有的数据都准备好了,接下来就是如何让程序自动填写数据并提交到网站上。这是重点,也是难点。如果纯粹使用后台代码模拟提交的话,就需要保证后台代码拥有已经被网站验证通过后的Cookie。之前我做过类似的提交程序,但是准备假Cookie的工作一直没有成功过,也比较麻烦。由于这次时间比较紧,没法再试验这种纯正的方案。所以静下心来想别的方案。后来灵机一动决定使用控制浏览器的方案来试试:在秒杀程序中嵌入一个浏览器,在浏览器中执行登录操作。这样,登录成功后的Cookie,就由浏览器自己来维护,而我要做的就是控制浏览器中页面的运行,让它以我的方式加载页面、填写数据、提交数据。在提交数据时,浏览器也会自动把Cookie一并提交。这样就可以在登录的状态下,把前面准备好的数据直接自动提交给服务器。
最后一个问题,让浏览器先访问A网站的页面,登录并拿到登录成功的凭证后,如何让浏览器运行我的代码来提交数据呢?我试了一下在WPF应用程序中直接使用WPF自带的浏览器控件,并研究它的API。在WebBrowser类的API列表中,我发现以下方法:
public void NavigateToString(string text); public object InvokeScript(string scriptName);
这正是我想要的啊,先构造一个模拟的页面,使用NavigateToString到这个页面上,然后使用InvokeScript方法来调用javascript提交表单到表单上指定的网站的地址就行了!
OK,至此,全部设计完成。由于验证码已经在活动前就准备好了,所以整个过程基本上是完全自动化的,速度当然比人快多了,IPad自然也就手到擒来!
第二版本
上面已经说过,网站改版后的秒杀活动,已经使用随机出现的题目来防止作弊。所以这次我的主要任务就是如何自动答题!其次,分析网站的提交页面中的表单,发现有很多的隐藏域是一连串随机的数字,没有任何规律,估计这些数据是每次活动都不一样的,所以再使用第一版中静态的模拟页面提交数据的方法不行了,必须使用动态的页面,把这些随机的数据都保留下来,并一起提交,或者直接在A网站发回来的页面中填写数据再提交。
首先,第一直觉就是获取大量的提交页面,提取出获取到的所有题目,存储在题目库中。答题时,直接在题库中进行匹配,如果找到相同的题目,则直接使用题目库中的答案进行回答。
当时,简单地画了一个流程的设计:
后来在该次活动的最后一轮秒杀时,程序开发完成,并开始使用。结果,发现没有一题匹配成功,都找不到答案,全部都显示到了右边的窗口中人为回答,结果我还答错了!!!活动结束!!!
很气人啊,这样的方案根本不行。后来分析了半天,发现原来所有的题目都是程序自动生成的,只是模式固定而已。所以改了设计方案,遵循设计模式写了一些类来自动回答题目,类结构如下:
这里,只贴一个子类的代码,展示一下解答的模式。这个子类的逻辑也是所有题目中最复杂的一个:
class TallerAlgorithm : SelectionQuestionAlgorithm { internal override void TryComplete(SelectionQuestion question) { var match = Regex.Match(question.Content, @"小(?<a>.)比小(?<b>.)(?<abUnit>.),小(?<c>.)比小(?<d>.)(?<cdUnit>.),请问他们当中谁最(?<finalUnit>.)?"); if (match.Success) { var a = match.Groups["a"].Value; var b = match.Groups["b"].Value; var c = match.Groups["c"].Value; var d = match.Groups["d"].Value; var abUnit = match.Groups["abUnit"].Value; var cdUnit = match.Groups["cdUnit"].Value; var finalUnit = match.Groups["finalUnit"].Value; var winner1 = abUnit == finalUnit ? a : b; var loster1 = abUnit == finalUnit ? b : a; var winner2 = cdUnit == finalUnit ? c : d; var loster2 = abUnit == finalUnit ? d : c; string champion = null; if (winner1 == winner2) champion = winner1; else if (winner1 == loster2) champion = winner2; else if (winner2 == loster1) champion = winner1; if (!string.IsNullOrWhiteSpace(champion)) { question.RightAnswer = question.Answers .SingleOrDefault(an => an.Text.Contains(champion)); } } } }
其次,是动态的页面的整理:把里面的题目都提取出来,自动答题之后,填写答案,插入一些提交的Javascript代码。然后继续使用NavigateToString、InvokeScript来提交数据。过程中,有两点心得:
1. 在一开始控制浏览器导向提交页面后,发现无法获取Html源代码,花了些时间研究,没搞出来。查了半天网页,最后使用WinForm中的WebBrowser来解决了这个问题。WinForm中WebBrowser不象WPF中的WebBrowser,它拥有着强大的API,DocumentText属性就取到了源代码。
2. 这次我使用了LinqToXml来维护Html Dom中的所有内容,发现XLinq的API实在是太方便了,查找某个元素,更改某个属性。如果没有XLinq,相同的功能,我可能需要3-5倍的时间来完成。
总结
这次秒杀器编写的过程,让我的一个心结给解了。一直以来,就想完全控制网页客户端程序的运行:大四在电信的时候,老总让我给领导刷票;再后来有要人给我给论坛自动提交数据。这些需求都需要持有客户端的用户凭证,然后用这个身份给服务端自动发送一些请求。一直使用纯后台代码的方式提交,没有成功过。这次,使用控制浏览器的方案,使得真正做到了一直想做到的:“完全控制客户端”。