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)。