解析12306订票流程
前言
每当春节临近时,因为网络的方便,访问12306购买火车票回家过年成了很多人的首选。但由于12306的种种不给力,给那些在官网刷票的人带来了很多的不便。从2011年未12306上线起,连续几年回家我都是靠网上购票,今年也不例外;我记得11年时我使用的是官网直接购票,到了12年则使用了新出的木鱼抢票助手,而今年我用了360与猎豹两款主流抢票浏览器,还发动了几位朋友一起帮忙,才买到了一张差强人意的票,现在感觉买票是越来越困难。而就在前几天媒体还曝出了商业黄牛使用假身份证生成器10分钟钞杀1000多张票的新闻,让人吃惊不已。于是就萌生了自己写一个抢票应用的念头,最开始设想的就是本地桌面应用,而非浏览器插件,个人觉得本地应用始终比浏览器插件敏捷,因为本地应用可以精确稳定的请求有用的链接,过滤图片和CSS等前台无用请求,可以节省网络消耗时间。于是我花了一段时间将12306的整体订票流程解析了一遍,其间还经历了一次12306的改版,幸好主体流程改动不是很大,终算有点收获。
粗略的将12306的流程划分为:登录、查询和订票三大模块,下面就这三大模块逐一说明:
1.登录
登录12306请求的URL是:https://kyfw.12306.cn/otn/login/init,可以使用Firbug抓取一下它的请求头,得到的response响应内容如下:

从中可以看到Set-Cookie信息,也就是说,如果想要登录就必须先请求https://kyfw.12306.cn/otn/login/init这个链接,以获取服务端设置的Cookie信息,而有了该Cookie信息就可以将其保存,以备下步的请求使用。
再来分析一下它的页面HTML与其对应处理登录的Javascript脚本文件(https://kyfw.12306.cn/otn/resources/merged/login_js.js),得到如下流程:
1.用户点击登录提交时先要验证请求一下:https://kyfw.12306.cn/otn/login/loginAysnSuggest链接,用于判断当前网络环境是否可以登录,得到JSON数据(通过Firebug抓包):
[plain] view plaincopy![]()
- {
- "validateMessagesShowId":"_validatorMessage"
- "status":true
- "httpstatus":200,
- "data":{
- "loginCheck":"Y"
- },
- "messages":[],
- "validateMessages":{}
- }
这里通过判断data.loginCheck是否为字符串Y判断用户是否可以登录,如不能登录,则显示messages中的内容.
2.当用户登录信息检查成功时,则POST请求https://kyfw.12306.cn/otn/login/userLogin,得到登录请求后的HTML,对应请求的参数为:
[plain] view plaincopy![]()
- "loginUserDTO.user_name": // 用户名
- "userDTO.password": // 密码
- "randCode": // 验证码
注:登录图片验证码的获取地址可以从登录页面的HTML中得到为:https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=login&rand=sjrand
3.通过解析获取的HTML可以根据id为login-txt的<span>标签来判断是否登录成功,登录成功的对应的HTML内容为:
[html] view plaincopy![]()
- <span class="login-txt" style="color: #666666">
- <span>意见反馈:
- <a class="cursor colorA" href="mailto:12306yjfk@rails.com.cn">
- 12306yjfk@rails.com.cn
- </a>您好,
- </span>
- <a id="login_user" href="/otn/index/initMy12306"
- class="colorA" style="margin-left:-0.5px;"><span>登录成功用户名</span></a>|
- <a id="regist_out" href="/otn/login/loginOut">退出</a>
- </span>
失败的内容为:
[html] view plaincopy![]()
- <span class="login-txt" style="color: #666666">
- <span>意见反馈:
- <a class="cursor colorA" href="mailto:12306yjfk@rails.com.cn">
- 12306yjfk@rails.com.cn
- </a>您好,请
- </span>
- <a id="login_user" href="/otn/login/init"
- class="colorA" style="margin-left:-0.5px;">登录</a> |
- <a id="regist_out" href="/otn/regist/init">注册</a>
- </span>
如上登录成功即可进行下一步的操作:对于车次的查询。
2,车次查询
新版车次预订的查询(这里单指单程票查询)大大减化了请求参数,只接收出发地编码,到达地编码,出发日期与旅客编码四个参数,所有的过滤操作都扔给了前台Javascript,这也说明了车次查询流程的简单,只需请求一个链接地址:
查询车次是通过GET:https://kyfw.12306.cn/otn/leftTicket/query链接获取的,对应的查询参数为(GET请求注意查询参数的顺序):
[plain] view plaincopy![]()
- leftTicketDTO.train_date=2014-01-23 // 出发日期
- leftTicketDTO.from_station=BJP // 出发站编码
- leftTicketDTO.to_station=SHH // 到达站编码
- purpose_codes=ADULT // 旅客编码:成人为ADULT,学生为:0X00
对应的获取的JSON信息格式如下:
[plain] view plaincopy![]()
- {"validateMessagesShowId": "_validatorMessage",
- "status": true,
- "httpstatus": 200,
- "data": [
- {"queryLeftNewDTO": {
- "train_no": "240000G14104", // 列车编号
- "station_train_code": "G141", // 车次
- "start_station_telecode": "VNP", // 始发站编码
- "start_station_name": "北京南", // 始发站名
- "end_station_telecode": "AOH", // 终到站编码
- "end_station_name": "上海虹桥", // 终到站名
- "from_station_telecode": "VNP", // 查询输入经过站编码
- "from_station_name": "北京南", // 查询输入经过站名
- "to_station_telecode": "AOH", // 查询输入到站编码
- "to_station_name": "上海虹桥", // 查询输入到站名
- "start_time": "14:16", // 出发时间
- "arrive_time": "19:47", // 到站时间
- "day_difference": "0", // 花费天数
- "train_class_name": "",
- "lishi": "05:31", // 历时
- "canWebBuy": "Y", // 是否可以预定
- "lishiValue": "331",
- "yp_info": "O055300094M0933000999174800017",
- "control_train_day": "20301231",
- "start_train_date": "20140123",
- "seat_feature": "O3M393",
- "yp_ex": "O0M090",
- "train_seat_feature": "3",
- "seat_types": "OM9",
- "location_code": "P3",
- "from_station_no": "01",
- "to_station_no": "09",
- "control_day": 19,
- "sale_time": "1400", // 出票时间点hhmm
- "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": "17" // 商务座剩余数
- },
- "secretStr": "预定请求令牌字符串",
- "buttonTextInfo": "预订或开售日期"
- },
- .......... // 省略其它车次,信息同上
- ],
- "messages": [],
- "validateMessages": {}
- }
注意这里的canWebBuy属性,用于标记该趟列车是否可以预订,还有对应列车的secretStr字符,它用于请求预订确认页面的令牌,
对于其中一直提到的列车站点编码,可以通过请求https://kyfw.12306.cn/otn/resources/js/framework/station_name.js链接,通过得到JS脚本中的station_names变量获取,对应的站点以@字符分隔,而每一个站点信息如下,这里以北京北为例:
[plain] view plaincopy![]()
- bjb|北京北|VAP|beijingbei|bjb|0
用于提取其中有用的信息是:北京北与VAP,使用查询北京北的编码就是VAP,其它站点的解析同理。
如上即可以查询指定出发地与到达地的车次预定信息,紧接着进行预订流程的分析。
3,车票预订
在12306的解析中,就属车票预订的解析最为费神,也是最核心的一个流程,我现在只掌握了成人单程票的预订流程,其他的比如返程,学生票等都还没有分析出来,如下讲解的就是关于成人单程票的预定基本流程:
3.1,获取预定确认页面
车票预定首先要请求获取车票的预订确认页面,如下流程图所示:

分析:该流程是在用户单击车次的“预订”按钮时触发的,如图所示,获取预订确认页面,先要判断用户是否登录,POST请求的地址是:https://kyfw.12306.cn/otn/login/checkUser,这个请求无参数,然后通过判断得到的JSON信息中的data.flag属性是否为true判断用户是否已登录,接着再根据对应列车查询时所获得的secretStr字符与用户输入的查询信息POST请求https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest,判断用户是否可以访问预定确认画面,通过得到JSON信息的status属性判断是否允许访问,如果为true说明可以访问,最后依据旅行类型为单程(dc)POST跳转获取单程车票的预订确认画面:https://kyfw.12306.cn/otn/confirmPassenger/initDc。如果登录用户不进行上述判断,直接POST请求https://kyfw.12306.cn/otn/confirmPassenger/initDc提示非法请求,只有成功获取预订确认页面后才能进行下一步的操作。
注:该流程可以查看对应JS脚本:https://kyfw.12306.cn/otn/resources/merged/queryLeftTicket_end_js.js,function L(b4, bX)方法获知。
从请求订单的确认画面还可以得到获取当前登录用户常用联系人的链接地址为:https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs。
3.2,预订提交
在车票的预定提交之前必先要获取预定确认画面的原因是因为预订确认HTML中声明的orderRequestDTO与ticketInfoForPassengerForm两个Javascript变量,含有预订提交的时的必需参数信息,下面就预订提交给出粗略的流程分析图,如下:

注:图片可以右击后查看大图,该流程对应的JS文件地址为:https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js
分析:如上图显示了车票预定提交的大体流程,可以依据请求的链接数将其分为四大块:
1.检查用户选择的乘客信息的合法性,POST请求:https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo,通过分析得到的JSON中的data.submitStatus属性是否为true判断,同时这一步的JSON信息中还会包含有一个data.isCheckOrderInfo属性将会作为下一步判断当前用户是否可排队请求的参数。对应请求参数有如下5个:
[javascript] view plaincopy![]()
- cancel_flag: "2", // 固定值
- bed_level_order_num: "000000000000000000000000000000", // 固定值
- passengerTicketStr: getpassengerTickets(), // 旅客信息字符串
- oldPassengerStr: getOldPassengers(), // 旅客信息字符串
- tour_flag: ticketInfoForPassengerForm.tour_flag, // 从ticketInfoForPassengerForm中获取
- randCode: $("#randCode").val() // 前台输入验证码
这五个参数中,有两个参数需要注意passengerTicketStr与oldPassengersStr:
passengerTicketStr是以下划线"_"分隔当每一个乘客信息组成的字符串,对应每个乘客信息字符串组成如下:
[plain] view plaincopy![]()
- 座位编号,0,票类型,乘客名,证件类型,证件号,手机号码,保存常用联系人(Y或N)
同样oldPassengersStr也是以下划线"_"分隔每个乘客信息组成的字符串,对应每个乘客信息字符串组成如下:
[plain] view plaincopy![]()
- 乘客名,证件类型,证件号,乘客类型
在上面的信息中座位编号指的是,一等座、二等座等的编码,从ticketInfoForPassengerForm.limitBuySeatTicketDTO.seat_type_codes属性中选择获取。
票类型指的是,成人票,学生票等的编码,可以从ticketInfoForPassengerForm.limitBuySeatTicketDTO.ticket_type_codes属性中选择获取。
证件类型指的是二代身份证,学生证,签证等的编码,可以从ticketInfoForPassengerForm.cardTypes属性中选择获取。
最后oldPassengersStr中的乘客类型主要有如下信息:
[javascript] view plaincopy![]()
- adult: "1",
- child: "2",
- student: "3",
- disability: "4"
取上面对应的数字编码。
注意:在组合oldPassengersStr乘客信息字符串时,未尾会多一个下划线,提交请求是一定要补上,从上也可以看出所有的一些参数都是通过ticketInfoForPassengerForm变量获取的,这也是为什么要事先获取预定确认画面HTML的原因。
2.检查乘合信息合法后,接下来就会结合返回的data.isCheckOrderInfo属性,POST请求:https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue,判断当前乘客是否可以排队,对应的参数如下:
[javascript] view plaincopy![]()
- train_date: new Date(orderRequestDTO.train_date.time).toString(), // 列车日期
- train_no: orderRequestDTO.train_no, // 列车号
- stationTrainCode: orderRequestDTO.station_train_code,
- seatType: limit_tickets[0].seat_type, // 座位类型
- fromStationTelecode: orderRequestDTO.from_station_telecode, // 发站编号
- toStationTelecode: orderRequestDTO.to_station_telecode, // 到站编号
- leftTicket: ticketInfoForPassengerForm.queryLeftTicketRequestDTO.ypInfoDetail,
- purpose_codes: n, // 默认取ADULT,表成人,学生表示为:0X00
- isCheckOrderInfo: m // data.isCheckOrderInfo
这里的参数要注意传递列车日期的方式,及座位类型编码,这里选择的是第一个乘客的座位类型编码。最后还要确保orderRequestDTO变量的准确性。
通过返回的JSON信息的data属性值来判断是否允许当前用户进行排队下单,并提示当前的剩余票数。
其中的data属性会包含有两个重要的参数,countT与ticket,(ticket的格式为:1*****30314*****00001*****00003*****0000的形式):
countT表示的是排队人数,而ticket指的是当前列车对应座位的剩余票数,可以通过https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js文件中的function L(l, m) 函数解析获取:
[javascript] view plaincopy![]()
- function L(l, m) {
- rt = "";
- seat_1 = -1;
- seat_2 = -1;
- i = 0;
- while (i < l.length) {
- s = l.substr(i, 10);
- c_seat = s.substr(0, 1);
- if (c_seat == m) {
- count = s.substr(6, 4);
- while (count.length > 1 && count.substr(0, 1) == "0") {
- count = count.substr(1, count.length)
- }
- count = parseInt(count);
- if (count < 3000) {
- seat_1 = count
- } else {
- seat_2 = (count - 3000)
- }
- }
- i = i + 10
- }
- if (seat_1 > -1) {
- rt += seat_1
- }
- if (seat_2 > -1) {
- rt += "," + seat_2
- }
- return rt
- }
函数中的l指的就是ticket,而m指的是第一位乘客所选择的座位编号。
如果计算的余票信息还有剩余,则会提示用户点击确认按进行订单的提交请求,如果没有充实的票,则会提示用户选择其它车次,处理该请求的方法详情见https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js文件中的function M(n, m) 方法。
3.当提示的有充足的余票,且用户点击了确定按钮,则接下来会POST请求:https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue,进行单程票(dc)类型的排队下单操作,通过判断返回的JSON信息data.submitStatus属性判断订单是否以成功提交至服务器,对应的请求参数为:
[javascript] view plaincopy![]()
- passengerTicketStr: getpassengerTickets(),
- oldPassengerStr: getOldPassengers(),
- randCode: $("#randCode").val(),
- purpose_codes: ticketInfoForPassengerForm.purpose_codes,
- key_check_isChange: ticketInfoForPassengerForm.key_check_isChange,
- leftTicketStr: ticketInfoForPassengerForm.leftTicketStr,
- train_location: ticketInfoForPassengerForm.train_location
这里的参数没有新意,主要是注意获取ticketInfoForPassengerForm变量的准确性。
4.订单提交至服务器后不一定说明订单已经成功了,还需要GET请求:https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime,判断系统是否已根据提交的订单信息为相应的乘客占位成功,并提示预估出票等待时间,这一步只有一个参数,就是旅行类型,由于我们主要考虑的是单程票,故提交时POST dc就行了,如下:
[javascript] view plaincopy![]()
- tourFlag: "dc"
这一步占位的操作在12306的官网中是将其封装在了一个名为OrderQueueWaitTime的对象中,可以解压https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js文件获知,对应的如果判断系统占位成功,将会从返的JSON信息中获取data.orderId属性,即为下单成功时的订单号。
如上4次请求就可以准确的模拟出12306官网订单提交的整套流程,其中其实还忽略了验证码的获取与判断操作,而这一步仅仅是判断验证码的合法性,与主体流程无关。对应订单确定页面的验证码获取链接为:https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=passenger&rand=randp,从中与登录页面的验证码链接对比,可知新版12306的验证码管理统一为了一个方法,登录与订单确认的验证码链接只是传递的module和rand参数不一样而已。
4,结束语:
根据上面的操作,基本可以全程模拟官网的订单操作,编写出一个属于自己的抢票助手。在写这篇文章时,我一直在想这样做是否有意义,因为12306随时都有可能变更,由于23:00点~07:00点的维护时间段的设置,也许今天写出来的东西明天马上就会失效过期。但仔细考虑后还是打算将他分享出来,就当是一种学习吧。同时在这里公布GitHub上使用Python3编写的一个订票项目源码:https://github.com/lzqwebsoft/trainticket,对应window下独立运行exe文件下载地址为:https://code.google.com/p/lzqwebsoft-projects/source/browse/#svn%2Ftrunk,软件运行效果如下:

买笔记本的8个小技巧 最适合自己才最好
显然,智能手机和平板在一定程度上可以替代传统电脑,让我们可以随时随地上网、使用各种应用。不过,传统电脑也拥有它的不可替代性,比如移动办公、视频编辑、玩游戏,笔记本电脑可能是个更好的选择。
作为一种成熟的电脑类型,笔记本电脑的定位、种类已经十分丰富,尺寸、性能、大小也都各不相同,还拥有几种主流的系统,该如何选择呢?下面我们就来分析分析。
你需要用笔记本电脑做什么?
多用途:如果你想要电脑能够胜任一切基本应用,比如处理文档、看看视频、玩玩休闲游戏,低端的15英寸笔记本会是一个好选择。如果再需要一点移动性,那就选择13英寸的入门机型。这种类型的笔记本,售价一般在3000至5000元之间,选择还是很丰富的。

商务:不论是商务人士还是学生,如果主要使用笔记本处理文档、表格,那么要注意选择轻薄、键盘手感好以及耐用的笔记本,屏幕效果也很重要。每个品牌都有商务笔记本类型,大家可以参考一下。
游戏:如果要使用笔记本电脑玩高端PC游戏,那么首选拥有高端酷睿i7处理器、高端独立显卡(甚至双显卡)以及高分辨率屏幕的Windows机型。此类产品价格普遍较高,所以至少要准备7000元及以上的预算。

创意工作:如果想要使用笔记本编辑视频、照片,需要一个强大的处理器、独立显卡、固态硬盘和大尺寸的高分辨率屏幕,比如苹果的Macbook Pro Retina等机型。
上网冲浪/电子邮件:如果你要给孩子购买笔记本、或是将它作为辅助电脑使用,可以选择类似Chromebook这样的低成本产品。
选择合适的尺寸

搞清楚要用笔记本电脑做什么之后,就可以选择尺寸了。通常来说,11至12英寸的产品最轻薄,还拥有一些采用了灵活设计的平板/电脑二合一产品,不过它们的屏幕和键盘设计稍微局促一些。
13到14英寸的笔记本电脑是最为易用和平衡的选择,大部分商务、学生本都使用了这个尺寸,所以拥有非常多的选择,可以轻松放在膝盖上使用。
15英寸也是极为流行的尺寸,通常以低成本产品为主,但重量偏重,不太适合外出携带。另外,15英寸笔记本通常也配备了光驱。
17至18英寸的笔记本电脑基本上以影音、游戏应用为主,性能达到了工作站级别,机身沉重性能强大,可以作为台式机的替代品使用。
检查键盘和触摸板

显然,我们选择笔记本电脑,是希望获得良好的输入感受,所以键盘和触摸板是非常重要的。如果你去笔记本电脑卖场进行试用,一定要亲自在键盘上打打字、并测试一下触摸板的滑动感和左右键按压感,以及多点触摸缩放功能,看看手感是否出色。一般情况下,苹果Macbook的键盘和触摸板是业界最为出色的,大家可以以此为标准进行测试。
了解硬件规格

笔记本电脑是一种硬件,所以硬件配置是非常重要的,能够决定你的电脑可以做什么。以下是你需要注意的硬件配置。
核心组件:处理器、内存和显卡基本上是计算机的核心组件,所以首先需要考虑这些部分。处理器方面以英特尔酷睿为主,目前最新的Haswell拥有最好的性能及电池寿命。除非你购买一个低端产品,否则不要考虑酷睿i3和AMD A系列的处理器。
至于内存,目前普遍达到了4GB标准,但随着64位系统的普及,内存自然是越大越好,8GB-16GB能够让你获得更好的多任务性能。至于显卡,如果需要运行大型3D游戏,需要配备AMD及Nvidia的独立显卡;如果只是办公,一般的集成显卡完全够用,并且更省电。
硬盘及闪存缓存:硬盘的种类,也是决定笔记本性能的重要部分。目前,我们更推荐固态硬盘选项,虽然容量偏低一些,但是寿命、速度都极具优势;如果预算不够,那么尽量选择7200转的大容量机械硬盘。如果选择“超极本”,通常它们会配备闪存缓存作为硬盘辅助,提供系统性能,尽量选择更大容量的缓存吧。
屏幕及分辨率:大多数的低价笔记本采用1366*768像素的屏幕,如果有可能尽量选择1920*1080像素甚至更高的分辨率。另外,如果选择了Windows 8笔记本,最好选择具备触摸屏的款式,毕竟这是一款针对触摸屏设计的系统。
光驱:目前配备光驱的笔记本已经越来越少了,但是如果你需要刻录或是观看蓝光碟片,就需要考虑一下具备光驱的机型了。
混合设计还是传统笔记本?

自Windows 8推出以来,我们看到越来越多拥有混合设计的PC产品出现,比如多模式笔记本、平板/笔记本二合一产品,比如联想Yoga系列、Suface Pro等等。但大多数情况下,它们都无法实现两全的体验。多模式电脑虽然能够变为平板模式,但是由于键盘不能拆卸,显然不如iPad好用;而平板/笔记本二合一产品的外接键盘,又比较狭窄、手感较差,所以还是要根据自己最经常的使用形式来选择。
电池寿命非常重要

如果要选择13、14英寸的便携式笔记本,那么电池寿命是非常重要的。我们建议用户选择Haswell平台、可拆卸电池设计的产品,如ThinkPad X240,最长可实现20小时的续航,十分惊人。虽然苹果Macbook系列的续航能力不低,但是电池不能拆卸,所以也需要考虑一下。
品牌的选择

品牌对于任何商品来说都非常重要,笔记本电脑也不例外。我们可以参考一些媒体的屏蔽数据,比如Laptop,在他们的“2013年笔记本品牌技术排行”中,索尼、苹果和三星位列前三;而在“2013年最佳笔记本品牌”中,苹果、联想和华硕位列前三。这些数据可以作为一些参考。
选择最适合自己的操作系统

目前市场中的笔记本电脑以Windows为主,另外还拥有苹果Mac和谷歌Chromebook,大多数用户都会选择Windows产品,目前包括少量Windows 7及大量Windows 8笔记本。其中,Windows 8/8.1笔记本通常搭载触摸屏、产品种类也最为丰富,可以应用在办公、娱乐、商务等多种领域。
苹果Macbook的总销量在2013年实际上是呈下降趋势的,但是最新的Mavericks OS和Haswell版Macbook Air、Pro也具备非常好的使用体验。相比Windows,它们更适合上网、屏幕设计以及视频编辑等应用,价格则偏高,预算需要至少7000元。
最后的谷歌Chromebook,有些类似早年的上网本,配置通常较低,但是由于Chrome OS基于互联网的特性,所以使用起来还是很流畅的。当然,它的功能稍有局限,比如不支持本地打印、Office应用、本地应用有限等等,但是价格通常极为低廉,1500元左右就可以买到。

