12306分析

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


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

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

οnclick=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=confirmSingleForQueueOrder
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/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值