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%3A00
url解码: 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=Q6
url解码: 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 硬卧:0 1*****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=confirmSingleForQueue
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
{"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.html http://sskaje.me/index.php/2012/01/12306bot/
源码:(2013.02.26 DLL已更新,持续更新)
PS: 该文编辑时经过多个周期,其中参数级数据没有连续性,以实际Fiddler数据为准。
如果借助该文,能帮你买到票,当然最好,如果没有,试着用自己掌握的知识,能去学习和解决一些实际生活中的问题,未尝不是更大的收获。
以前工作中有问题也偶尔上园子、msdn找找资料,没什么特别大的感触,感觉对.net(确切的说是asp.net)理解也仅仅是拖拖控件,然后数据绑定完事了。园子里逛久了,看了大量技术文章及分享,如汤姆大叔,老赵,刘未鹏等大神们的博文,才知道.net的博大精深及自己的浅薄,知道自己的无知也算知吧(聊表安慰)?。作为要奔三去了的老菜鸟一枚,惭愧的同时又满怀希望。也感谢网络那一端无私分享的你。
下面部分为吐槽和求助:
作为一名80后、没学历(弱弱的问,高中不算学历吧?)、培训出身(著名的***鸟,被各种鄙视和吐槽,囧)、菜鸟程序员。看着园子里各种大神(园子普遍称大牛)及90后新人们的2012总结,深感愧疚。
网上投了一箩筐的简历,没有收到一个面试邀请,不知道是简历的问题(菜+低调),学历的问题,还是人品?有招聘信息的码农推荐一下。
深圳,.net程序员,3年经验(asp.net)。