本系列文章
- 12306订票客户端 FOR .NET 演示项目 【7】登录9年前 (2015-08-18)
- 12306订票客户端 FOR .NET 演示项目 【6】验证码输入9年前 (2015-08-12)
- 12306订票客户端 FOR .NET 演示项目 【5】获得余票数据9年前 (2015-06-10)
- 12306订票客户端 FOR .NET 演示项目 【4】界面框架&基础数据初始化9年前 (2015-06-08)
- 12306订票客户端 FOR .NET 演示项目 【3】流程分析和项目规划9年前 (2015-05-28)
- 12306订票客户端 FOR .NET 演示项目 【2】准备工具9年前 (2015-05-22)
- 12306订票客户端 FOR .NET 演示项目 【1】项目概况9年前 (2015-05-19)
5.1 查票流程
5.1.1 流程分析
其实查票是一个相对比较简单的流程。掏出Fiddler回去看抓包的结果。简单的浏览请求后,只要不是眼瞎应该都可以看到请求。。。
(这里为了方便,用的是Fiddler抓包。其实用浏览器的开发者工具抓包效果是一样的,某些方面可能比Fiddler还方便,比如看数据预览)
右侧很明显可以看到是我们有兴趣的数据,然后回过头看看网址。
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2015-07-20&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=HFH&purpose_codes=ADULT
前面粗体字部分很明显是网址,而后面的查询参数中,日期很容易识别,而from_station
和to_station
从字面意思上看是站和到站,那BJP和HFH是啥?结合之前我们看到的车站数据,能看得出来其实这是车站的电报码。
至于最后的ADULT,则是出行方式,如果选择的是学生票,那么这个标记会变成0X00。如何知道这个的呢?可以通过好几种方式知道。比如通过尝试切换查询条件(选择学生票),就会看到结果变成了0X00。
或者你也可以通过读取源码知道是怎么回事。在开发者工具的Source标签里,按Ctrl+Shift+F打开搜索,输入ADULT后回车,能找到出现的地方,点击找到的行后在Source中跳到对应的位置。
在搜索结果中,ADULT不是明显作为变量返回的地方我们直接忽略,点击搜索结果后可以跳到原来的位置,但是我们会发现JS经过压缩了很难查看,这时可以通过点击“{}”来进行格式化。格式化后,可以重新搜索一下,这时会搜索出更加准确的位置。
从代码的意思来看,应该是判断ID为sf2的对象是否被选中,选中了则返回0X00,否则返回ADULT。那么这个sf2到底是个啥嘞?我们可以选中 $("#sf2")
,右击并选择“Evaluate in console”(在控制台中求值),即可看到对应的DOM元素。
鼠标移动到输出的元素上,会看到“学生”的单选框被高亮了。原来就是学生票的意思。
回过头来看查票请求,我们会看到还有一个很奇怪的log请求,之所以奇怪是因为它的参数看起来和查票一样,但是返回的信息中并没有什么有用的信息,并且从数量看来和查票请求是完全一致的。
一般的做法,就是既然他们要这个请求,那就一起发好了。但是如果深入源码去找这个请求的发源地,那么就能看到一些额外的逻辑。
从上图可以看到,这个请求是不是发送,是根据 isSaveQueryLog
变量进行控制的,如果它为 Y,则发送,否则不发。而这个请求很奇怪的是一个撒手不管的请求:发就发了,成功出错不管,获得的响应也不处理。还是个奇奇怪怪的异步请求,超时时间居然为可怕的1000毫秒(1秒)。这个请求到底有啥意义?不知道,问12306好了~
那么这个 isSaveQueryLog
是哪里来的?再搜索一下,会发现原来是查票页面定义的。
这时候回来继续看查票的请求,我们可以追查一下查票的网址从哪里来的。搜索之后你会发现,这个请求地址其实也是查票页面定义的。
所以总的来说,就是查票前其实我们需要请求一次查票页面(leftTicket/init),以拿到正确的查票网址和是否需要发送log请求。
备注:从经验看来,查票网址是会动态变化的,但是一次登录会话期间是否会变化暂时没有注意观察。这里留个问号,其实关于这个动态地址的切换,在JS中也有体现,有兴趣的同学可以先自己找找看。
5.1.2 查票结果数据分析
码字好累……
从上面的查票请求中我们可以拿回来获得的车次数据,如下图所示。
{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":[{"queryLeftNewDTO":{"train_no":"240000G26109″,"station_train_code":"G261″,"start_station_telecode":"VNP","start_station_name":"北京南","end_station_telecode":"UAH","end_station_name":"六安","from_station_telecode":"VNP","from_station_name":"北京南","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"07:15″,"arrive_time":"11:42″,"day_difference":"0″,"train_class_name":"","lishi":"04:27″,"canWebBuy":"Y","lishiValue":"267″,"yp_info":"O042750088M0720500009135050000″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"O3M393″,"yp_ex":"O0M090″,"train_seat_feature":"3″,"seat_types":"OM9″,"location_code":"P2″,"from_station_no":"01″,"to_station_no":"09″,"control_day":59,"sale_time":"1230″,"is_support_card":"1″,"gg_num":"–","gr_num":"–","qt_num":"–","rw_num":"–","rz_num":"–","tz_num":"–","wz_num":"–","yb_num":"–","yw_num":"–","yz_num":"–","ze_num":"有","zy_num":"无","swz_num":"无"},"secretStr":"MjAxNS0wNy0yMCMwWDAwI0cyNjEjMDQ6MjcjMDc6MTUjMjQwMDAwRzI2MTA5I1ZOUCNIRkgjMTE6NDIj5YyX5Lqs5Y2XI%2BWQiOiCpSMwMSMwOSNPMDQyNzUwMDg4TTA3MjA1MDAwMDkxMzUwNTAwMDAjUDIjMTQzMzgzNTA5MjA2NSMxNDMyMjY5MDAwMDAwIzY4Q0JFRjE3RTFBNDlGM0E0OTU5QzlERTZDRDdCMzI0MEQ1REFDNDVDMDVCMzM4RDFCOUM1MjlE","buttonTextInfo":"预订"},{"queryLeftNewDTO":{"train_no":"24000K10710H","station_train_code":"K1071″,"start_station_telecode":"BXP","start_station_name":"北京西","end_station_telecode":"AQH","end_station_name":"安庆","from_station_telecode":"BXP","from_station_name":"北京西","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"11:53″,"arrive_time":"02:49″,"day_difference":"1″,"train_class_name":"","lishi":"14:56″,"canWebBuy":"Y","lishiValue":"896″,"yp_info":"1013853000403725001230238500001013850098″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"W3431333″,"yp_ex":"10403010″,"train_seat_feature":"3″,"seat_types":"1431″,"location_code":"P2″,"from_station_no":"01″,"to_station_no":"15″,"control_day":59,"sale_time":"0800″,"is_support_card":"0″,"gg_num":"–","gr_num":"–","qt_num":"–","rw_num":"12″,"rz_num":"–","tz_num":"–","wz_num":"无","yb_num":"–","yw_num":"无","yz_num":"有","ze_num":"–","zy_num":"–","swz_num":"–"},"secretStr":"MjAxNS0wNy0yMCMwWDAwI0sxMDcxIzE0OjU2IzExOjUzIzI0MDAwSzEwNzEwSCNCWFAjSEZIIzAyOjQ5I%2BWMl%2BS6rOilvyPlkIjogqUjMDEjMTUjMTAxMzg1MzAwMDQwMzcyNTAwMTIzMDIzODUwMDAwMTAxMzg1MDA5OCNQMiMxNDMzODM1MDkyMDY1IzE0MzIyNTI4MDAwMDAjMjdEOTZCNTIyQTdDRTExMzgwRDE2MUQ1Q0FENDE2OTM3NDc4OTc1RDM2OEUzRjVEQzg2QUNBQUI%3D","buttonTextInfo":"预订"},{"queryLeftNewDTO":{"train_no":"24000K11090D","station_train_code":"K1109″,"start_station_telecode":"BXP","start_station_name":"北京西","end_station_telecode":"HKH","end_station_name":"黄山","from_station_telecode":"BXP","from_station_name":"北京西","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"13:26″,"arrive_time":"02:42″,"day_difference":"1″,"train_class_name":"","lishi":"13:16″,"canWebBuy":"Y","lishiValue":"796″,"yp_info":"1013853000403725001810138500893023850000″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"W3431333″,"yp_ex":"10401030″,"train_seat_feature":"3″,"seat_types":"1413″,"location_code":"P4″,"from_station_no":"01″,"to_station_no":"15″,"control_day":59,"sale_time":"0800″,"is_support_card":"0″,"gg_num":"–","gr_num":"–","qt_num":"–","rw_num":"18″,"rz_num":"–","tz_num":"–","wz_num":"无","yb_num":"–","yw_num":"无","yz_num":"有","ze_num":"–","zy_num":"–","swz_num":"–"},"secretStr":"MjAxNS0wNy0yMCMwWDAwI0sxMTA5IzEzOjE2IzEzOjI2IzI0MDAwSzExMDkwRCNCWFAjSEZIIzAyOjQyI%2BWMl%2BS6rOilvyPlkIjogqUjMDEjMTUjMTAxMzg1MzAwMDQwMzcyNTAwMTgxMDEzODUwMDg5MzAyMzg1MDAwMCNQNCMxNDMzODM1MDkyMDY1IzE0MzIyNTI4MDAwMDAjMjAxNkQ4QzY3OUI0Q0EwMUFEN0U3MkZERUJGMTJFNkEyMUJBRUIyM0VDMDA4RjExNjE1NzdFMTY%3D","buttonTextInfo":"预订"},{"queryLeftNewDTO":{"train_no":"240000G2710F","station_train_code":"G271″,"start_station_telecode":"VNP","start_station_name":"北京南","end_station_telecode":"HFH","end_station_name":"合肥","from_station_telecode":"VNP","from_station_name":"北京南","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"18:39″,"arrive_time":"23:03″,"day_difference":"0″,"train_class_name":"","lishi":"04:24″,"canWebBuy":"Y","lishiValue":"264″,"yp_info":"O042750501M0720500009135050005″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"O3M393″,"yp_ex":"O0M090″,"train_seat_feature":"3″,"seat_types":"OM9″,"location_code":"P2″,"from_station_no":"01″,"to_station_no":"09″,"control_day":59,"sale_time":"1230″,"is_support_card":"1″,"gg_num":"–","gr_num":"–","qt_num":"–","rw_num":"–","rz_num":"–","tz_num":"–","wz_num":"–","yb_num":"–","yw_num":"–","yz_num":"–","ze_num":"有","zy_num":"无","swz_num":"5″},"secretStr":"MjAxNS0wNy0yMCMwWDAwI0cyNzEjMDQ6MjQjMTg6MzkjMjQwMDAwRzI3MTBGI1ZOUCNIRkgjMjM6MDMj5YyX5Lqs5Y2XI%2BWQiOiCpSMwMSMwOSNPMDQyNzUwNTAxTTA3MjA1MDAwMDkxMzUwNTAwMDUjUDIjMTQzMzgzNTA5MjA2NSMxNDMyMjY5MDAwMDAwI0JDMzVFOEM3QkUxNUNFNzlGRTFGRTNGMTlBQkU2M0JGMzkzRjAzMDFFNTA5NDEwN0QzRUE5RTc3″,"buttonTextInfo":"预订"},{"queryLeftNewDTO":{"train_no":"2400000T630O","station_train_code":"T63″,"start_station_telecode":"BJP","start_station_name":"北京","end_station_telecode":"HFH","end_station_name":"合肥","from_station_telecode":"BJP","from_station_name":"北京","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"20:03″,"arrive_time":"07:38″,"day_difference":"1″,"train_class_name":"","lishi":"11:35″,"canWebBuy":"Y","lishiValue":"695″,"yp_info":"10141531594038150018607005001010141501453024450185″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"W343631333″,"yp_ex":"1040601030″,"train_seat_feature":"3″,"seat_types":"14613″,"location_code":"P3″,"from_station_no":"01″,"to_station_no":"06″,"control_day":59,"sale_time":"1000″,"is_support_card":"0″,"gg_num":"–","gr_num":"10″,"qt_num":"–","rw_num":"18″,"rz_num":"–","tz_num":"–","wz_num":"有","yb_num":"–","yw_num":"有","yz_num":"有","ze_num":"–","zy_num":"–","swz_num":"–"},"secretStr":"MjAxNS0wNy0yMCMwWDAwI1Q2MyMxMTozNSMyMDowMyMyNDAwMDAwVDYzME8jQkpQI0hGSCMwNzozOCPljJfkuqwj5ZCI6IKlIzAxIzA2IzEwMTQxNTMxNTk0MDM4MTUwMDE4NjA3MDA1MDAxMDEwMTQxNTAxNDUzMDI0NDUwMTg1I1AzIzE0MzM4MzUwOTIwNjUjMTQzMjI2MDAwMDAwMCNCRTQ5RkU0NTNFREE0NjFEMzUxMTIwREQ3Q0VCNTY0QUM3MDlBQ0U0M0JBQTM4RTI5RUJEMEQ0OA%3D%3D","buttonTextInfo":"预订"},{"queryLeftNewDTO":{"train_no":"240000Z22500″,"station_train_code":"Z225″,"start_station_telecode":"BJP","start_station_name":"北京","end_station_telecode":"HFH","end_station_name":"合肥","from_station_telecode":"BJP","from_station_name":"北京","to_station_telecode":"HFH","to_station_name":"合肥","start_time":"21:50″,"arrive_time":"07:30″,"day_difference":"1″,"train_class_name":"","lishi":"09:40″,"canWebBuy":"Y","lishiValue":"580″,"yp_info":"607005001440381502563024450312″,"control_train_day":"20301231″,"start_train_date":"20150720″,"seat_feature":"634333″,"yp_ex":"604030″,"train_seat_feature":"3″,"seat_types":"643″,"location_code":"P3″,"from_station_no":"01″,"to_station_no":"05″,"control_day":59,"sale_time":"1000″,"is_support_card":"0″,"gg_num":"–","gr_num":"14″,"qt_num":"–","rw_num":"有","rz_num":"–","tz_num":"–","wz_num":"–","yb_num":"–","yw_num":"有","yz_num":"–","ze_num":"–","zy_num":"–","swz_num":"–"},"secretStr":"MjAxNS0wNy0yMCMwWDAwI1oyMjUjMDk6NDAjMjE6NTAjMjQwMDAwWjIyNTAwI0JKUCNIRkgjMDc6MzAj5YyX5LqsI%2BWQiOiCpSMwMSMwNSM2MDcwMDUwMDE0NDAzODE1MDI1NjMwMjQ0NTAzMTIjUDMjMTQzMzgzNTA5MjA2NSMxNDMyMjYwMDAwMDAwI0Q2Qjk5OTI1MDQ1M0M4MDEwMTY3QjM3NkZENDIwN0RCQjE3NzAyNDUzQzBBQzlGRjY4NDE2NDlF","buttonTextInfo":"预订"}],"messages":[],"validateMessages":{}}
……这些都是个啥啊
这些是JSON数据,当然没有任何格式化和缩进。对于程序读取是没问题的,但是对人工读取来说,真是……
这时候可以用工具格式化。要读取的话,可以有两种方式。一种是使用第三方工具(或者在线工具)来进行格式化,比如在线代码格式化:
或者你可以直接在Chrome的开发者工具中的预览里看:
具体用哪种工具看你自己乐意。
回到这个数据上来看,用猜吧,比较多的东西还是比较简单的,比如 data是个数组,表示所有的车次信息,下面的 canWebBuy 一定是说能不能买,然后到达时间出发时间,发站,到站,发站电报码到站电报码车次编号等等……
这里比较新鲜的一个是secureStr
,这个比较关键。其实现在还看不出来关键,但是后面当我们想去提交订单的时候,就会发现它很关键,现在我们只是提前说一声。
数据分析出来的话,那么就开工吧
5.2 接入查票流程
5.2.1 点击按钮事件处理
点击查票按钮的时候,我们先准备好查询的数据。
btnQueryTicket.Click += (s, e) => QueryTicket();
/// <summary> /// 从选择的字符串获得电报码 /// </summary> /// <param name="str"></param> /// <returns></returns> bool GetTeleCode(string str, out string name, out string code) { name = code = null; if (str.IsNullOrEmpty()) return false; var args = Regex.Split(str, @"\s+"); if (args.Length != 3) return false; name = args[1]; code = args[2]; return true; } /// <summary> /// 查票 /// </summary> /// <returns></returns> async Task QueryTicket() { string fromCode, fromName, toCode, toName; var date = dtDate.Value; if (!GetTeleCode(cbFrom.Text, out fromName, out fromCode) || !GetTeleCode(cbTo.Text, out toName, out toCode)) { MessageBox.Show(this, "哎呀,没有选择车站,逗我呢 o(╯□╰)o", "哎呀", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } }
嗯……我隐隐地赶脚这里要接入服务了
在服务上大展拳脚之前我们先定义好实体类吧。为了清晰明了,这里定义俩实体类。一个实体类用于存放查询回来的数据,数据项和JSON类成对应关系,以便于JSON.net直接反序列化。一个是自己的实体类,经过分析转换后的数据,便于使用数据进行处理或绑定。
在开始之前,我们再回去看看log和query两个请求的响应内容,会发现有部分内容是统一的。
虽然部分数据对我们来说并没有什么意义,但是有些东西是值得注意的,比如messages
。为什么这么说呢,比如你尝试手动发起请求,并故意制造一点错误看看。
喔,原来messages
里放的是错误信息,还是个数组。
然后在响应中,data
才是存放实际最终数据的地方,而status则意味着是否成功(表问我怎么知道的,我猜的 )。
所以我们可以把这部分公共数据提取出来,作为一个响应基类。当然,不提取每个请求都分开也未尝不可,但……我就提取了,怎么滴吧,你咬我啊
多抓一些不同操作的响应来看,data
的实际数据类型是不一样的,因此我们设计为泛型。
先定义泛型的响应基类(Service/Entities/Web/WebResponseResult.cs)。
/// <summary> /// 服务器数据响应基类 /// </summary> /// <typeparam name="T"></typeparam> class WebResponseResult<T> { /// <summary> /// 状态 /// </summary> public bool Status { get; private set; } /// <summary> /// 数据 /// </summary> public T Data { get; private set; } /// <summary> /// 响应的错误信息 /// </summary> public string[] Messages { get; private set; } /// <summary> /// 获得响应的错误信息 /// </summary> /// <returns></returns> public string GetErrorMessage() { return Messages == null || Messages.Length == 0 ? "" : Messages.JoinAsString("; "); } }
然后定义查票数据的实际承载类(Service/Entities/Web/QueryTicketResultData.cs)。
PS:映射这些玩意儿真是个力气活
class QueryTicketResultDataInfo { public string Train_No { get; private set; } public string Station_Train_Code { get; private set; } /// <summary> /// 始发站电报码 /// </summary> public string Start_Station_TeleCode { get; private set; } /// <summary> /// 始发站名称 /// </summary> public string Start_Station_Name { get; private set; } /// <summary> /// 发站电报码 /// </summary> public string From_Station_TeleCode { get; private set; } /// <summary> /// 发站名称 /// </summary> public string From_Station_Name { get; private set; } /// <summary> /// 到站电报码 /// </summary> public string To_Station_TeleCode { get; private set; } /// <summary> /// 到站名称 /// </summary> public string To_Station_Name { get; private set; } /// <summary> /// 到站电报码 /// </summary> public string End_Station_TeleCode { get; private set; } /// <summary> /// 到站名称 /// </summary> public string End_Station_Name { get; private set; } public string Start_Time { get; private set; } public string Arrive_Time { get; private set; } public int Day_Difference { get; private set; } /// <summary> /// 历时 /// </summary> public int LiShiValue { get; private set; } public string CanWebBuy { get; private set; } public string Gg_Num { get; private set; } public string Gr_Num { get; private set; } public string Qt_Num { get; private set; } public string Rw_Num { get; private set; } public string Rz_Num { get; private set; } public string Tz_Num { get; private set; } public string Wz_Num { get; private set; } public string Yb_Num { get; private set; } public string Yw_Num { get; private set; } public string Yz_Num { get; private set; } public string Ze_Num { get; private set; } public string Zy_Num { get; private set; } public string Swz_Num { get; private set; } }
class QueryTicketResultDataItem { /// <summary> /// 提交密钥 /// </summary> public string SecretStr { get; private set; } /// <summary> /// 提交密钥 /// </summary> public string ButtonTextInfo { get; private set; } /// <summary> /// 车次信息 /// </summary> public QueryTicketResultDataInfo QueryLeftNewDTO { get; private set; } }
class QueryTicketResultData : List<QueryTicketResultDataItem> { }
……最后我们再定义要用于显示绑定和处理的自己的数据类。
原谅我懒得写注释了……
using Ticket12306Demo.Service.Entities.Web; /// <summary> /// 查票结果 /// </summary> class TicketQueryResult : List<TicketQueryResultItem> { /// <summary> /// 查询的发站 /// </summary> public string QueryFromStationName { get; set; } /// <summary> /// 查询的发站电报码 /// </summary> public string QueryFromStationCode { get; set; } /// <summary> /// 查询的到站 /// </summary> public string QueryToStationName { get; set; } /// <summary> /// 查询的到站电报码 /// </summary> public string QueryToStationCode { get; set; } /// <summary> /// 查询的日期 /// </summary> public DateTime QueryDate { get; set; } } /// <summary> /// 查票结果-单车次 /// </summary> class TicketQueryResultItem { QueryTicketResultDataItem _rawData; /// <summary> /// Initializes a new instance of the <see cref="TicketQueryResultItem"/> class. /// </summary> /// <param name="queryDate">当前的查询日期</param> /// <param name="rawData"></param> public TicketQueryResultItem(DateTime queryDate, QueryTicketResultDataItem rawData) { _rawData = rawData; ElapsedTime = new TimeSpan(0, _rawData.QueryLeftNewDTO.LiShiValue, 0); LeftTime = queryDate.Add(TimeSpan.Parse(_rawData.QueryLeftNewDTO.Start_Time)); ArriveTime = LeftTime.Add(ElapsedTime); DaysCost = _rawData.QueryLeftNewDTO.Day_Difference; } public string TrainCode { get { return _rawData.QueryLeftNewDTO.Station_Train_Code; } } public string TrainID { get { return _rawData.QueryLeftNewDTO.Train_No; } } /// <summary> /// 发站名 /// </summary> public string FromStationName { get { return _rawData.QueryLeftNewDTO.From_Station_Name; } } /// <summary> /// 是否是始发站 /// </summary> public bool IsFirstStation { get { return _rawData.QueryLeftNewDTO.From_Station_TeleCode == _rawData.QueryLeftNewDTO.Start_Station_TeleCode; } } public string FromStationCode { get { return _rawData.QueryLeftNewDTO.From_Station_TeleCode; } } public string ToStationName { get { return _rawData.QueryLeftNewDTO.To_Station_Name; } } public string ToStationCode { get { return _rawData.QueryLeftNewDTO.To_Station_TeleCode; } } /// <summary> /// 是否是终到站 /// </summary> public bool IsLastStation { get { return _rawData.QueryLeftNewDTO.End_Station_TeleCode == _rawData.QueryLeftNewDTO.To_Station_TeleCode; } } public TimeSpan ElapsedTime { get; private set; } public DateTime LeftTime { get; private set; } public DateTime ArriveTime { get; private set; } public int DaysCost { get; private set; } public string SecretStr { get; private set; } public string Gg_Num { get { return _rawData.QueryLeftNewDTO.Gg_Num; } } public string Gr_Num { get { return _rawData.QueryLeftNewDTO.Gr_Num; } } public string Qt_Num { get { return _rawData.QueryLeftNewDTO.Qt_Num; } } public string Rw_Num { get { return _rawData.QueryLeftNewDTO.Rw_Num; } } public string Rz_Num { get { return _rawData.QueryLeftNewDTO.Rz_Num; } } public string Tz_Num { get { return _rawData.QueryLeftNewDTO.Tz_Num; } } public string Wz_Num { get { return _rawData.QueryLeftNewDTO.Wz_Num; } } public string Yb_Num { get { return _rawData.QueryLeftNewDTO.Yb_Num; } } public string Yw_Num { get { return _rawData.QueryLeftNewDTO.Yw_Num; } } public string Yz_Num { get { return _rawData.QueryLeftNewDTO.Yz_Num; } } public string Ze_Num { get { return _rawData.QueryLeftNewDTO.Ze_Num; } } public string Zy_Num { get { return _rawData.QueryLeftNewDTO.Zy_Num; } } public string Swz_Num { get { return _rawData.QueryLeftNewDTO.Swz_Num; } } #region 部分额外的显示信息 public string Disp_FromName { get { if (!IsFirstStation) return FromStationName; return FromStationName + " [始]"; } } public string Disp_ToName { get { if (!IsLastStation) return ToStationName; return ToStationName + " [终]"; } } public string Disp_ElapsedTime { get { var hour = ((int)ElapsedTime.TotalHours) + "时" + ElapsedTime.Minutes + "分"; if (DaysCost > 0) { return hour + "/" + DaysCost + "天"; } return hour; } } public string ButtonText { get { return _rawData.ButtonTextInfo; } } #endregion }
数据类准备好了,那么就开工吧……在TicketQueryService 中引入查询函数,参见注释。
在发送查票请求之前,我们需要先准备好一个函数和俩变量用于保存之前我们分析得到的查询地址和查询日志请求开关
string _queryUrl; bool _saveQueryLog; async Task LoadInitPage() { var client = ServiceContext.Session.NetClient; var ctx = client.Create<string>(HttpMethod.Get, "https://kyfw.12306.cn/otn/leftTicket/init"); await ctx.SendTask(); if (!ctx.IsValid()) { //失败了,没有能获得地址 return; } //获得到了查票页HTML,分析参数 var matches = Regex.Matches(ctx.Result, @"var\s*(CLeftTicketUrl|isSaveQueryLog)\s*=\s*['""]([^'""]+)['""];"); foreach (Match match in matches) { switch (match.GetGroupValue(1)) { case "CLeftTicketUrl": _queryUrl = match.GetGroupValue(2); break; case "isSaveQueryLog": _saveQueryLog = match.GetGroupValue(2) == "Y"; break; } } }
如果抓取成功查询页,则直接使用正则表达式提取地址和开关变量。
抓到查询地址后,我们就可以羞羞地查票了……
/// <summary> /// 查票 /// </summary> /// <param name="date">出发日期</param> /// <param name="fromName">发站名</param> /// <param name="fromCode">发站电报码</param> /// <param name="toName">到站名</param> /// <param name="toCode">到站电报码</param> /// <param name="isStudent">是否是学生票</param> /// <returns></returns> public async Task<TicketQueryResult> QueryTicket(DateTime date, string fromName, string fromCode, string toName, string toCode, bool isStudent) { var client = ServiceContext.Session.NetClient; if (_queryUrl.IsNullOrEmpty()) { //没有查询地址,说明咱还没查询过。所以这里先加载一下引导页 await LoadInitPage(); } //如果还是没有找到查询页地址,则报错 if (_queryUrl.IsNullOrEmpty()) { throw new Exception("无法找到查询页地址"); } //构造查询字符串 var queryparam = string.Format("?leftTicketDTO.train_date={0}&leftTicketDTO.from_station={1}&leftTicketDTO.to_station={2}&purpose_codes={3}", date.ToString("yyyy-MM-dd"), fromCode, toCode, (isStudent ? "0X00" : "ADULT")); //如果需要发送日志,则发送 if (_saveQueryLog) { client.Create<string>(HttpMethod.Get, "https://kyfw.12306.cn/otn/leftTicket/log" + queryparam, "https://kyfw.12306.cn/otn/leftTicket/init" ).SendAsync(false); } //发送查询请求 var ctx = client.Create<WebResponseResult<QueryTicketResultData>>( HttpMethod.Get, "https://kyfw.12306.cn/otn/" + _queryUrl + queryparam, "https://kyfw.12306.cn/otn/leftTicket/init" ); await ctx.SendTask(); if (!ctx.IsValid()) { throw ctx.Exception ?? new Exception("未能查询。"); } if (ctx.Result.Data == null) { //服务器返回错误信息 throw new Exception(ctx.Result.GetErrorMessage().DefaultForEmpty("未知错误信息")); } //生成结果 var result = new TicketQueryResult() { QueryFromStationCode = fromCode, QueryFromStationName = fromName, QueryToStationCode = toCode, QueryToStationName = toName, QueryDate = date }; result.AddRange(ctx.Result.Data.Select(s => new TicketQueryResultItem(date, s))); return result; }
注意 /log 请求我们使用异步请求发送后,并没有管它的结果,因为官网也没管。查询到结果后,通过数据包装,将原始数据包装为我们使用的实体,然后再返回。
拿到数据后,就可以进行绑定显示了。此时可以完善查询函数如下。
TicketQueryResult _result; /// <summary> /// 查票 /// </summary> /// <returns></returns> async Task QueryTicket() { string fromCode, fromName, toCode, toName; var date = dtDate.Value; if (!GetTeleCode(cbFrom.Text, out fromName, out fromCode) || !GetTeleCode(cbTo.Text, out toName, out toCode)) { MessageBox.Show(this, "哎呀,没有选择车站,逗我呢 o(╯□╰)o", "哎呀", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } _result = null; Exception exception = null; BeginOperation("正在查询...", 0); btnQueryTicket.Enabled = false; try { _result = await _context.TicketQueryService.QueryTicket(date, fromName, fromCode, toName, toCode, ckStudent.Checked); } catch (Exception ex) { exception = ex; } finally { EndOperation(); } if (_result != null) { stStatus.Text = string.Format("查询到 {0} 趟车次。", _result.Count); //绑定数据 bs.DataSource = _result; } else { stStatus.Text = "查询出错,错误信息:" + exception.Message; } }
因为查票有可能失败,所以等待查询结果需要用 try....catch
进行封装,以便于查票失败的话领取错误
查票结束后,根据实际的结果进行状态显示。值得注意的是这里的DataGridView使用了BindingDataSource进行绑定,用数据源进行绑定的原因是……我太懒了。
绑定这里又有问题了,虽然列的设计布局和12306官网一模一样,顺序也完全一致,但是我肿么知道哪个列对应的那个变量呢?
这个简单……直接扒12306的源码……
看到顺序了吧……对应的变量也看到了……看到了看到了……
界面上的各个列也都直接绑定到对应的属性了,对DataGridView不熟的同学可以查阅下相关的教程(老实说我这是第一次用数据绑定,向来不屑的,如果搞错了也别告诉我,就让我一错到底吧 )
此时我们希望根据查票的结果还对单元格进行一点格式化以直观的查看,于是进行了简单的格式化。
void InitTicketGrid() { dgvTickets.CellFormatting += DgvTickets_CellFormatting; } private void DgvTickets_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { var index = e.ColumnIndex; var data = _result[e.RowIndex]; if (index == 1) { //发站 if (data.IsFirstStation) { e.CellStyle.Font = new Font(e.CellStyle.Font, FontStyle.Bold); } } else if (index == 3) { //到站 if (data.IsLastStation) { e.CellStyle.Font = new Font(e.CellStyle.Font, FontStyle.Bold); } } else if (index >= 6 && index <= 16) { var text = e.Value.ToString(); if (text == "" || text == "无" || text == "--" || text == "*") { e.CellStyle.ForeColor = Color.Gray; } else if (text == "有" || char.IsDigit(text[0])) { e.CellStyle.ForeColor = Color.Blue; e.CellStyle.Font = new Font(e.CellStyle.Font, FontStyle.Bold); } } }
至此,查票的整个流程就结束了,让我们来看看吧。
FSLIB.NETWORK 网络库系列文章
- 12306订票助手.NET V10.6.1 发布8年前 (2016-09-01)
- 开源 FSLIB.NETWORK 库 2.2.0.08年前 (2016-08-02)
- FSLIB.NETWORK手册(1) · 基本概念和流程8年前 (2016-05-05)
- 原创FSLib.Network库更新 2.0.0 版8年前 (2016-04-05)
- 原创FSLib.Network库更新 1.6.0版(目前专注于HTTP的高性能高易用性网络库)9年前 (2015-12-13)
- 玩具系列:批量QQ群签到工具v2 (暂时屏蔽自定义位置功能)9年前 (2015-08-29)
- 玩具系列:批量QQ群签到工具(支持自定义位置)9年前 (2015-08-28)
- 12306订票助手.NET 8.0.8 发布9年前 (2015-08-21)
- 12306订票客户端 FOR .NET 演示项目 【7】登录9年前 (2015-08-18)
- 12306订票客户端 FOR .NET 演示项目 【6】验证码输入9年前 (2015-08-12)
- 12306订票客户端 FOR .NET 演示项目 【5】获得余票数据9年前 (2015-06-10)
- 原创FSLib.Network库发布 1.5 版9年前 (2015-06-09)
- 12306订票客户端 FOR .NET 演示项目 【4】界面框架&基础数据初始化9年前 (2015-06-08)
- 12306订票客户端 FOR .NET 演示项目 【3】流程分析和项目规划9年前 (2015-05-28)
- 12306订票客户端 FOR .NET 演示项目 【2】准备工具9年前 (2015-05-22)
- 12306订票客户端 FOR .NET 演示项目 【1】项目概况9年前 (2015-05-19)
- 原创FSLib.Network库发布 1.4 版9年前 (2015-05-08)
- 放一个抓取网页的信息监控小工具源码9年前 (2015-04-27)
- FSLib.Network网络库使用教程[2] 实例教程·美女们快到硬盘里来!9年前 (2015-01-30)
- FSLib.Network网络库使用教程[1] 基本使用9年前 (2015-01-19)
- 原创FSLib.Network库(目前专注于HTTP的高性能高易用性网络库)9年前 (2015-01-18)
现在还能用吗
不错,学到很多
鱼哥,//发送查询请求
var ctx = client.Create<WebResponseResult>(
HttpMethod.Get,
"https://kyfw.12306.cn/otn/" + _queryUrl + queryparam,
"https://kyfw.12306.cn/otn/leftTicket/init"
);
await ctx.SendTask();
这里的client.Create<WebResponseResult>是怎么处理的,我这里怎么总是返回NULL呢?
什么返回null?后续问题去 http://ask.fishlee.net/ 询问哦。
鱼哥,我在http://ask.fishlee.net/question/7 有追问了
遇到问题的话请发新问题,在别的问题下回复一般是看不到的。我现在还不清楚你遇到的是啥问题。请在那个板块提交新问题,最好附上调试过程中的步骤结果。
要烂尾了么
下一篇什么时候出来
期待待下一篇咯,顶顶顶
期待待下一篇咯,顶顶顶
嗯 似乎解析浏览器解析错了~~
这个明天看看
我用的是QQ浏览器 9.0 就是最近新出的那个。
但不是手机QQ内置浏览器
已修复。
FSLib.Network 好像版本不对 下了个1.5的编译才成功
版本不对?
我是直接拉取你的GitHub上的代码。 重新编译运行报错FSLib.Network 。 然后我就在NuGet上查找你的FSLib.Network 。 安装过程中提示 由1.4升级到1.5 然后就可以正常运行了
GitHub上的不一定是最新的,可能会落后几天。没有特殊必要的话不建议用源码自己编译。
赞一个
不知怎么就进来了,先看看
码字辛苦,赞一个!