转载--12306购票之自动化提交初体验

来源:互联网 发布:阿里xin域名 编辑:程序博客网 时间:2024/05/16 13:05

转载--http://www.cnblogs.com/sanmi/archive/2013/01/14/2851534.html

http://www.360doc.com/content/13/0122/17/453497_261791230.shtml


12306购票之自动化提交初体验

10年想自己建个网站练练手,于是上万网申请域名,为了找个稍微心仪的域名是伤透了脑筋。当时写了个很简单的自动提交表单的查询,是用webbrowser做的,分析表单数据累了个半死,倒也做出来个简单能用的,递归一直查询(a,b...z,az,ab...az...)单线程,并且万网有限制,查询间隔太快会被屏蔽,扫了很久也没扫到多少数据,然后就不了了之。

12年南下深圳,在园子里看到各种对12306的思考及吐槽,打算做个简单的12306买票的小程序,也做过一些尝试,但由于自己太菜,遇到各种问题后停了下来。一晃晃过了世界末日,2013来了,买票的问题推到了眼前,硬着头皮开始编码。

先来看看下面这个对http请求的封装方法,作者小坦克,我这里拿来主义了。

代码


        public static CookieContainer CookieContainers = new CookieContainer();


        public static string FireFoxAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23";
        public static string IE7 = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET4.0C; .NET4.0E)";
        public static string IE = "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11";


        public static string GetResponse(string url, string method, string data)
        {
            try
            {
                HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);


                req.KeepAlive = true;
                req.Method = method.ToUpper();
                req.AllowAutoRedirect = true;
                req.CookieContainer = CookieContainers;
                req.ContentType = "application/x-www-form-urlencoded";
                req.UserAgent = IE7;
                req.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
                req.Timeout = 50000;


                if (method.ToUpper() == "POST" && data != null)
                {
                    ASCIIEncoding encoding = new ASCIIEncoding();
                    byte[] postBytes = encoding.GetBytes(data); ;
                    req.ContentLength = postBytes.Length;
                    Stream st = req.GetRequestStream();
                    st.Write(postBytes, 0, postBytes.Length);
                    st.Close();
                }


                System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) =>
                {
                    return true;
                };


                Encoding myEncoding = Encoding.GetEncoding("UTF-8");


                HttpWebResponse res = (HttpWebResponse)req.GetResponse();
                Stream resst = res.GetResponseStream();
                StreamReader sr = new StreamReader(resst, myEncoding);
                string str = sr.ReadToEnd();


                return str;
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

需要fiddler或者类似的工具来分析http请求,简单介绍fiddler,如图:

选择左边URL后,选择右边上下都为Raw的标签窗口看到的就是这张图了,右上角窗口为http请求(Request),右下角为http相应(Response)。

继续看上图,你在登录页面中点击登录实际是发送上图的第三条请求,该请求为post,它需要form数据,格式为Request区域最后一行数据: 

 

一:登录

url(Post): https://dynamic.12306.cn/otsweb/loginAction.do?method=login // 登录请求data: loginRand=随机数&loginUser.user_name=用户名&nameErrorFocus=&user.password=密码&passwordErrorFocus=&randCode=验证码&randErrorFocus=
url(Get):https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=sjrand // 验证码
url(Post):https://dynamic.12306.cn/otsweb/loginAction.do?method=loginAysnSuggest  // 随机数{"loginRand":"494","randError":"Y"} // 返回值

到这里,登录就完成了,貌似很简单啊!

 

二:查询

url(Get): https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=queryLeftTicket&orderRequest.train_date=2013-01-27&orderRequest.from_station_telecode=GZQ&orderRequest.to_station_telecode=WHN&orderRequest.train_no=&trainPassType=QB&trainClass=QB%23D%23Z%23T%23K%23QT%23&includeStudent=00&seatTypeAndNum=&orderRequest.start_time_str=00%3A00--24%3A00url解码: https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=queryLeftTicket&orderRequest.train_date=2013-01-27&orderRequest.from_station_telecode=GZQ&orderRequest.to_station_telecode=WHN&orderRequest.train_no=&trainPassType=QB&trainClass=QB#D#Z#T#K#QT#&includeStudent=00&seatTypeAndNum=&orderRequest.start_time_str=00:00--24:00 orderRequest.train_date:日期 orderRequest.from_station_telecode/orderRequest.to_station_telecode: 车站代码(url(Get): https://dynamic.12306.cn/otsweb/js/common/station_name.js) orderRequest.train_no:车次 trainPassType/trainClass: 车次类型 includeStudent: 学生票标识 seatTypeAndNum:貌似有牛人得出这里跟下铺有关系?对我来说未知 orderRequest.start_time_str:起止时间

可以在登录状态下直接请求,比在查询页面快并且没有限制,返回的json(去掉html标签)为:

0,T264,广州12:19,兰州16:37,28:18,--,--,--,--,--,无,无,--,无,有,--,预订\n1,K226,广州20:36,兰州07:12,34:36,--,--,--,--,--,无,无,--,1,有,--,预订\n2,T38,广州23:53,兰州06:28,30:35,--,--,--,--,--,无,无,--,无,有,--,预订

依次分别为:

商务座,特等座,一等座,二等座,高级软卧,软卧,硬卧,软座,硬座,无座,其他。--:没有该席别;*:未到开始时间;有:有并且数量充足;数字:有但数量有限:无:已售完

查询也是这样简单,其实这里还返回来很重要的信息,这里我们卖个关子,继续:

下一步干什么呢?预定按钮,这一步比较麻烦,Post提交的信息比较多,很繁琐,需要细心的去反复调试

三:预定

url(Post): https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=submutOrderRequest  // 预定data: station_train_code=T38&train_date=2013-01-27&seattype_num=&from_station_telecode=GZQ&to_station_telecode=LZJ&include_student=00&from_station_telecode_name=%E5%B9%BF%E5%B7%9E&to_station_telecode_name=%E5%85%B0%E5%B7%9E&round_train_date=2013-01-27&round_start_time_str=00%3A00--24%3A00&single_round_type=1&train_pass_type=QB&train_class_arr=QB%23D%23Z%23T%23K%23QT%23&start_time_str=00%3A00--24%3A00&lishi=30%3A35&train_start_time=23%3A53&trainno4=6300000T3803&arrive_time=06%3A28&from_station_name=%E5%B9%BF%E5%B7%9E&to_station_name=%E5%85%B0%E5%B7%9E&from_station_no=01&to_station_no=22&ypInfoDetail=1*****30884*****00001*****00003*****0000&mmStr=3C8A201EB0DAF5F17803BF07AAFC2016A2D44E0C4302D3469551C86A&locationCode=Q6url解码: station_train_code=T38&train_date=2013-01-27&seattype_num=&from_station_telecode=GZQ&to_station_telecode=LZJ&include_student=00&from_station_telecode_name=广州&to_station_telecode_name=兰州&round_train_date=2013-01-27&round_start_time_str=00:00--24:00&single_round_type=1&train_pass_type=QB&train_class_arr=QB#D#Z#T#K#QT#&start_time_str=00:00--24:00&lishi=30:35&train_start_time=23:53&trainno4=6300000T3803&arrive_time=06:28&from_station_name=广州&to_station_name=兰州&from_station_no=01&to_station_no=22&ypInfoDetail=1*****30884*****00001*****00003*****0000&mmStr=3C8A201EB0DAF5F17803BF07AAFC2016A2D44E0C4302D3469551C86A&locationCode=Q6

前面的参数不再赘述(有疑问可回头看看查询的参数及说明),看看这段:

&ypInfoDetail=1*****30884*****00001*****00003*****0000&mmStr=3C8A201EB0DAF5F17803BF07AAFC2016A2D44E0C4302D3469551C86A&locationCode=Q6

坦率的讲,我也不知道它是干嘛的,我只知道它是从哪里来的,这里就是上文卖的关子,其实在点击预定时附加了该信息(查询时获得)

onclick=javascript:getSelected('T38#26:47#23:53#6300000T3803#GZQ#TSJ#02:40#广州#天水#01#20#1*****30884*****00001*****00003*****0000#3C8A201EB0DAF5F17803BF07AAFC2016A2D44E0C4302D3469551C86A#Q6')

预定这里痛苦了很久,这里多说几句,如上图,该请求为post类型请求,返回302,即重定向,来看302之后的请求

url(Get): https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=init // 申请令牌// 返回值<input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="2508bfa47ec2b4d909fb30190cabf71a"><input type="hidden" name="leftTicketStr" id="left_ticket" value="1000003166400000000010000000023000000000" />

就是说302到上面URL之后 上面请求会返回一个TOKEN(令牌,防止重复提交),这两个值在后续提交订单和确认购票时会用到。但是重定向之后的请求我们是拿不到的,我们可以再向它请求一次令牌(302的令牌拿不到,我们再主动找它要一个令牌),记录即可。

这里,预定的模拟就完成了,接下来提交订单。

 

四:提交订单

url(Get): https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=randp  // 提交订单验证码 注意该部分参数与登录不同
url(Post): https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=checkOrderInfo&rand=bdte // 提交订单请求data: // 该部分数据由于涉及身份信息,见下文解码信息url解码: org.apache.struts.taglib.html.TOKEN=ad45f047d7c4222a11c437ebd1f977f7&leftTicketStr=1026353107408145000010263500003046250000&textfield=中文或拼音首字母&checkbox1=1&orderRequest.train_date=2013-01-28&orderRequest.train_no=630000K22609&orderRequest.station_train_code=K226&orderRequest.from_station_telecode=GZQ&orderRequest.to_station_telecode=TSJ&orderRequest.seat_type_code=&orderRequest.ticket_type_order_num=&orderRequest.bed_level_order_num=000000000000000000000000000000&orderRequest.start_time=20:36&orderRequest.end_time=02:22&orderRequest.from_station_name=广州&orderRequest.to_station_name=天水&orderRequest.cancel_flag=1&orderRequest.id_mode=Y&passengerTickets=1,0,1,姓名,1,身份证号码,电话号码,Y&oldPassengers=姓名,1,身份证号码&passenger_1_seat=1&passenger_1_ticket=1&passenger_1_name=姓名&passenger_1_cardtype=1&passenger_1_cardno=身份证号码&passenger_1_mobileno=电话号码&checkbox9=Y&oldPassengers=&checkbox9=Y&oldPassengers=&checkbox9=Y&oldPassengers=&checkbox9=Y&randCode=h94b&orderRequest.reserve_flag=A&tFlag=dc
url(Get): https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=getQueueCount&train_date=2013-01-27&train_no=630000K22609&station=K226&seat=1&from=GZQ&to=LZJ&ticket=1029053183409105000010290500553050750000  // 查询余票{"countT":0,"count":229,"ticket":"1*****31644*****00001*****00013*****0000","op_1":true,"op_2":false} // 返回值

提交订单的请求完成。

我们回来来看 1*****31644*****00001*****00013*****0000 这段,从查询请求开始,反复出现该部分,通过在提交订单环节余票信息分析,该数据就是返回的余票信息,即余票信息在第一次查询时就已经返回,但在第一次查询和提交订单后的查询的数字稍微有所出入,估计为查询时获得数据的缓存时间有关系,当然,提交订单后查询获得的应该更为接近数据库,具体数据如下:

1*****31644*****00001*****00013*****0000  // 无座:164  软卧:0  硬座:1  硬卧:01*****3无座4*****0软卧1*****0硬座3*****0硬卧

上面不部分为较为普通车型返回的余票数据,什么是普通车型:K,T,Z系列(不包括高铁,普通慢车,临客),并且该车型包含软卧,硬卧,硬座,无座四中票种。也可能出现卧铺车(Z系列),或者无卧铺车所以返回的数据应该是1*****31644*****00001酱紫的,高铁未测试,道理亦然。

 

无论在最开始的查询,还是提交订单后查询,都是操作缓存,所以在提交订单后查询数据为0时,也可以无视余票直接强行确认订单,有机会定到票哦。没有经过大量测试,通常会返回当前排队人数大于与票数或者余票不足(这里需要取舍的,推荐还是查询余票>0时提交订单)。

工作基本完成了,临门一脚。

 

五:确认订单

url(Post): https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=confirmSingleForQueuedata: // 该部分数据由于涉及身份信息,见下文解码信息url解码: org.apache.struts.taglib.html.TOKEN=ad45f047d7c4222a11c437ebd1f977f7&leftTicketStr=1026353107408145000010263500003046250000&textfield=中文或拼音首字母&checkbox1=1&orderRequest.train_date=2013-01-28&orderRequest.train_no=630000K22609&orderRequest.station_train_code=K226&orderRequest.from_station_telecode=GZQ&orderRequest.to_station_telecode=TSJ&orderRequest.seat_type_code=&orderRequest.ticket_type_order_num=&orderRequest.bed_level_order_num=000000000000000000000000000000&orderRequest.start_time=20:36&orderRequest.end_time=02:22&orderRequest.from_station_name=广州&orderRequest.to_station_name=天水&orderRequest.cancel_flag=1&orderRequest.id_mode=Y&passengerTickets=1,0,1,姓名,1,身份证号码,电话号码,Y&oldPassengers=姓名,1,身份证号码&passenger_1_seat=1&passenger_1_ticket=1&passenger_1_name=姓名&passenger_1_cardtype=1&passenger_1_cardno=身份证号码&passenger_1_mobileno=电话号码&checkbox9=Y&oldPassengers=&checkbox9=Y&oldPassengers=&checkbox9=Y&oldPassengers=&checkbox9=Y&randCode=h94b&orderRequest.reserve_flag=A{"errMsg":"Y"} // 返回值

又一大堆参数,但回头对照提交订单Data,直接将 &tFlag=dc 截掉即可。

铛铛铛铛...,多想来段美妙的音乐,回家的路通了,遗憾的是,高兴的太早了。

春运(1月26日)之前如果返回Y,那么直接就表示有票了,但在春运之后,坑爹的排队又开始了,所以表示只是排上队了,不代表一定有票,如果在开售的第一个整点,排上队拿到票的几率很大,越往后拿到飘的几率越小。

如果返回的信息包含:非法的购票请求,意味着某一个请求的data部分参数错误。

以上完全根据小坦克博文(感谢)推进,地址:

http://www.cnblogs.com/TankXiao/archive/2012/02/20/2350421.html

下面的地址分析是在完成后才找到的,遗憾没有早看到,走了很多弯路:

http://www.cnblogs.com/waninlezu/archive/2012/01/07/tran_ticket.htmlhttp://sskaje.me/index.php/2012/01/12306bot/

 源码:(2013.02.26 DLL已更新,持续更新)

博客园下载 

百度下载

 

PS: 该文编辑时经过多个周期,其中参数级数据没有连续性,以实际Fiddler数据为准。

如果借助该文,能帮你买到票,当然最好,如果没有,试着用自己掌握的知识,能去学习和解决一些实际生活中的问题,未尝不是更大的收获。

 

以前工作中有问题也偶尔上园子、msdn找找资料,没什么特别大的感触,感觉对.net(确切的说是asp.net)理解也仅仅是拖拖控件,然后数据绑定完事了。园子里逛久了,看了大量技术文章及分享,如汤姆大叔,老赵,刘未鹏等大神们的博文,才知道.net的博大精深及自己的浅薄,知道自己的无知也算知吧(聊表安慰)?。作为要奔三去了的老菜鸟一枚,惭愧的同时又满怀希望。也感谢网络那一端无私分享的你。


原创粉丝点击