UI设计 薇晓朵数字商城

 找回密碼
 加入我們

C++開發12306自動搶票軟件實例教程

[複製鏈接]
yxqq 發表於 2019-1-14 19:33:27 | 顯示全部樓層 |閱讀模式
本文來自CSDN


寫在前面的話

每年逢年過節,一票難求讀者肯定不陌生。這篇文章,我們帶領讀者從零實現一款12306刷票軟件,其核心原理還是通過發送http請求模擬登錄12306網站的購票的過程,最後買到票。

關於http請求的格式和如何組裝http數據包給服務器發送請求,我們在上一篇文章《從零實現一個http服務器》中已經詳細介紹過了,如果還不明白的朋友可以去那篇文章看下。

鄭重申明一下:這裡介紹的技術僅供用於學習,不可用於惡意攻擊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如下:

  1. {
  2.     "validateMessagesShowId": "_validatorMessage",
  3.     "status": true,
  4.     "httpstatus": 200,
  5.     "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": "上海" } },
  6.     "messages": [],
  7.     "validateMessages": {} }
複製代碼
回復

使用道具 舉報

 樓主| yxqq 發表於 2019-1-14 19:38:58 | 顯示全部樓層

其中含有的余票信息在result節點中,這是一個數組。每個節點以|分割,我們可以格式化後顯示在自己的界面上:



我這裡做的界面比較簡陋,讀者如果有興趣可以做更精美的界面。我們列下這個請求發送的http數據包和應答包:

請求包:

  1. GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-20&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1
  2. Host: kyfw.12306.cn
  3. Connection: keep-alive
  4. Cache-Control: no-cache
  5. Accept: */*
  6. X-Requested-With: XMLHttpRequest
  7. If-Modified-Since: 0
  8. 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
  9. Referer: https://kyfw.12306.cn/otn/leftTicket/init
  10. Accept-Encoding: gzip, deflate, br
  11. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
  12. 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
  13. HTTP/1.1 200 OK
  14. Date: Sat, 19 May 2018 15:23:58 GMT
  15. Content-Type: application/json;charset=UTF-8
  16. Transfer-Encoding: chunked
  17. ct: C1_217_85_8
  18. Content-Encoding: gzip
  19. Age: 1
  20. X-Via: 1.1 houdianxin183:6 (Cdn Cache Server V2.0)
  21. Connection: keep-alive
  22. X-Dscp-Value: 0
  23. X-Cdn-Src-Port: 33963
  24. Cache-Control: no-cache, no-store
複製代碼


通過上一篇文章《從零實現一個http服務器》我們知道這是一個http GET請求,其中在url後面是請求附帶的參數:

  1. leftTicketDTO.train_date: 2018-05-20
  2. leftTicketDTO.from_station: SHH
  3. leftTicketDTO.to_station: BJP
  4. purpose_codes: ADULT
複製代碼


這四個參數分別是購票日期、出發站、到達站和票類型(這裡是成人票(普通票)),正好對應我們界面上的查詢信息:



但是,讀者可能會問,這裡的出發站和到達站分別是SHH和BJP,這些站點代碼,我如何獲得呢?因為只有知道這些站點編碼我才能自己購買指定出發站和到達站的火車票啊。如果您是一位細心的人,您肯定會想到,我們查票的時候再進入查票頁面,這些站點信息就已經有了,那麼可能是在這個查票頁面加載時,從服務器請求的站點信息,所以我們刷新下查票頁面,發現果然是這樣:



進入查票頁面之前,瀏覽器從https://kyfw.12306.cn/otn/resour ... tion_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/leftTi ... purpose_codes=ADULT

當然,這裡需要說明一下的就是,由於全國的火車站點信息文件比較大,我們程序解析起來時間較長,加上火車站編碼信息並不是經常變動,所以,我們我們沒必要每次都下載這個station_name.js,所以我在寫程序模擬這個請求時,一般先看本地有沒有這個文件,如果有就使用本地的,沒有才發http請求向12306服務器請求。這裡我貼下我請求站點信息的程序代碼(C++代碼):

  1. /**
  2.   * 獲取全國車站信息
  3.   * @param si 返回的車站信息
  4.   * @param bForceDownload 強制從網絡上下載,即不使用本地副本
  5.   */
  6. bool GetStationInfo(vector<stationinfo>& si, bool bForceDownload = false);
  7. #define URL_STATION_NAMES   "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"
  8. bool Client12306::GetStationInfo(vector<stationinfo>& si, bool bForceDownload/* = false*/)
  9. {  
  10.     FILE* pfile;
  11.     pfile = fopen("station_name.js", "rt+");
  12.     //文件不存在,則必須下載
  13.     if (pfile == NULL)
  14.     {
  15.         bForceDownload = true;
  16.     }
  17.     string strResponse;
  18.     if (bForceDownload)
  19.     {
  20.         if (pfile != NULL)
  21.             fclose(pfile);
  22.         pfile = fopen("station_name.js", "wt+");
  23.         if (pfile == NULL)
  24.         {
  25.             LogError("Unable to create station_name.js");
  26.             return false;
  27.         }

  28.         CURLcode res;
  29.         CURL* curl = curl_easy_init();
  30.         if (NULL == curl)
  31.         {
  32.             fclose(pfile);
  33.             return false;
  34.         }

  35.         //URL_STATION_NAMES
  36.         curl_easy_setopt(curl, CURLOPT_URL, URL_STATION_NAMES);
  37.         //響應結果中保留頭部信息
  38.         //curl_easy_setopt(curl, CURLOPT_HEADER, 1);
  39.         curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
  40.         curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
  41.         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
  42.         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
  43.         curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
  44.         //設定為不驗證證書和HOST
  45.         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);
  46.         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);

  47.         curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);
  48.         curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);

  49.         res = curl_easy_perform(curl);
  50.         bool bError = false;
  51.         if (res == CURLE_OK)
  52.         {
  53.             int code;
  54.             res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
  55.             if (code != 200)
  56.             {
  57.                 bError = true;
  58.                 LogError("http response code is not 200, code=%d", code);
  59.             }
  60.         }
  61.         else
  62.         {
  63.             LogError("http request error, error code = %d", res);
  64.             bError = true;
  65.         }

  66.         curl_easy_cleanup(curl);

  67.         if (bError)
  68.         {
  69.             fclose(pfile);
  70.             return !bError;
  71.         }

  72.         if (fwrite(strResponse.data(), strResponse.length(), 1, pfile) != 1)
  73.         {
  74.             LogError("Write data to station_name.js error");            
  75.             return false;
  76.         }
  77.         fclose(pfile);
  78.     }
  79.     //直接讀取文件
  80.     else
  81.     {
  82.         //得到文件大小
  83.         fseek(pfile, 0, SEEK_END);
  84.         int length = ftell(pfile);
  85.         if (length < 0)
  86.         {
  87.             LogError("invalid station_name.js file");
  88.             fclose(pfile);
  89.         }
  90.         fseek(pfile, 0, SEEK_SET);
  91.         length++;
  92.         char* buf = new char[length];
  93.         memset(buf, 0, length*sizeof(char));
  94.         if (fread(buf, length-1, 1, pfile) != 1)
  95.         {
  96.             LogError("read station_name.js file error");
  97.             fclose(pfile);
  98.             return false;
  99.         }
  100.         strResponse = buf;
  101.         fclose(pfile);
  102.     }


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

  106.     vector<string> singleStation;
  107.     split(strResponse, "@", singleStation);

  108.     size_t size = singleStation.size();
  109.     for (size_t i = 1; i < size; ++i)
  110.     {
  111.         vector<string> v;
  112.         split(singleStation[i], "|", v);
  113.         if (v.size() < 6)
  114.             continue;

  115.         stationinfo st;
  116.         st.code1 = v[0];
  117.         st.hanzi = v[1];
  118.         st.code2 = v[2];
  119.         st.pingyin = v[3];
  120.         st.simplepingyin = v[4];
  121.         st.no = atol(v[5].c_str());

  122.         si.push_back(st);
  123.     }

  124.     return true;
  125. }
複製代碼
回復 支持 反對

使用道具 舉報

 樓主| yxqq 發表於 2019-1-14 20:24:23 | 顯示全部樓層

這裡用了一個站點信息結構體stationinfo,定義如下:

  1. //var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2
  2. struct stationinfo {
  3.     string code1;
  4.     string hanzi;
  5.     string code2;
  6.     string pingyin;
  7.     string simplepingyin;
  8.     int no;
  9. };
複製代碼


因為我們這裡目的是為了模擬http請求做買火車票相關的操作,而不是技術方面本身,所以為了快速實現我們的目的,我們就使用curl庫。這個庫是一個強大的http相關的庫,例如12306服務器返回的數據可能是分塊的(chunked),這個庫也能幫我們組裝好;再例如,服務器返回的數據是使用gzip格式壓縮的,curl也會幫我們自動解壓好。所以,接下來的所有12306的接口,都基於我封裝的curl庫一個接口:

  1. /**
  2. * 發送一個http請求
  3. *@param url 請求的url
  4. *@param strResponse http響應結果
  5. *@param get true為GET,false為POST
  6. *@param headers 附帶發送的http頭信息
  7. *@param postdata post附帶的數據
  8. *@param bReserveHeaders http響應結果是否保留頭部信息
  9. *@param timeout http請求超時時間
  10. */
  11. bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10);
複製代碼


函數各種參數已經在函數註釋中寫的清清楚楚了,這裡就不一一解釋了。這個函數的實現代碼如下:

  1. bool Client12306::HttpRequest(const char* url,
  2.                               string& strResponse,
  3.                               bool get/* = true*/,
  4.                               const char* headers/* = NULL*/,
  5.                               const char* postdata/* = NULL*/,
  6.                               bool bReserveHeaders/* = false*/,
  7.                               int timeout/* = 10*/)
  8. {
  9.     CURLcode res;
  10.     CURL* curl = curl_easy_init();
  11.     if (NULL == curl)
  12.     {
  13.         LogError("curl lib init error");
  14.         return false;
  15.     }

  16.     curl_easy_setopt(curl, CURLOPT_URL, url);

  17.     //響應結果中保留頭部信息
  18.     if (bReserveHeaders)
  19.        curl_easy_setopt(curl, CURLOPT_HEADER, 1);
  20.     curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
  21.     curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
  22.     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
  23.     curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
  24.     curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
  25.     //設定為不驗證證書和HOST
  26.     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);
  27.     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);

  28.     //設置超時時間
  29.     curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout);
  30.     curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
  31.     curl_easy_setopt(curl, CURLOPT_REFERER, URL_REFERER);
  32.     //12306早期版本是不需要USERAGENT這個字段的,現在必須了,估計是為了避免一些第三方的非法刺探吧。
  33.     //如果沒有這個字段,會返回
  34.     /* 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 */
  35.     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");
  36.     //不設置接收的編碼格式或者設置為空,libcurl會自動解壓壓縮的格式,如gzip
  37.     //curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");


  38.     //添加自定義頭信息
  39.     if (headers != NULL)
  40.     {
  41.         //LogInfo("http custom header: %s", headers);
  42.         struct curl_slist *chunk = NULL;        
  43.         chunk = curl_slist_append(chunk, headers);      
  44.         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
  45.     }

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

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

  52.     res = curl_easy_perform(curl);
  53.     bool bError = false;
  54.     if (res == CURLE_OK)
  55.     {
  56.         int code;
  57.         res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
  58.         if (code != 200 && code != 302)
  59.         {
  60.             bError = true;
  61.             LogError("http response code is not 200 or 302, code=%d", code);
  62.         }
  63.     }
  64.     else
  65.     {
  66.         LogError("http request error, error code = %d", res);
  67.         bError = true;
  68.     }

  69.     curl_easy_cleanup(curl);

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

  71.    return !bError;
  72. }
複製代碼
回復 支持 反對

使用道具 舉報

 樓主| yxqq 發表於 2019-1-14 20:35:02 | 顯示全部樓層
正如上面註釋中所提到的,瀏覽器在發送http請求時帶的一些字段,我們不是必須的,如查票接口瀏覽器可能會發以下http數據包:

  1. GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1
  2. Host: kyfw.12306.cn
  3. Connection: keep-alive
  4. Cache-Control: no-cache
  5. Accept: */*
  6. X-Requested-With: XMLHttpRequest
  7. If-Modified-Since: 0
  8. 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
  9. Referer: https://kyfw.12306.cn/otn/leftTicket/init
  10. Accept-Encoding: gzip, deflate, br
  11. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
  12. 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請求數據包格式是:

  1. GET /passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.7520968747611347 HTTP/1.1
  2. Host: kyfw.12306.cn
  3. Connection: keep-alive
  4. 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
  5. Accept: image/webp,image/apng,image/*,*/*;q=0.8
  6. Referer: https://kyfw.12306.cn/otn/login/init
  7. Accept-Encoding: gzip, deflate, br
  8. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
  9. 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請求和應答包如下:

請求包:

  1. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
  2. Accept-Encoding: gzip, deflate, br
  3. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
  4. Cache-Control: max-age=0
  5. Connection: keep-alive
  6. 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
  7. Host: kyfw.12306.cn
  8. Referer: https://kyfw.12306.cn/otn/passport?redirect=/otn/login/loginOut
  9. Upgrade-Insecure-Requests: 1
  10. 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
複製代碼


應答包:

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


這個值在應答包字段Set-Cookie中拿到:

  1. Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn
複製代碼


所以,我們每次請求圖片驗證碼時,都重新請求一下這個JSESSIONID,代碼如下:

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

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

  15.     return true;
  16. }
  17. bool Client12306::GetCookies(const string& data)
  18. {
  19.     if (data.empty())
  20.     {
  21.         LogError("http data is empty");
  22.         return false;
  23.     }

  24.     //解析http頭部
  25.     string str;
  26.     str.append(data.c_str(), data.length());
  27.     size_t n = str.find("\r\n\r\n");
  28.     string header = str.substr(0, n);
  29.     str.erase(0, n + 4);

  30.     //m_cookie.clear();
  31.     //獲取http頭中的JSESSIONID=21AC68643BBE893FBDF3DA9BCF654E98;
  32.     vector<string> v;
  33.     while (true)
  34.     {
  35.         size_t index = header.find("\r\n");
  36.         if (index == string::npos)
  37.             break;
  38.         string tmp = header.substr(0, index);
  39.         v.push_back(tmp);
  40.         header.erase(0, index + 2);

  41.         if (header.empty())
  42.             break;
  43.     }

  44.     string jsessionid;
  45.     string BIGipServerotn;
  46.     string BIGipServerportal;
  47.     string current_captcha_type;
  48.     size_t m;
  49.     OutputDebugStringA("\nresponse http headers:\n");
  50.     for (size_t i = 0; i < v.size(); ++i)
  51.     {
  52.         OutputDebugStringA(v[i].c_str());
  53.         OutputDebugStringA("\n");
  54.         m = v[i].find("Set-Cookie: ");
  55.         if (m == string::npos)
  56.             continue;

  57.         string tmp = v[i].substr(11);
  58.         Trim(tmp);
  59.         m = tmp.find("JSESSIONID");
  60.         if (m != string::npos)
  61.         {
  62.             size_t comma = tmp.find(";");
  63.             if (comma != string::npos)
  64.                 jsessionid = tmp.substr(0, comma);
  65.         }

  66.         m = tmp.find("BIGipServerotn");
  67.         if (m != string::npos)
  68.         {
  69.             size_t comma = tmp.find(";");
  70.             if (comma != string::npos)
  71.                 BIGipServerotn = tmp.substr(m, comma);
  72.             else
  73.                 BIGipServerotn = tmp;
  74.         }

  75.         m = tmp.find("BIGipServerportal");
  76.         if (m != string::npos)
  77.         {
  78.             size_t comma = tmp.find(";");
  79.             if (comma != string::npos)
  80.                 BIGipServerportal = tmp.substr(m, comma);
  81.             else
  82.                 BIGipServerportal = tmp;
  83.         }

  84.         m = tmp.find("current_captcha_type");
  85.         if (m != string::npos)
  86.         {
  87.             size_t comma = tmp.find(";");
  88.             if (comma != string::npos)
  89.                 current_captcha_type = tmp.substr(m, comma);
  90.             else
  91.                 current_captcha_type = tmp;           
  92.         }
  93.     }

  94.     if (!jsessionid.empty())
  95.     {
  96.         m_strCookies = jsessionid;
  97.         m_strCookies += "; ";
  98.         m_strCookies += BIGipServerotn;
  99.         if (!BIGipServerportal.empty())
  100.         {
  101.             m_strCookies += "; ";
  102.             m_strCookies += BIGipServerportal;
  103.         }
  104.         m_strCookies += "; ";
  105.         m_strCookies += current_captcha_type;
  106.         return true;
  107.     }

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

  112. bool Client12306::DownloadVCodeImage(const char* module)
  113. {
  114.     if (module == NULL)
  115.     {
  116.         LogError("module is invalid");
  117.         return false;
  118.     }

  119.     //https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.06851784300754482
  120.     ostringstream osUrl;
  121.     osUrl << URL_GETPASSCODENEW;
  122.     osUrl << "?login_site=E&module=";
  123.     osUrl << module;
  124.     //購票驗證碼
  125.     if (strcmp(module, "passenger") != 0)
  126.     {
  127.         osUrl << "&rand=sjrand&";
  128.     }
  129.     //登錄驗證碼
  130.     else
  131.     {      
  132.         osUrl << "&rand=randp&";     
  133.     }
  134.     double d = rand() * 1.000000 / RAND_MAX;
  135.     osUrl.precision(17);
  136.     osUrl << d;

  137.     string strResponse;
  138.     string strCookie = "Cookie: ";
  139.     strCookie += m_strCookies;
  140.     if (!HttpRequest(osUrl.str().c_str(), strResponse, true, strCookie.c_str(), NULL, false, 10))
  141.     {
  142.         LogError("DownloadVCodeImage failed");
  143.         return false;
  144.     }

  145.     //寫入文件
  146.     time_t now = time(NULL);
  147.     struct tm* tblock = localtime(&now);
  148.     memset(m_szCurrVCodeName, 0, sizeof(m_szCurrVCodeName));
  149. #ifdef _DEBUG
  150.     sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.jpg",
  151. + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday,
  152.         tblock->tm_hour, tblock->tm_min, tblock->tm_sec);
  153. #else
  154.     sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.v",
  155. + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday,
  156.         tblock->tm_hour, tblock->tm_min, tblock->tm_sec);
  157. #endif

  158.     FILE* fp = fopen(m_szCurrVCodeName, "wb");
  159.     if (fp == NULL)
  160.     {
  161.         LogError("open file %s error", m_szCurrVCodeName);
  162.         return false;
  163.     }

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

  172.     fclose(fp);

  173.     return true;
  174. }
複製代碼


我們再看下驗證碼去服務器驗證的接口https://kyfw.12306.cn/passport/captcha/captcha-check

請求頭:

  1. POST /passport/captcha/captcha-check HTTP/1.1
  2. Host: kyfw.12306.cn
  3. Connection: keep-alive
  4. Content-Length: 50
  5. Accept: application/json, text/javascript, */*; q=0.01
  6. Origin: https://kyfw.12306.cn
  7. X-Requested-With: XMLHttpRequest
  8. 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
  9. Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  10. Referer: https://kyfw.12306.cn/otn/login/init
  11. Accept-Encoding: gzip, deflate, br
  12. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
  13. 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值:

  1. answer: 175,58,30,51
  2. login_site: E
  3. rand: sjrand
複製代碼


這裡我選擇了兩張圖片,所以有兩組坐標值,(175,58)是一組,(30,51)是另外一組,這個坐標系如下:



因為每個圖片的尺寸都一樣,所以,我可以給每個圖片設置一個坐標範圍,當選擇了一個圖片,給一個在其中的坐標即可,不一定是鼠標點擊時的準確位置:

  1. //刷新驗證碼 登錄狀態下的驗證碼傳入」randp「,非登錄傳入」sjrand「 具體參看原otsweb中的傳入參數
  2. struct VCodePosition
  3. {
  4.     int x;
  5.     int y;
  6. };

  7. const VCodePosition g_pos[] =
  8. {
  9.     { 39, 40 },
  10.     { 114, 43 },
  11.     { 186, 42 },
  12.     { 252, 47 },
  13.     { 36, 120 },
  14.     { 115, 125 },
  15.     { 194, 125 },
  16.     { 256, 120 }
  17. };

  18. //驗證碼圖片八個區塊的位置
  19. struct VCODE_SLICE_POS
  20. {
  21.     int xLeft;
  22.     int xRight;
  23.     int yTop;
  24.     int yBottom;
  25. };

  26. const VCODE_SLICE_POS g_VCodeSlicePos[] =
  27. {
  28.     {0,   70,  0,  70},
  29.     {71,  140, 0,  70 },
  30.     {141, 210, 0,  70 },
  31.     {211, 280, 0,  70 },
  32.     { 0,  70,  70, 140 },   
  33.     {71,  140, 70, 140 },
  34.     {141, 210, 70, 140 },
  35.     {211, 280, 70, 140 }
  36. };

  37. //8個驗證碼區塊的鼠標點擊狀態
  38. bool g_bVodeSlice1Pressed[8] = { false, false, false, false, false, false, false, false};
複製代碼


驗證的圖片驗證碼的接口代碼是:

  1. int Client12306::checkRandCodeAnsyn(const char* vcode)
  2. {
  3.     string param;
  4.     param = "randCode=";
  5.     param += vcode;
  6.     param += "&rand=sjrand";    //passenger:randp

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

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

  26.     //參數無效
  27.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":""},"messages":[],"validateMessages":{}}
  28.     //驗證碼過期
  29.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":"EXPIRED"},"messages":[],"validateMessages":{}}
  30.     //驗證碼錯誤
  31.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"FALSE"},"messages":[],"validateMessages":{}}
  32.     //驗證碼正確
  33.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"TRUE"},"messages":[],"validateMessages":{}}
  34.     Json::Reader JsonReader;
  35.     Json::Value JsonRoot;
  36.     if (!JsonReader.parse(strResponse, JsonRoot))
  37.         return -1;
  38.     //{"validateMessagesShowId":"_validatorMessage", "status" : true, "httpstatus" : 200, "data" : {"result":"1", "msg" : "TRUE"}, "messages" : [], "validateMessages" : {}}
  39.     if (JsonRoot["status"].isNull() || JsonRoot["status"].asBool() != true)
  40.         return -1;

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

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

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

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

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

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


  57.     return 1;
  58. }
複製代碼


同理,這裡也給出驗證用戶名和密碼的接口實現代碼:

  1. int Client12306::loginAysnSuggest(const char* user, const char* pass, const char* vcode)
  2. {
  3.     string param = "loginUserDTO.user_name=";
  4.     param += user;
  5.     param += "&userDTO.password=";
  6.     param += pass;
  7.     param += "&randCode=";
  8.     param += vcode;
  9.     string strResponse;
  10.     string strCookie = "Cookie: ";
  11.     strCookie += m_strCookies;
  12.     if (!HttpRequest(URL_LOGINAYSNSUGGEST, strResponse, false, strCookie.c_str(), param.c_str(), false, 10))
  13.     {
  14.         LogError("loginAysnSuggest failed");
  15.         return 2;
  16.     }

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

  28.     //郵箱不存在
  29.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["該郵箱不存在。"],"validateMessages":{}}
  30.     //密碼錯誤
  31.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["密碼輸入錯誤。如果輸錯次數超過4次,用戶將被鎖定。"],"validateMessages":{}}
  32.     //登錄成功
  33.     //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"otherMsg":"",loginCheck:"Y"},"messages":[],"validateMessages":{}}
  34.     //WCHAR* psz1 = Utf8ToAnsi(strResponse.c_str());
  35.     //wstring str = psz1;
  36.     //delete[] psz1;

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

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

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

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

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

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

  56.     return 0;
  57. }
複製代碼
回復 支持 反對

使用道具 舉報

 樓主| yxqq 發表於 2019-1-15 10:32:42 | 顯示全部樓層

這裡還有個注意細節,就是通過POST請求發送的數據需要對一些符號做URL Encode,這個我在上一篇文章《從零實現一個http服務器》也詳細做了介紹,還不清楚的可以參見上一篇文章。所以對於向圖片驗證碼坐標信息中含有的逗號信息就要進行URL編碼,從

  1. answer=114,54,44,46&login_site=E&rand=sjrand
複製代碼


變成

  1. answer=114%2C54%2C44%2C46&login_site=E&rand=sjrand
複製代碼


所以,在http包頭中指定的Content-Length字段的值應該是編碼後的字符串長度,而不是原始的長度,這個地方特別容易出錯。



如果驗證成功後,接下來就是查票和購票了。這裡就不一一介紹了,所有的原理都是一樣的,作者可以自行探索。當然,我已經將所有的接口都探索完了,並實現了,我這裡貼一下吧:

  1. /**
  2. *@desc: 封裝獲取驗證碼、校驗驗證碼、登錄等12306各個請求的類,Client12306.h文件
  3. *@author: zhangyl
  4. *@date: 2017.01.17
  5. */

  6. #ifndef __CLIENT_12306_H__
  7. #define __CLIENT_12306_H__

  8. #include <vector>
  9. #include <string>

  10. using namespace std;

  11. //車次類型
  12. #define TRAIN_GC 0x00000001
  13. #define TRAIN_D (0x00000001 << 1)
  14. #define TRAIN_Z (0x00000001 << 2)
  15. #define TRAIN_T (0x00000001 << 3)
  16. #define TRAIN_K (0x00000001 << 4)
  17. #define TRAIN_OTHER (0x00000001 << 5)
  18. #define TRAIN_ALL (TRAIN_GC | TRAIN_D | TRAIN_Z | TRAIN_T | TRAIN_K | TRAIN_OTHER)

  19. //票信息
  20. struct queryLeftNewDTO
  21. {
  22.     string  train_no;
  23.     string  station_train_code;
  24.     string  start_station_telecode;     //始發站
  25.     string  start_station_name;     
  26.     string  end_station_telecode;       //終點站
  27.     string  end_station_name;
  28.     string  from_station_telecode;      //出發站
  29.     string  from_station_name;          //到達站
  30.     string  to_station_telecode;
  31.     string  to_station_name;
  32.     string  start_time;
  33.     string  arrive_time;
  34.     string  day_difference;
  35.     string  train_class_name;
  36.     string  lishi;
  37.     string  canWebBuy;
  38.     string  lishiValue;
  39.     string  yp_info;
  40.     string  control_train_day;
  41.     string  start_train_date;
  42.     string  seat_feature;
  43.     string  yp_ex;
  44.     string  train_seat_feature;
  45.     string  seat_types;
  46.     string  location_code;
  47.     string  from_station_no;
  48.     string  to_station_no;
  49.     string  control_day;
  50.     string  sale_time;
  51.     string  is_support_card;
  52.     string  controlled_train_flag;
  53.     string  controlled_train_message;
  54.     string  train_type_code;
  55.     string  start_province_code;
  56.     string  start_city_code;
  57.     string  end_province_code;
  58.     string  end_city_code;

  59.     string  swz_num;    //商務座   
  60.     string  rz_num;     //軟座
  61.     string  yz_num;     //硬座

  62.     string  gr_num;     //高級軟臥
  63.     string  rw_num;     //軟臥
  64.     string  yw_num;     //硬臥

  65.     string  tz_num;     //特等座
  66.     string  zy_num;     //一等座
  67.     string  ze_num;     //二等座   
  68.     string  wz_num;     //無座

  69.     string  gg_num;
  70.     string  yb_num;   
  71.     string  qt_num;

  72. };

  73. struct ticketinfo
  74. {
  75.     queryLeftNewDTO DTO;
  76.     string secretStr;
  77.     string buttonTextInfo;
  78. };

  79. //var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2
  80. struct stationinfo
  81. {
  82.     string code1;
  83.     string hanzi;
  84.     string code2;
  85.     string pingyin;
  86.     string simplepingyin;
  87.     int no;
  88. };

  89. struct passenager
  90. {
  91.     string code;  //"8"
  92.     string passenger_name;  //"范蠡"
  93.     string sex_code;// "M"
  94.     string sex_name; // "男"
  95.     string born_date; //"1989-12-08 00:00:00"
  96.     string country_code;// "CN"
  97.     string passenger_id_type_code;//  "1"
  98.     string passenger_id_type_name; // "二代身份證"
  99.     string passenger_id_no; // "14262319781108815X"
  100.     string passenger_type; // "1"
  101.     string passenger_flag; // "0"
  102.     string passenger_type_name; // "成人"
  103.     string mobile_no; // "13917043320"
  104.     string phone_no;
  105.     string email; // "[email protected]"
  106.     string address; //  ""
  107.     string postalcode; // ""
  108.     string first_letter;// ""
  109.     string recordCount;// "13"
  110.     string total_times;// "99"
  111.     string index_id;// "0"
  112. };

  113. class Client12306
  114. {
  115. public:
  116.     static Client12306& GetInstance();

  117. private:
  118.     Client12306();
  119.     ~Client12306();

  120. private:
  121.     Client12306(const Client12306&);
  122.     Client12306& operator=(const Client12306&);

  123. public:
  124.     bool ReloadVCodeImage();

  125.     /**
  126.      * 遊客查票
  127.      * https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2017-05-24&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT
  128.      * 應答:{「validateMessagesShowId」:」_validatorMessage」,」status」:true,」httpstatus」:200,」messages」:[],」validateMessages」:{}}
  129.      *@param: train_date列車發車日期,格式:2017-01-28
  130.      *@param: from_station出發站,格式:SHH 對應上海
  131.      *@parma: to_station到站,格式:BJP 對應北京
  132.      *@param: purpose_codes 票類型,成人票:ADULT 學生票:0X00
  133.      *@param: v 查票結果
  134.      */
  135.     bool GuestQueryTicket(const char* train_date, const char* from_station, const char* to_station, const char* purpose_codes, vector<ticketinfo>& v);

  136.     /**
  137.     * 初始化session,獲取JSESSIONID
  138.     */
  139.     bool loginInit();
  140.     bool DownloadVCodeImage(const char* module = "login");
  141.     /**
  142.     *@return 0校驗成功;1校驗失敗;2校驗出錯
  143.     */
  144.     int checkRandCodeAnsyn(const char* vcode);
  145.     /**
  146.     *@return 0校驗成功;1校驗失敗;2校驗出錯
  147.     */
  148.     int loginAysnSuggest(const char* user, const char* pass, const char* vcode);

  149.     /**
  150.      * 正式登錄
  151.      */
  152.     bool userLogin();

  153.     /**
  154.      * 模擬12306跳轉
  155.      */
  156.     bool initMy12306();

  157.     /**
  158.      * 拉取乘客買票驗證碼
  159.      */
  160.     //bool GetVCodeImage();

  161.     /**
  162.      * 拉取乘客買票驗證碼
  163.      */

  164.     /**
  165.      * 查詢余票第一步
  166.      * https://kyfw.12306.cn/otn/leftTicket/log?leftTicketDTO.train_date=2017-02-08&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=NJH&purpose_codes=ADULT
  167.      * 應答:{「validateMessagesShowId」:」_validatorMessage」,」status」:true,」httpstatus」:200,」messages」:[],」validateMessages」:{}}
  168.      *@param: train_date列車發車日期,格式:2017-01-28
  169.      *@param: from_station出發站,格式:SHH 對應上海
  170.      *@parma: to_station到站,格式:BJP 對應北京
  171.      *@param: purpose_codes 票類型,成人票:ADULT 學生票:0X00
  172.      */
  173.     bool QueryTickets1(const char* train_date, const char* from_station, const char* to_station, const char* purpose_codes);

  174.     /**
  175.      * 查詢余票第二步
  176.      * 這幾種情形都有可能,所以應該都嘗試一下
  177.      * https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date=2017-02-08&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=NJH&purpose_codes=ADULT
  178.      * https://kyfw.12306.cn/otn/leftTicket/queryX?leftTicketDTO.train_date=2017-02-08&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=NJH&purpose_codes=ADULT
  179.      * https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2017-02-08&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=NJH&purpose_codes=ADULT
  180.      * {"status":false,"c_url":"leftTicket/query","c_name":"CLeftTicketUrl"}
  181.      * {"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"messages":["非法請求"],"validateMessages":{}}
  182.      * 應答中含有實際余票信息
  183.      *@param: train_date列車發車日期,格式:2017-01-28
  184.      *@param: from_station出發站,格式:SHH 對應上海
  185.      *@parma: to_station到站,格式:BJP 對應北京
  186.      *@param: purpose_codes 票類型,成人票:ADULT 學生票:0X00
  187.      */
  188.     bool QueryTickets2(const char* train_date, const char* from_station, const char* to_station, const char* purpose_codes, vector<ticketinfo>& v);

  189.     /**
  190.      * 檢測用戶是否登錄
  191.      * https://kyfw.12306.cn/otn/login/checkUser POST _json_att=
  192.      * Cookie: JSESSIONID=0A01D967FCD9827FC664E43DEE3C7C6EF950F677C2; __NRF=86A7CBA739653C1CC2C3C3AA7C88A1E3; BIGipServerotn=1742274826.64545.0000; BIGipServerportal=3134456074.17695.0000; current_captcha_type=Z; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5357%u4EAC%2CNJH; _jc_save_fromDate=2017-01-22; _jc_save_toDate=2017-01-22; _jc_save_wfdc_flag=dc
  193.      * {"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"flag":true},"messages":[],"validateMessages":{}}
  194.      */
  195.     bool checkUser();

  196.     /**
  197.      * 預提交訂單 POST
  198.      * https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest?secretStr=secretStr&train_date=2017-01-21&back_train_date=2016-12-23&tour_flag=dc&purpose_codes=ADULT&query_from_station_name=深圳&query_to_station_name=武漢&undefined=
  199.      */
  200.     bool submitOrderRequest(const char* secretStr, const char* train_date, const char* back_train_date, const char* tour_flag, const char* purpose_codes, const char* query_from_station_name, const char* query_to_station_name);

  201.     /**
  202.      * 模擬跳轉頁面InitDc,Post
  203.      */
  204.     bool initDc();

  205.     /**
  206.      * 拉取常用聯繫人 POST
  207.      * https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs?_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken
  208.      */
  209.     bool getPassengerDTOs(vector<passenager>& v);

  210.     /**
  211.      * 購票人確定
  212.      * https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo
  213.      @param oldPassengerStr oldPassengerStr組成的格式:乘客名,passenger_id_type_code,passenger_id_no,passenger_type,』_』
  214.                             示例: 范蠡,1,14262319781108815X,1_
  215.      @param passengerTicketStr passengerTicketStr組成的格式:seatType,0,票類型(成人票填1),乘客名,passenger_id_type_code,passenger_id_no,mobile_no,』N』
  216.                             示例: O,0,1,范蠡,1,14262319781108815X,13917043320,N    101
  217.      @tour_flag dc表示單程票
  218.      應答:{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"ifShowPassCode":"N","canChooseBeds":"N","canChooseSeats":"N","choose_Seats":"MOP9","isCanChooseMid":"N","ifShowPassCodeTime":"1","submitStatus":true,"smokeStr":""},"messages":[],"validateMessages":{}}
  219.      */
  220.     bool checkOrderInfo(const char* oldPassengerStr, const char* passengerTicketStr, const char* tour_flag, bool& bVerifyVCode);

  221.     /**
  222.      * 準備進入排隊
  223.      * https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount
  224.      _json_att        10
  225.      fromStationTelecode    VNP    23
  226.      leftTicket    enu80ehMzuVJlK2Q43c6kn5%2BzQF41LEI6Nr14JuzThrooN57    63
  227.      purpose_codes    00    16
  228.      REPEAT_SUBMIT_TOKEN    691c09b5605e46bfb2ec2380ee65de0e    52
  229.      seatType    O    10
  230.      stationTrainCode    G5    19
  231.      toStationTelecode    AOH    21
  232.      train_date    Fri Feb 10 00:00:00 UTC+0800 2017    50
  233.      train_location    P2    17
  234.      train_no    24000000G502    21
  235.      應答:{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"count":"4","ticket":"669","op_2":"false","countT":"0","op_1":"true"},"messages":[],"validateMessages":{}}
  236.      */
  237.     bool getQueueCount(const char* fromStationTelecode, const char* leftTicket, const char* purpose_codes, const char* seatType, const char* stationTrainCode, const char* toStationTelecode, const char* train_date, const char* train_location, const char* train_no);

  238.     /**
  239.      * 確認購買
  240.      * https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue
  241.      _json_att        10
  242.      choose_seats        13
  243.      dwAll    N    7
  244.      key_check_isChange    7503FD317E01E290C3D95CAA1D26DD8CFA9470C3643BA9799D3FB753    75
  245.      leftTicketStr    enu80ehMzuVJlK2Q43c6kn5%2BzQF41LEI6Nr14JuzThrooN57    66
  246.      oldPassengerStr    范蠡,1,14262319781108815X,1_    73
  247.      passengerTicketStr    O,0,1,范蠡,1,14262319781108815X,13917043320,N    101
  248.      purpose_codes    00    16
  249.      randCode        9
  250.      REPEAT_SUBMIT_TOKEN    691c09b5605e46bfb2ec2380ee65de0e    52
  251.      roomType    00    11
  252.      seatDetailType    000    18
  253.      train_location    P2    17
  254.      應答:{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"submitStatus":true},"messages":[],"validateMessages":{}}
  255.      */
  256.     bool confirmSingleForQueue(const char* leftTicketStr, const char* oldPassengerStr, const char* passengerTicketStr, const char* purpose_codes, const char* train_location);

  257.     /**
  258.      * 查詢訂單狀態: https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime?random=1486368851278&tourFlag=dc&_json_att=&REPEAT_SUBMIT_TOKEN=691c09b5605e46bfb2ec2380ee65de0e
  259.      GET
  260.      _json_att
  261.      random    1486368851278
  262.      REPEAT_SUBMIT_TOKEN    691c09b5605e46bfb2ec2380ee65de0e
  263.      tourFlag    dc
  264.      響應:{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"queryOrderWaitTimeStatus":true,"count":0,"waitTime":-1,"requestId":6234282826330508533,"waitCount":0,"tourFlag":"dc","orderId":"E061149209"},"messages":[],"validateMessages":{}}
  265.      */
  266.     bool queryOrderWaitTime(const char* tourflag, string& orderId);

  267.     /**
  268.      * https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue POST
  269.      _json_att        10
  270.      orderSequence_no    E061149209    27
  271.      REPEAT_SUBMIT_TOKEN    691c09b5605e46bfb2ec2380ee65de0e    52
  272.      {"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"submitStatus":true},"messages":[],"validateMessages":{}}
  273.      */
  274.     //bool resultOrderForDcQueue();

  275.     /**
  276.      * 未完成的訂單頁面 https://kyfw.12306.cn/otn/queryOrder/initNoComplete GET
  277.      * 獲取未完成的訂單 https://kyfw.12306.cn/otn/queryOrder/queryMyOrderNoComplete POST _json_att=
  278.      */
  279.     /*
  280.     {
  281.     "validateMessagesShowId": "_validatorMessage",
  282.     "status": true,
  283.     "httpstatus": 200,
  284.     "data": {
  285.         "orderDBList": [
  286.             {
  287.                 "sequence_no": "E079331507",
  288.                 "order_date": "2017-02-09 10:10:55",
  289.                 "ticket_totalnum": 1,
  290.                 "ticket_price_all": 55300,
  291.                 "cancel_flag": "Y",
  292.                 "resign_flag": "4",
  293.                 "return_flag": "N",
  294.                 "print_eticket_flag": "N",
  295.                 "pay_flag": "Y",
  296.                 "pay_resign_flag": "N",
  297.                 "confirm_flag": "N",
  298.                 "tickets": [
  299.                     {
  300.                         "stationTrainDTO": {
  301.                             "trainDTO": {},
  302.                             "station_train_code": "G41",
  303.                             "from_station_telecode": "VNP",
  304.                             "from_station_name": "北京南",
  305.                             "start_time": "1970-01-01 09:16:00",
  306.                             "to_station_telecode": "AOH",
  307.                             "to_station_name": "上海虹橋",
  308.                             "arrive_time": "1970-01-01 14:48:00",
  309.                             "distance": "1318"
  310.                         },
  311.                         "passengerDTO": {
  312.                             "passenger_name": "范蠡",
  313.                             "passenger_id_type_code": "1",
  314.                             "passenger_id_type_name": "二代身份證",
  315.                             "passenger_id_no": "14262319781108815X",
  316.                             "total_times": "98"
  317.                         },
  318.                         "ticket_no": "E079331507110008B",
  319.                         "sequence_no": "E079331507",
  320.                         "batch_no": "1",
  321.                         "train_date": "2017-02-11 00:00:00",
  322.                         "coach_no": "10",
  323.                         "coach_name": "10",
  324.                         "seat_no": "008B",
  325.                         "seat_name": "08B號",
  326.                         "seat_flag": "0",
  327.                         "seat_type_code": "O",
  328.                         "seat_type_name": "二等座",
  329.                         "ticket_type_code": "1",
  330.                         "ticket_type_name": "成人票",
  331.                         "reserve_time": "2017-02-09 10:10:55",
  332.                         "limit_time": "2017-02-09 10:10:55",
  333.                         "lose_time": "2017-02-09 10:40:55",
  334.                         "pay_limit_time": "2017-02-09 10:40:55",
  335.                         "ticket_price": 55300,
  336.                         "print_eticket_flag": "N",
  337.                         "resign_flag": "4",
  338.                         "return_flag": "N",
  339.                         "confirm_flag": "N",
  340.                         "pay_mode_code": "Y",
  341.                         "ticket_status_code": "i",
  342.                         "ticket_status_name": "待支付",
  343.                         "cancel_flag": "Y",
  344.                         "amount_char": 0,
  345.                         "trade_mode": "",
  346.                         "start_train_date_page": "2017-02-11 09:16",
  347.                         "str_ticket_price_page": "553.0",
  348.                         "come_go_traveller_ticket_page": "N",
  349.                         "return_deliver_flag": "N",
  350.                         "deliver_fee_char": "",
  351.                         "is_need_alert_flag": false,
  352.                         "is_deliver": "N",
  353.                         "dynamicProp": "",
  354.                         "fee_char": "",
  355.                         "insure_query_no": ""
  356.                     }
  357.                 ],
  358.                 "reserve_flag_query": "p",
  359.                 "if_show_resigning_info": "N",
  360.                 "recordCount": "1",
  361.                 "isNeedSendMailAndMsg": "N",
  362.                 "array_passser_name_page": [
  363.                     "范蠡"
  364.                 ],
  365.                 "from_station_name_page": [
  366.                     "北京南"
  367.                 ],
  368.                 "to_station_name_page": [
  369.                     "上海虹橋"
  370.                 ],
  371.                 "start_train_date_page": "2017-02-11 09:16",
  372.                 "start_time_page": "09:16",
  373.                 "arrive_time_page": "14:48",
  374.                 "train_code_page": "G41",
  375.                 "ticket_total_price_page": "553.0",
  376.                 "come_go_traveller_order_page": "N",
  377.                 "canOffLinePay": "N",
  378.                 "if_deliver": "N",
  379.                 "insure_query_no": ""
  380.             }
  381.         ],
  382.         "to_page": "db"
  383.     },
  384.     "messages": [],
  385.     "validateMessages": {}
  386. }
  387.     */

  388.     /**
  389.      * 已完成訂單(改/退) : https://kyfw.12306.cn/otn/queryOrder/queryMyOrder POST
  390.      * queryType 1 按訂票日期 2 按乘車日期
  391.      * 查詢日期queryStartDate=2017-02-09&queryEndDate=2017-02-09
  392.      * come_from_flag: my_order 全部 my_resign 可改簽 my_cs_resign 可變更到站 my_refund 可退票
  393.      * &pageSize=8&pageIndex=0&
  394.      * query_where G 未出行訂單 H 歷史訂單
  395.      * sequeue_train_name 訂單號/車次/乘客姓名
  396.      */
  397.     /* 歷史訂單格式
  398.        參見[歷史訂單.txt]
  399.     */

  400.     /**
  401.      * 獲取全國車站信息
  402.      *@param si 返回的車站信息
  403.      *@param bForceDownload 強制從網絡上下載,即不使用本地副本
  404.      */
  405.     bool GetStationInfo(vector<stationinfo>& si, bool bForceDownload = false);

  406.     /**
  407.      * 獲取所有高校信息 https://kyfw.12306.cn/otn/userCommon/schoolNames POST provinceCode=11&_json_att=
  408.      */

  409.     /**
  410.      * 獲取所有城市信息 https://kyfw.12306.cn/otn/userCommon/allCitys POST station_name=&_json_att=
  411.      */

  412.     /**
  413.      * 查詢常用聯繫人
  414.      */
  415.     bool QueryPassengers(int pageindex = 2, int pagesize = 10);


  416.     bool GetVCodeFileName(char* pszDst, int nLength);

  417. private:
  418.     bool GetCookies(const string& data);

  419.     /**
  420.      * 發送一個http請求
  421.      *@param url 請求的url
  422.      *@param strResponse http響應結果
  423.      *@param get true為GET,false為POST
  424.      *@param headers 附帶發送的http頭信息
  425.      *@param postdata post附帶的數據
  426.      *@param bReserveHeaders http響應結果是否保留頭部信息
  427.      *@param timeout http請求超時時間
  428.      */
  429.     bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10);

  430. private:
  431.     char                m_szCurrVCodeName[256]; //當前驗證碼圖片的名稱
  432.     string              m_strCookies;
  433.     string              m_strGlobalRepeatSubmitToken;
  434.     string              m_strKeyCheckIsChange;
  435. };


  436. #endif //!__CLIENT_12306_H__
複製代碼


具體的實現代碼就不在文章中貼出來了,您可以下載我的代碼。下載地址在微信公眾號『easyserverdev』中回復『12306源碼』即可得到下載地址,當然,由於12306的接口經常發生改變,當你拿到代碼時,可能12306服務器的接口已經稍微發生了改變,您可以按上面介紹的原理做響應的修改。

最後當您實現了基本的登錄和購票功能後,你就可以不斷模擬某些請求去進行刷票了。
回復 支持 反對

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 加入我們

本版積分規則

备案权重域名预定

點基跨境

GMT+8, 2025-1-23 03:14

By DZ X3.5

QQ

快速回復 返回頂部 返回列表