C++写的12306抢票软件

您所在的位置:网站首页 抢单脚本制作教学 C++写的12306抢票软件

C++写的12306抢票软件

2024-07-10 10:13| 来源: 网络整理| 查看: 265

写在前面的话 每年逢年过节,一票难求读者肯定不陌生。这篇文章,我们带领读者从零实现一款12306刷票软件,其核心原理还是通过发送http请求模拟登录12306网站的购票的过程,最后买到票。 郑重申明一下:这里介绍的技术仅供用于学习,不可用于恶意攻击12306服务器,请勿滥用本文介绍的技术。对12306服务器造成的任何损失,后果自负。 当然,由于12306服务器用户量巨大,为了防止黄牛和其他一些非法攻击者,12306的很多url和在购票过程中各个步骤的协议细节经常发生变化。所以,本文中介绍的一些具体的url,可能在你看到本文时已经失效。但是这并没有关系,只要你掌握了本文中介绍的分析方法,您就可以灵活地修改您的代码,以适应最新的12306服务器的要求。举个例子,如12306的查票接口目前的url是:https://kyfw.12306.cn/otn/leftTicket/query,可能过几天就变成了https://kyfw.12306.cn/otn/leftTicket/queryX,再过几天又可能变成https://kyfw.12306.cn/otn/leftTicket/queryY,然后一个星期后又可能变成https://kyfw.12306.cn/otn/leftTicket/queryZ,这些笔者都见过。所以,重在原理的学习,掌握了原理,不管12306的相关url变成什么样,都可以以不变应万变。哎,12306在与黄牛斗争的路上越走越远啊。T_T

本文将使用以下工具来分析12306购票的过程,然后使用C++语言,模拟相关的过程,最终购票。 1、Chrome浏览器(其他的浏览器也可以,都有类似的界面,如Chrome,装了httpwatch的IE浏览器等) 2、一个可以登录12306网址并且可以购票的12306账号 3、Visual Studio(版本随意,我这里用的是VS 2013) 一、查票与站点信息接口 之所以先分析这个接口,是因为查票不需要用户登录的,相对来说最简单。我们在Chrome浏览器中打开12306余票查询页面,网址是:https://kyfw.12306.cn/otn/leftTicket/init,如下图所示:

然后在页面中右键菜单中选择【检查】菜单,打开后,选择【网络】选项卡。如下图所示:

打开后页面变成二分窗口了,左侧是正常的网页页面,右侧是浏览器自带的控制台,当我们在左侧页面中进行操作后,右侧会显示我们浏览器发送的各种http请求和应答。我们这里随便查一个票吧,如查2018年5月20日从上海到北京的票,点击查询后,我们发现右侧是这样的:

通过图中列表的type值是xhr,我们可以得出这是一个ajax请求(ajax是浏览器原生支持的一种异步请求,详情请自行百度)。我们选择这个请求,你能看到这个请求的细节——请求和响应结果:

在reponse中,我们可以看到我们的这个http的去除http头的响应结果:

这是一个json格式,我们找个json格式化工具,把这个json格式化后贴出来给大家看一下,其实您后面会发现12306的http请求结果中与购票相关的数据基本上都是json格式。这里的json如下:

{     "validateMessagesShowId": "_validatorMessage",     "status": true,     "httpstatus": 200,     "data": { "result": ["null|23:00-06:00系统维护时间|5l0000G10270|G102|AOH|VNP|AOH|VNP|06:26|12:29|06:03|IS_TIME_NOT_BUY|RLVVIt093U2EZuy2NE+VQyRloXyqTzFp6YyNk6J52QcHEA01|20180520|3|HZ|01|11|1|0|||||||||||1|有|13||O090M0|O9M|0", "null|23:00-06:00系统维护时间|5l0000G10470|G104|AOH|VNP|AOH|VNP|06:40|12:33|05:53|IS_TIME_NOT_BUY|j/TM45GgyJRRKvdalo3VIal8nYF7Hy9VL6njjGX3nOR3xwIu|20180520|3|HZ|01|09|1|0|||||||||||2|有|15||O090M0|O9M|0", "null|23:00-06:00系统维护时间|55000000G600|G6|SHH|VNP|SHH|VNP|07:00|11:38|04:38|IS_TIME_NOT_BUY|SO6mCijnVzhdTrntsbeMoJ4Vuw/WsAnsBz80diva/wuIfsS5|20180520|3|H1|01|05|1|0|||||||||||1|5|8||O090M0|O9M|0", "null|23:00-06:00系统维护时间|5l0000G106A0|G106|AOH|VNP|AOH|VNP|07:12|13:13|06:01|IS_TIME_NOT_BUY|Limy8VLpKgfmzb1EJZ0G7P8/Ai5iR7qbbwhplNeOVIxLQYab|20180520|3|HY|01|11|1|0|||||||||||1|11|12||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G10870|G108|AOH|VNP|AOH|VNP|07:22|13:23|06:01|IS_TIME_NOT_BUY|OJIuMonF9ctgAxxDpZRkNy0fn4HrG8Y+6ThVIAxtGrCWIp0N|20180520|3|HY|01|12|1|0|||||||||||无|6|3||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G110B0|G110|AOH|VNP|AOH|VNP|07:28|13:38|06:10|IS_TIME_NOT_BUY|HVY2cA5DQzMC1VDiotEG4zXAOwG4fHHYq2bh1ZFhm47pySly|20180520|3|HY|01|11|1|0|||||||||||无|5|13||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G120S0|G120|AOH|VNP|AOH|VNP|07:51|13:33|05:42|IS_TIME_NOT_BUY|G2C5o+MADORl4B9HQ2jmTdT2+fBnCbCXvfKCjqf0Fmm6fbU2|20180520|3|H6|01|08|1|0|||||||||||无|有|10||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l000000G814|G8|AOH|VNP|AOH|VNP|08:00|12:24|04:24|IS_TIME_NOT_BUY|dEqPPAVH6ICSdUQQwQ1ry/Ns0+QJCE2N+EZd4oC7FOmz855B|20180520|3|H6|01|04|1|0|||||||||||4|4|9||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G11293|G112|AOH|VNP|AOH|VNP|08:05|14:08|06:03|IS_TIME_NOT_BUY|j1BM0nZuw/phl6Z7WFxg0kFAc5Z4t+qKWZe3fjKB5ZR72nLl|20180520|3|HY|01|11|1|0|||||||||||无|3|2||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G11470|G114|AOH|VNP|AOH|VNP|08:15|14:13|05:58|IS_TIME_NOT_BUY|OwWGlKxfnPfPYGOuhjVhioA2r3kj2krs0zxNVD04+IDhPhfc|20180520|3|HY|01|11|1|0|||||||||||无|1|无||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l000000G232|G2|AOH|VNP|AOH|VNP|09:00|13:28|04:28|IS_TIME_NOT_BUY|8Q4veHYksOBLKJU03KPa0jbPDTgUByjp+UFMScwuarKvhZ+F|20180520|3|HY|01|04|1|0|||||||||||无|5|1||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G11670|G116|AOH|VNP|AOH|VNP|09:33|15:23|05:50|IS_TIME_NOT_BUY|jsCsXdkuWHZVgZ0YzaO+zWokRnnDQ4zowg78aRmc/hzNEMjK|20180520|3|HY|01|10|1|0|||||||||||无|6|2||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G11860|G118|AOH|VNP|AOH|VNP|24:00|24:00|99:59|IS_TIME_NOT_BUY||20180520||H6|01|11|0|1|||||||||||||||||0", "null|23:00-06:00系统维护时间|5l00000G1001|G10|AOH|VNP|AOH|VNP|10:00|14:28|04:28|IS_TIME_NOT_BUY|ycAb36mk9wXaSIll0bTc5WbH8wLT1YRVjvGH/cYzAxIoVMcU|20180520|3|H1|01|04|1|0|||||||||||无|无|5||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5600000G4280|G42|HGH|VNP|AOH|VNP|10:26|16:08|05:42|IS_TIME_NOT_BUY|usY+Ul57hWKitIUp1d4m3n3e0ys4iJTdDfedKU6oXk7F3bAb|20180520|3|H6|04|13|1|0|||||||||||无|无|无||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G12290|G122|AOH|VNP|AOH|VNP|10:41|16:43|06:02|IS_TIME_NOT_BUY|tNu43MkXqpjkcIe80jbPhpSgQ3IOcIyLbwMSspllz0Btc3mJ|20180520|3|H6|01|12|1|0|||||||||||无|5|3||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G124V0|G124|AOH|VNP|AOH|VNP|11:00|16:18|05:18|IS_TIME_NOT_BUY|otn+9ShYEtsJ+6yDQexyyomS8daAeRrvr958XuZ8C4hldEB1|20180520|3|H6|01|06|1|0|||||||||||1|8|3||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G126B0|G126|AOH|VNP|AOH|VNP|11:05|17:05|06:00|IS_TIME_NOT_BUY|HIpEbr9n0fqeUtQGaASOBoD+/duc8JM5U1M602j0rnrf0XfA|20180520|3|H6|01|12|1|0|||||||||||4|8|无||O090M0|O9M|0", "null|23:00-06:00系统维护时间|5l0000G128N0|G128|AOH|VNP|AOH|VNP|24:00|24:00|99:59|IS_TIME_NOT_BUY||20180520||H1|01|12|0|1|||||||||||||||||0", "null|23:00-06:00系统维护时间|5l0000G13080|G130|AOH|VNP|AOH|VNP|11:20|17:29|06:09|IS_TIME_NOT_BUY|eaISX27C/T247JdvbJCFWkXvFimDh4W5rNAht1O5/1PhCbLN|20180520|3|H1|01|13|1|0|||||||||||无|无|2||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5500000G1200|G12|SHH|VNP|SHH|VNP|12:00|16:38|04:38|IS_TIME_NOT_BUY|GxssVQj1spkQVDnyUYodUASXXdwKUnuMjltjIAMwB2IbtIxC|20180520|3|H1|01|04|1|0|||||||||||无|无|无||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G132C0|G132|AOH|VNP|AOH|VNP|12:17|18:32|06:15|IS_TIME_NOT_BUY|2obvVTZf5/iiIKfTAkXU8tDIK4dMypDrpaoQO0WhfqKp3b5h|20180520|3|H1|01|13|1|0|||||||||||无|2|4||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5500001462I0|1462|SHH|BJP|SHH|BJP|12:18|10:46|22:28|IS_TIME_NOT_BUY|05Xf+SuYrrrVUcoitze9/BO1a6zlhm/43WFiXQjDEU7Z+hbDUoKqD2myF3Y=|20180520|3|H2|01|23|0|0||||2|||有||无|有|||||10401030|1413|0", "null|23:00-06:00系统维护时间|5l0000G41250|G412|AOH|VNP|AOH|VNP|12:28|18:48|06:20|IS_TIME_NOT_BUY|CtWjFYsZE3ih/LiOPF03WQb8CvMe6jwdlqUwBRxKn3yRAn9F|20180520|3|H2|01|11|1|0|||||||||||无|2|2||O090M0|O9M|0", "null|23:00-06:00系统维护时间|5l0000G134B0|G134|AOH|VNP|AOH|VNP|13:01|18:58|05:57|IS_TIME_NOT_BUY|AO3hxVofuYXk7l6EhzGCCEu4ZHPpS/0A/nkroM7xlpx/fIIX|20180520|3|H6|01|11|1|0|||||||||||3|6|12||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G136O0|G136|AOH|VNP|AOH|VNP|24:00|24:00|99:59|IS_TIME_NOT_BUY||20180520||H6|01|11|0|1|||||||||||||||||0", "null|23:00-06:00系统维护时间|5l0000G13860|G138|AOH|VNP|AOH|VNP|13:30|19:28|05:58|IS_TIME_NOT_BUY|qgHsrIv2ECcib/ImiXBHGt9Vis0yzPG8bKHoOZ0RgY7aE5sK|20180520|3|H6|01|12|1|0|||||||||||无|8|5||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G14060|G140|AOH|VNP|AOH|VNP|13:35|19:41|06:06|IS_TIME_NOT_BUY|ERb1/PPb8O6WfX503UB/hvYJsZO74WIYIjQsCisEZ4esappf|20180520|3|H6|01|13|1|0|||||||||||2|无|6||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l000000G432|G4|AOH|VNP|AOH|VNP|14:00|18:28|04:28|IS_TIME_NOT_BUY|2x7UHKlapgd4OJrubhQIW25wn5ZyA0jvumVcUSzkWJZu+9yr|20180520|3|H6|01|04|1|0|||||||||||无|3|1||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G14253|G142|AOH|VNP|AOH|VNP|14:10|20:18|06:08|IS_TIME_NOT_BUY|LuImd+o+UIDry0/CjwMAzgBtvfwyN4dSpjzXZnTQxN89PqQk|20180520|3|H6|01|11|1|0|||||||||||1|7|9||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G144M3|G144|AOH|VNP|AOH|VNP|14:40|20:29|05:49|IS_TIME_NOT_BUY|xNsqS1nHci52T9o6E1hU3epRaV9cHSpKnl6i+5+2sWsHHOZQ|20180520|3|H6|01|10|1|0|||||||||||1|2|5||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G146F0|G146|AOH|VNP|AOH|VNP|14:52|20:48|05:56|IS_TIME_NOT_BUY|jAmoXkDA3YgUo4lorosGtKbjeNZ15a764hrcb9URyVEUCWBU|20180520|3|H6|01|10|1|0|||||||||||1|6|13||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l00000G1442|G14|AOH|VNP|AOH|VNP|15:00|19:36|04:36|IS_TIME_NOT_BUY|VyN8KW3DEeWDipXBnZoMhHHVf6m6YwwJ3QT5GnlQqbQPFOCK|20180520|3|H6|01|05|1|0|||||||||||2|2|1||O090M0|O9M|0", "null|23:00-06:00系统维护时间|5l0000G148D0|G148|AOH|VNP|AOH|VNP|15:23|21:13|05:50|IS_TIME_NOT_BUY|v4DRs/7cxkGkWywbOoZYi/lM8FMuYWVO31zuFqaoPsWzuk2N|20180520|3|H6|01|11|1|0|||||||||||无|有|4||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G17000|G170|AOH|VNP|AOH|VNP|15:52|21:18|05:26|IS_TIME_NOT_BUY|OwWGlKxfnPfPYGOuhjVhioA2r3kj2krs0zxNVD04+IDhPhfc|20180520|3|H1|01|08|1|0|||||||||||无|1|无||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G15060|G150|AOH|VNP|AOH|VNP|16:05|22:00|05:55|IS_TIME_NOT_BUY|B+kl5hvzm26b184g8odo4t15OHC22ban1A1nGGF301bDERGO|20180520|3|H6|01|10|1|0|||||||||||1|有|8||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G152E0|G152|AOH|VNP|AOH|VNP|16:18|22:12|05:54|IS_TIME_NOT_BUY|81VzXPX7cSnMfNL08HCNwU+u50GpJ+QNOZctnNmnxXE8onhQ|20180520|3|H6|01|10|1|0|||||||||||无|有|15||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l00000G1613|G16|AOH|VNP|AOH|VNP|17:00|21:36|04:36|IS_TIME_NOT_BUY|1Tjp2E11rAd8KSvlP8BLxwfyqQNNurrS6nFPFNIumUhIkIX3|20180520|3|H6|01|05|1|0|||||||||||1|无|4||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G15470|G154|AOH|VNP|AOH|VNP|17:13|22:48|05:35|IS_TIME_NOT_BUY|FMIX4FHuTLpNf0wPQlJhJvoLN5kawBBXSs2PWGQJ/422H0c0|20180520|3|H6|01|08|1|0|||||||||||无|有|5||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G156R0|G156|AOH|VNP|AOH|VNP|17:18|22:58|05:40|IS_TIME_NOT_BUY|wnJtQjVkFz37b4Xp1eP4obJTdrV9ioOqRUvqvJzy7+AYI7YL|20180520|3|H6|01|09|1|0|||||||||||1|有|17||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5600000G44B0|G44|HGH|VNP|AOH|VNP|17:23|23:08|05:45|IS_TIME_NOT_BUY|4M/BToLy7SoKriz9NLnM6EZwyFF9Tt//rrPb6JCTSb6DtMgW|20180520|3|H6|04|13|1|0|||||||||||8|无|1||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G158C0|G158|AOH|VNP|AOH|VNP|17:34|23:29|05:55|IS_TIME_NOT_BUY|3qcvQyDRKrXX2hJGyupGQxH/evCUFK0TJKN6KMqh8Lzyu/dQ|20180520|3|H6|01|10|1|0|||||||||||1|有|15||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l0000G160V0|G160|AOH|VNP|AOH|VNP|17:44|23:48|06:04|IS_TIME_NOT_BUY|Fs4rA/vbQ+b+MOZP5UK4sCe4nmEcE9xJsA1SywdMcZ2otlav|20180520|3|H6|01|10|1|0|||||||||||有|有|有||O0M090|OM9|0", "null|23:00-06:00系统维护时间|5l00000G1829|G18|AOH|VNP|AOH|VNP|18:00|22:36|04:36|IS_TIME_NOT_BUY|NEuxbLCppnaF8Fm+wuVXDFCSMsVBbOGsCrawCMD/YLarh6s3|20180520|3|H6|01|05|1|0|||||||||||1|5|4||O0M090|OM9|0", "null|23:00-06:00系统维护时间|550000T11061|T110|SHH|BJP|SHH|BJP|18:02|09:30|15:28|IS_TIME_NOT_BUY|Zqy8vHHz4tA2WNH/H1f8d2PE0pc2k+48QoX2hffwrKeUU8zTljDvKT0kSNLlww66AJUR/1v6ckE=|20180520|3|H3|01|09|0|0||无||无|||有||2|1|||||1040106030|14163|0", "null|23:00-06:00系统维护时间|5l00000G2219|G22|AOH|VNP|AOH|VNP|19:00|23:18|04:18|IS_TIME_NOT_BUY|pbuRJ1NgYwLV0f1B6kNwLT1sMCL9o/+CDoQJ6vd1Kbe3GP+1|20180520|3|H6|01|03|1|0|||||||||||6|3|5||O0M090|OM9|0", "null|23:00-06:00系统维护时间|550000D31270|D312|SHH|VNP|SHH|VNP|19:10|07:07|11:57|IS_TIME_NOT_BUY|QNf6TCZV01wG6pmiy2gz3lg/QUAA/Uvm|20180520|3|H3|01|04|0|0||||5||||||||||1|F040|F4|1", "null|23:00-06:00系统维护时间|550000D32260|D322|SHH|VNP|SHH|VNP|19:53|07:45|11:52|IS_TIME_NOT_BUY|xtuqf0inq39vWyfVaA6GfBad2dPnjBk6|20180520|3|H3|01|03|0|0||||有|||||||无||||O040|O4|0", "null|23:00-06:00系统维护时间|550000D31490|D314|SHH|VNP|SHH|VNP|21:07|08:55|11:48|IS_TIME_NOT_BUY|Lamvi3Rs8Nk3cxG7zey21PJvsuzo7v5O|20180520|3|H3|01|04|0|0||||有|||||||5||||O040|O4|0"], "flag": "1", "map": { "AOH": "上海虹桥", "BJP": "北京", "VNP": "北京南", "SHH": "上海" } },     "messages": [],     "validateMessages": {} }

其中含有的余票信息在result节点中,这是一个数组。每个节点以|分割,我们可以格式化后显示在自己的界面上:

我这里做的界面比较简陋,读者如果有兴趣可以做更精美的界面。我们列下这个请求发送的http数据包和应答包: 请求包:

GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-20&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive Cache-Control: no-cache Accept: */* X-Requested-With: XMLHttpRequest If-Modified-Since: 0 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Referer: https://kyfw.12306.cn/otn/leftTicket/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_fromDate=2018-05-20; _jc_save_toDate=2018-05-19; _jc_save_wfdc_flag=dc HTTP/1.1 200 OK Date: Sat, 19 May 2018 15:23:58 GMT Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked ct: C1_217_85_8 Content-Encoding: gzip Age: 1 X-Via: 1.1 houdianxin183:6 (Cdn Cache Server V2.0) Connection: keep-alive X-Dscp-Value: 0 X-Cdn-Src-Port: 33963 Cache-Control: no-cache, no-store

通过上一篇文章《从零实现一个http服务器》我们知道这是一个http GET请求,其中在url后面是请求附带的参数: leftTicketDTO.train_date: 2018-05-20 leftTicketDTO.from_station: SHH leftTicketDTO.to_station: BJP purpose_codes: ADULT 这四个参数分别是购票日期、出发站、到达站和票类型(这里是成人票(普通票)),正好对应我们界面上的查询信息:

但是,读者可能会问,这里的出发站和到达站分别是SHH和BJP,这些站点代码,我如何获得呢?因为只有知道这些站点编码我才能自己购买指定出发站和到达站的火车票啊。如果您是一位细心的人,您肯定会想到,我们查票的时候再进入查票页面,这些站点信息就已经有了,那么可能是在这个查票页面加载时,从服务器请求的站点信息,所以我们刷新下查票页面,发现果然是这样:

进入查票页面之前,浏览器从https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053下载一个叫station.name.js文件,这是一个javascript脚本,里面只有一行代码,就是定义了一个station_names的js变量,之所以url地址后面加一个station_version=1.9053,你可以理解成版本号,但是主要是通过一个随机值1.9053,让浏览器不要使用缓存中的station_name.js,而是每次都从服务器重新加载下这个文件,这样的话如果站点信息有更新,也可以避免因为缓存问题,导致本地的缓存与服务器上的站点信息不一致。由于站点信息比较多,我们截个图吧:

看上图,我们可以看出来,每个站点信息都是通过@符号分割,然后通过|分割每一个站点的各种信息。这样的话,根据上文的格式假如我们要查询2018年5月30日从长春到南京的火车普通票,就可以通过网址https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=CCT&leftTicketDTO.to_station=NJH&purpose_codes=ADULT。 当然,这里需要说明一下的就是,由于全国的火车站点信息文件比较大,我们程序解析起来时间较长,加上火车站编码信息并不是经常变动,所以,我们我们没必要每次都下载这个station_name.js,所以我在写程序模拟这个请求时,一般先看本地有没有这个文件,如果有就使用本地的,没有才发http请求向12306服务器请求。这里我贴下我请求站点信息的程序代码(C++代码):

/**    * 获取全国车站信息   * @param si 返回的车站信息   * @param bForceDownload 强制从网络上下载,即不使用本地副本   */  bool GetStationInfo(vector& si, bool bForceDownload = false); #define URL_STATION_NAMES   "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053" bool Client12306::GetStationInfo(vector& si, bool bForceDownload/* = false*/) {       FILE* pfile;     pfile = fopen("station_name.js", "rt+");     //文件不存在,则必须下载     if (pfile == NULL)     {         bForceDownload = true;     }     string strResponse;     if (bForceDownload)     {         if (pfile != NULL)             fclose(pfile);         pfile = fopen("station_name.js", "wt+");         if (pfile == NULL)         {             LogError("Unable to create station_name.js");             return false;         }

        CURLcode res;         CURL* curl = curl_easy_init();         if (NULL == curl)         {             fclose(pfile);             return false;         }

        //URL_STATION_NAMES         curl_easy_setopt(curl, CURLOPT_URL, URL_STATION_NAMES);         //响应结果中保留头部信息         //curl_easy_setopt(curl, CURLOPT_HEADER, 1);         curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");         curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);         curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);         //设定为不验证证书和HOST         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);

        curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);         curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);

        res = curl_easy_perform(curl);         bool bError = false;         if (res == CURLE_OK)         {             int code;             res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);             if (code != 200)             {                 bError = true;                 LogError("http response code is not 200, code=%d", code);             }         }         else         {             LogError("http request error, error code = %d", res);             bError = true;         }

        curl_easy_cleanup(curl);

        if (bError)         {             fclose(pfile);             return !bError;         }

        if (fwrite(strResponse.data(), strResponse.length(), 1, pfile) != 1)         {             LogError("Write data to station_name.js error");                         return false;         }         fclose(pfile);     }     //直接读取文件     else     {         //得到文件大小         fseek(pfile, 0, SEEK_END);         int length = ftell(pfile);         if (length < 0)         {             LogError("invalid station_name.js file");             fclose(pfile);         }         fseek(pfile, 0, SEEK_SET);         length++;         char* buf = new char[length];         memset(buf, 0, length*sizeof(char));         if (fread(buf, length-1, 1, pfile) != 1)         {             LogError("read station_name.js file error");             fclose(pfile);             return false;         }         strResponse = buf;         fclose(pfile);     }

    /* 返回结果为一个js文件, var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2" */     //LogInfo("recv json = %s", strResponse.c_str());     OutputDebugStringA(strResponse.c_str());

    vector singleStation;     split(strResponse, "@", singleStation);

    size_t size = singleStation.size();     for (size_t i = 1; i < size; ++i)     {         vector v;         split(singleStation[i], "|", v);         if (v.size() < 6)             continue;

        stationinfo st;         st.code1 = v[0];         st.hanzi = v[1];         st.code2 = v[2];         st.pingyin = v[3];         st.simplepingyin = v[4];         st.no = atol(v[5].c_str());

        si.push_back(st);     }

    return true; }

这里用了一个站点信息结构体stationinfo,定义如下:

//var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2 struct stationinfo {     string code1;     string hanzi;     string code2;     string pingyin;     string simplepingyin;     int no; };

因为我们这里目的是为了模拟http请求做买火车票相关的操作,而不是技术方面本身,所以为了快速实现我们的目的,我们就使用curl库。这个库是一个强大的http相关的库,例如12306服务器返回的数据可能是分块的(chunked),这个库也能帮我们组装好;再例如,服务器返回的数据是使用gzip格式压缩的,curl也会帮我们自动解压好。所以,接下来的所有12306的接口,都基于我封装的curl库一个接口:

/**  * 发送一个http请求  *@param url 请求的url  *@param strResponse http响应结果  *@param get true为GET,false为POST  *@param headers 附带发送的http头信息  *@param postdata post附带的数据   *@param bReserveHeaders http响应结果是否保留头部信息  *@param timeout http请求超时时间  */  bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10);

函数各种参数已经在函数注释中写的清清楚楚了,这里就不一一解释了。这个函数的实现代码如下:

bool Client12306::HttpRequest(const char* url,                                string& strResponse,                                bool get/* = true*/,                                const char* headers/* = NULL*/,                                const char* postdata/* = NULL*/,                                bool bReserveHeaders/* = false*/,                                int timeout/* = 10*/) {     CURLcode res;     CURL* curl = curl_easy_init();     if (NULL == curl)     {         LogError("curl lib init error");         return false;     }

    curl_easy_setopt(curl, CURLOPT_URL, url);

    //响应结果中保留头部信息     if (bReserveHeaders)        curl_easy_setopt(curl, CURLOPT_HEADER, 1);     curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");     curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);     curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);     curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);     //设定为不验证证书和HOST     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);

    //设置超时时间     curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout);     curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);     curl_easy_setopt(curl, CURLOPT_REFERER, URL_REFERER);     //12306早期版本是不需要USERAGENT这个字段的,现在必须了,估计是为了避免一些第三方的非法刺探吧。     //如果没有这个字段,会返回     /* HTTP/1.0 302 Moved Temporarily Location: http://www.12306.cn/mormhweb/logFiles/error.html Server: Cdn Cache Server V2.0 Mime-Version: 1.0 Date: Fri, 18 May 2018 02:52:05 GMT Content-Type: text/html Content-Length: 0 Expires: Fri, 18 May 2018 02:52:05 GMT X-Via: 1.0 PSshgqdxxx63:10 (Cdn Cache Server V2.0) Connection: keep-alive X-Dscp-Value: 0 */     curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36");     //不设置接收的编码格式或者设置为空,libcurl会自动解压压缩的格式,如gzip     //curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");

    //添加自定义头信息     if (headers != NULL)     {         //LogInfo("http custom header: %s", headers);         struct curl_slist *chunk = NULL;                 chunk = curl_slist_append(chunk, headers);               curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);     }

    if (!get && postdata != NULL)     {         //LogInfo("http post data: %s", postdata);         curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata);     }

    LogInfo("http %s: url=%s, headers=%s, postdata=%s", get ? "get" : "post", url, headers != NULL ? headers : "", postdata!=NULL?postdata : "");

    res = curl_easy_perform(curl);     bool bError = false;     if (res == CURLE_OK)     {         int code;         res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);         if (code != 200 && code != 302)         {             bError = true;             LogError("http response code is not 200 or 302, code=%d", code);         }     }     else     {         LogError("http request error, error code = %d", res);         bError = true;     }

    curl_easy_cleanup(curl);

    LogInfo("http response: %s", strResponse.c_str());

   return !bError; }

正如上面注释中所提到的,浏览器在发送http请求时带的一些字段,我们不是必须的,如查票接口浏览器可能会发以下http数据包:

GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive Cache-Control: no-cache Accept: */* X-Requested-With: XMLHttpRequest If-Modified-Since: 0 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Referer: https://kyfw.12306.cn/otn/leftTicket/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20

其中像Connection、Cache-Control、Accept、If-Modified-Since等字段都不是必须的,所以我们在模拟我们自己的http请求时可以不用可以添加这些字段,当然据我观察,12306服务器现在对发送过来的http数据包要求越来越严格了,如去年的时候,User-Agent这个字段还不是必须的,现在如果你不带上这个字段,可能12306返回的结果就不一定正确。当然,不正确的结果中一定不会有明确的错误信息,充其量可能会告诉你页面不存在或者系统繁忙请稍后再试,这是服务器自我保护的一种重要的措施,试想你做服务器程序,会告诉非法用户明确的错误信息吗?那样不就给了非法攻击服务器的人不断重试的机会了嘛。 需要特别注意的是:查票接口发送的http协议的头还有一个字段叫Cookie,其值是一串非常奇怪的东西:JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-2。 在这串字符中有一个JSESSIONID,在不需要登录的查票接口,我们可以传或者不传这个字段值。但是在购票以及查询常用联系人这些需要在已经登录的情况下才能进行的操作,我们必须带上这个数据,这是服务器给你的token(验证令牌),而这个令牌是在刚进入12306站点时,服务器发过来的,你后面的登录等操作必须带上这个token,否则服务器会认为您的请求是非法请求。我第一次去研究12306的买票流程时,即使在用户名、密码和图片验证码正确的情况下,也无法登录就是这个原因。这是12306为了防止非法登录使用的一个安全措施。 二、登录与拉取图片验证码接口 我的登录页面效果如下:

12306的图片验证码一般由八个图片组成,像上面的“龙舟”文字,也是图片,这两处的图片(文字图片和验证码)都是在服务器上拼装后,发给客户端的,12306服务器上这种类型的小图片有一定的数量,虽然数量比较大,但是是有限的。如果你要做验证码自动识别功能,可以尝试着下载大部分图片,然后做统计规律。所以,我这里并没有做图片自动识别功能。有兴趣的读者可自行尝试。 先说下,拉取验证码的接口。我们打开Chrome浏览器12306的登录界面:https://kyfw.12306.cn/otn/login/init,如下图所示:

可以得到拉取验证码的接口:

我们可以看到发送的http请求数据包格式是:

GET /passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.7520968747611347 HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Accept: image/webp,image/apng,image/*,*/*;q=0.8 Referer: https://kyfw.12306.cn/otn/login/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: _passport_session=badc97f6a852499297796ee852515f957153; _passport_ct=9cf4ea17c0dc47b6980cac161483f522t9022; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20; BIGipServerpassport=837288202.50215.0000

这里也是一个http GET请求,Host、Referer和Cookie这三个字段是必须的,且Cookie字段必须带上上文说的JSESSIONID,下载图片验证码和下文中各个步骤也必须在Cookie字段中带上这个JSESSIONID值,否则无法从12306服务器得到正确的应答。后面会介绍如何拿到这个这。这个拉取图片验证码的http GET请求需要三个参数,如上面的代码段所示,即login_site、module、rand和一个类似于0.7520968747611347的随机值,前三个字段的值都是固定的,module字段表示当前是哪个模块,当前是登录模块,所以值是login,后面获取最近联系人时取值是passenger。这里还有一个需要注意的地方是,如果您验证图片验证码失败时,重新请求图片时,必须也重新请求下JSESSIONID。这个url是https://kyfw.12306.cn/otn/login/init。http请求和应答包如下: 请求包:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cache-Control: max-age=0 Connection: keep-alive Cookie: RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20; BIGipServerpassport=837288202.50215.0000 Host: kyfw.12306.cn Referer: https://kyfw.12306.cn/otn/passport?redirect=/otn/login/loginOut Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36

应答包:

HTTP/1.1 200 OK Date: Sun, 20 May 2018 02:23:53 GMT Content-Type: text/html;charset=utf-8 Transfer-Encoding: chunked Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn ct: C1_217_101_6 Content-Language: zh-CN Content-Encoding: gzip X-Via: 1.1 houdianxin184:4 (Cdn Cache Server V2.0) Connection: keep-alive X-Dscp-Value: 0 X-Cdn-Src-Port: 46480

这个值在应答包字段Set-Cookie中拿到: Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn 所以,我们每次请求图片验证码时,都重新请求一下这个JSESSIONID,代码如下:

#define URL_LOGIN_INIT      "https://kyfw.12306.cn/otn/login/init" bool Client12306::loginInit() {         string strResponse;     if (!HttpRequest(URL_LOGIN_INIT, strResponse, true, "Upgrade-Insecure-Requests: 1", NULL, true, 10))     {         LogError("loginInit failed");         return false;     }

    if (!GetCookies(strResponse))     {         LogError("parse login init cookie error, url=%s", URL_LOGIN_INIT);         return false;     }

    return true; } bool Client12306::GetCookies(const string& data) {     if (data.empty())     {         LogError("http data is empty");         return false;     }

    //解析http头部     string str;     str.append(data.c_str(), data.length());     size_t n = str.find("\r\n\r\n");     string header = str.substr(0, n);     str.erase(0, n + 4);

    //m_cookie.clear();     //获取http头中的JSESSIONID=21AC68643BBE893FBDF3DA9BCF654E98;     vector v;     while (true)     {         size_t index = header.find("\r\n");         if (index == string::npos)             break;         string tmp = header.substr(0, index);         v.push_back(tmp);         header.erase(0, index + 2);

        if (header.empty())             break;     }

    string jsessionid;     string BIGipServerotn;     string BIGipServerportal;     string current_captcha_type;     size_t m;     OutputDebugStringA("\nresponse http headers:\n");     for (size_t i = 0; i < v.size(); ++i)     {         OutputDebugStringA(v[i].c_str());         OutputDebugStringA("\n");         m = v[i].find("Set-Cookie: ");         if (m == string::npos)             continue;

        string tmp = v[i].substr(11);         Trim(tmp);         m = tmp.find("JSESSIONID");         if (m != string::npos)         {             size_t comma = tmp.find(";");             if (comma != string::npos)                 jsessionid = tmp.substr(0, comma);         }

        m = tmp.find("BIGipServerotn");         if (m != string::npos)         {             size_t comma = tmp.find(";");             if (comma != string::npos)                 BIGipServerotn = tmp.substr(m, comma);             else                 BIGipServerotn = tmp;         }

        m = tmp.find("BIGipServerportal");         if (m != string::npos)         {             size_t comma = tmp.find(";");             if (comma != string::npos)                 BIGipServerportal = tmp.substr(m, comma);             else                 BIGipServerportal = tmp;         }

        m = tmp.find("current_captcha_type");         if (m != string::npos)         {             size_t comma = tmp.find(";");             if (comma != string::npos)                 current_captcha_type = tmp.substr(m, comma);             else                 current_captcha_type = tmp;                    }     }

    if (!jsessionid.empty())     {         m_strCookies = jsessionid;         m_strCookies += "; ";         m_strCookies += BIGipServerotn;         if (!BIGipServerportal.empty())         {             m_strCookies += "; ";             m_strCookies += BIGipServerportal;         }         m_strCookies += "; ";         m_strCookies += current_captcha_type;         return true;     }

    LogError("jsessionid is empty");     return false; } #define URL_GETPASSCODENEW  "https://kyfw.12306.cn/passport/captcha/captcha-image"

bool Client12306::DownloadVCodeImage(const char* module) {     if (module == NULL)     {         LogError("module is invalid");         return false;     }

    //https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.06851784300754482     ostringstream osUrl;     osUrl tm_sec); #else     sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.v",         1900 + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday,         tblock->tm_hour, tblock->tm_min, tblock->tm_sec); #endif

    FILE* fp = fopen(m_szCurrVCodeName, "wb");     if (fp == NULL)     {         LogError("open file %s error", m_szCurrVCodeName);         return false;     }

    const char* p = strResponse.data();     size_t count = fwrite(p, strResponse.length(), 1, fp);     if (count != 1)     {         LogError("write file %s error", m_szCurrVCodeName);         fclose(fp);         return false;     }

    fclose(fp);

    return true; }

我们再看下验证码去服务器验证的接口https://kyfw.12306.cn/passport/captcha/captcha-check。 请求头:

POST /passport/captcha/captcha-check HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive Content-Length: 50 Accept: application/json, text/javascript, */*; q=0.01 Origin: https://kyfw.12306.cn X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Referer: https://kyfw.12306.cn/otn/login/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: _passport_session=3e39a33a25bf4ea79146bd9362c11ad62327; _passport_ct=c5c7940e08ce44db9ad05d213c1296ddt4410; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20; BIGipServerpassport=837288202.50215.0000

这是一个POST请求,其中POST数据带上的输入的图片验证码选择的坐标X和Y值:   answer: 175,58,30,51  login_site: E  rand: sjrand 这里我选择了两张图片,所以有两组坐标值,(175,58)是一组,(30,51)是另外一组,这个坐标系如下:

因为每个图片的尺寸都一样,所以,我可以给每个图片设置一个坐标范围,当选择了一个图片,给一个在其中的坐标即可,不一定是鼠标点击时的准确位置:

//刷新验证码 登录状态下的验证码传入”randp“,非登录传入”sjrand“ 具体参看原otsweb中的传入参数 struct VCodePosition {     int x;     int y; };

const VCodePosition g_pos[] = {     { 39, 40 },     { 114, 43 },     { 186, 42 },     { 252, 47 },     { 36, 120 },     { 115, 125 },     { 194, 125 },     { 256, 120 } };

//验证码图片八个区块的位置 struct VCODE_SLICE_POS {     int xLeft;     int xRight;     int yTop;     int yBottom; };

const VCODE_SLICE_POS g_VCodeSlicePos[] =  {     {0,   70,  0,  70},     {71,  140, 0,  70 },     {141, 210, 0,  70 },     {211, 280, 0,  70 },     { 0,  70,  70, 140 },         {71,  140, 70, 140 },     {141, 210, 70, 140 },     {211, 280, 70, 140 } };

//8个验证码区块的鼠标点击状态 bool g_bVodeSlice1Pressed[8] = { false, false, false, false, false, false, false, false};

验证的图片验证码的接口代码是:

int Client12306::checkRandCodeAnsyn(const char* vcode) {     string param;     param = "randCode=";     param += vcode;     param += "&rand=sjrand";    //passenger:randp

    string strResponse;     string strCookie = "Cookie: ";     strCookie += m_strCookies;     if (!HttpRequest(URL_CHECKRANDCODEANSYN, strResponse, false, strCookie.c_str(), param.c_str(), false, 10))     {         LogError("checkRandCodeAnsyn failed");         return -1;     }

    ///** 成功返回     //HTTP/1.1 200 OK     //Date: Thu, 05 Jan 2017 07:44:16 GMT     //Server: Apache-Coyote/1.1     //X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1     //ct: c1_103     //Content-Type: application/json;charset=UTF-8     //Content-Length: 144     //X-Via: 1.1 jiandianxin29:6 (Cdn Cache Server V2.0)     //Connection: keep-alive     //X-Cdn-Src-Port: 19153

    //参数无效     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":""},"messages":[],"validateMessages":{}}     //验证码过期     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":"EXPIRED"},"messages":[],"validateMessages":{}}     //验证码错误     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"FALSE"},"messages":[],"validateMessages":{}}     //验证码正确     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"TRUE"},"messages":[],"validateMessages":{}}     Json::Reader JsonReader;     Json::Value JsonRoot;     if (!JsonReader.parse(strResponse, JsonRoot))         return -1;     //{"validateMessagesShowId":"_validatorMessage", "status" : true, "httpstatus" : 200, "data" : {"result":"1", "msg" : "TRUE"}, "messages" : [], "validateMessages" : {}}     if (JsonRoot["status"].isNull() || JsonRoot["status"].asBool() != true)         return -1;

    if (JsonRoot["httpstatus"].isNull() || JsonRoot["httpstatus"].asInt() != 200)         return -1;

    if (JsonRoot["data"].isNull() || !JsonRoot["data"].isObject())         return -1;

    if (JsonRoot["data"]["result"].isNull())         return -1;

    if (JsonRoot["data"]["result"].asString() != "1" && JsonRoot["data"]["result"].asString() != "0")         return -1;

    if (JsonRoot["data"]["msg"].isNull())         return -1;     //if (JsonRoot["data"]["msg"].asString().empty())             //    return -1;

    if (JsonRoot["data"]["msg"].asString() == "")         return 0;     else if (JsonRoot["data"]["msg"].asString() == "FALSE")         return 1;

    return 1; }

同理,这里也给出验证用户名和密码的接口实现代码:

int Client12306::loginAysnSuggest(const char* user, const char* pass, const char* vcode) {     string param = "loginUserDTO.user_name=";     param += user;     param += "&userDTO.password=";     param += pass;     param += "&randCode=";     param += vcode;     string strResponse;     string strCookie = "Cookie: ";     strCookie += m_strCookies;     if (!HttpRequest(URL_LOGINAYSNSUGGEST, strResponse, false, strCookie.c_str(), param.c_str(), false, 10))     {         LogError("loginAysnSuggest failed");         return 2;     }

    ///** 成功返回     //HTTP/1.1 200 OK     //Date: Thu, 05 Jan 2017 07:49:53 GMT     //Server: Apache-Coyote/1.1     //X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1     //ct: c1_103     //Content-Type: application/json;charset=UTF-8     //Content-Length: 146     //X-Via: 1.1 f186:10 (Cdn Cache Server V2.0)     //Connection: keep-alive     //X-Cdn-Src-Port: 48361

    //邮箱不存在     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["该邮箱不存在。"],"validateMessages":{}}     //密码错误     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["密码输入错误。如果输错次数超过4次,用户将被锁定。"],"validateMessages":{}}     //登录成功     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"otherMsg":"",loginCheck:"Y"},"messages":[],"validateMessages":{}}     //WCHAR* psz1 = Utf8ToAnsi(strResponse.c_str());     //wstring str = psz1;     //delete[] psz1;

    Json::Reader JsonReader;     Json::Value JsonRoot;     if (!JsonReader.parse(strResponse, JsonRoot))         return 2;

    //{"validateMessagesShowId":"_validatorMessage", "status" : true,      //"httpstatus" : 200, "data" : {"otherMsg":"", loginCheck : "Y"}, "messages" : [], "validateMessages" : {}}     if (JsonRoot["status"].isNull())         return -1;

    bool bStatus = JsonRoot["status"].asBool();     if (!bStatus)         return -1;

    if (JsonRoot["httpstatus"].isNull() || JsonRoot["httpstatus"].asInt() != 200)         return 2;

    if (JsonRoot["data"].isNull() || !JsonRoot["data"].isObject())         return 2;

    if (JsonRoot["data"]["otherMsg"].isNull() || JsonRoot["data"]["otherMsg"].asString() != "")         return 2;     if (JsonRoot["data"]["loginCheck"].isNull() || JsonRoot["data"]["loginCheck"].asString() != "Y")         return 1;

    return 0; }

这里还有个注意细节,就是通过POST请求发送的数据需要对一些符号做URL Encode,这个我在上一篇文章《从零实现一个http服务器》也详细做了介绍,还不清楚的可以参见上一篇文章。所以对于向图片验证码坐标信息中含有的逗号信息就要进行URL编码,从 answer=114,54,44,46&login_site=E&rand=sjrand 变成 answer=114%2C54%2C44%2C46&login_site=E&rand=sjrand 所以,在http包头中指定的Content-Length字段的值应该是编码后的字符串长度,而不是原始的长度,这个地方特别容易出错。

如果验证成功后,接下来就是查票和购票了。这里就不一一介绍了,所有的原理都是一样的,作者可以自行探索。当然,我已经将所有的接口都探索完了,并实现了,我这里贴一下吧:

/**  *@desc: 封装获取验证码、校验验证码、登录等12306各个请求的类,Client12306.h文件  *@author: zhangyl  *@date: 2017.01.17  */

#ifndef __CLIENT_12306_H__ #define __CLIENT_12306_H__

#include #include

using namespace std;

//车次类型 #define TRAIN_GC 0x00000001 #define TRAIN_D (0x00000001



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3