前段時間我發布過一篇文章描述Socket進行HTTP/HTTPS操作,但是還是很多朋友覺得多次一舉,放着簡單的HttpWebRequest不用!
實際是有些人根本沒看文章就亂說了,我們的目地是提高訪問速率,了解HTTP協議與一般網絡開發,並非奔着簡單去!
正好這年末,大家搶火車票搶的火熱,但是我們很多程序員朋友卻只沒有應用好自己的專業知識為自己購得回家的車票。
網上已經不乏一些搶票軟件、以及對12306流程分析的文章,
google code中也已經有了java版的全自動購票程序,自動AI+OCR識別的。有興趣的可以去搜索下看看。
回到文章中來,我這次也是通過編寫一下購票軟件來實踐下上次文章中的內容:
(http://www.cnblogs.com/cxwx/archive/2011/10/25/2224105.html)
下面的這款小軟件還沒有完全完成,軟件是WPF縮寫,基本的MVVM模式(有需要學習MVVM的也可以參考學習下)
下面就按照截圖走流程+編碼說明:
一:登錄
我們知道,首次請求一個頁面,服務端會為客戶端創建一個Session來保存客戶端狀態,我們通常在登錄頁面提交信息成功后,服務端會在其他頁面跳轉之后判斷
Seesion是否已經成功登錄。
對於存在驗證碼的網站,其實請求圖片與請求頁面沒有什么區別,如果是首次請求那么HTTP返回協議一般都帶附帶Cookies信息,即Set-Cookies 信息!
對於12306網站來說,我們的登錄頁面只要請求驗證碼圖片即可,因為這個請求的HTTP返回協議會包含服務端為本次請求創建的Cookies,
如果您利用上一篇文章講解的方法來請求,你會發現請求返回協議類似如下:
1 Response Headers Value
2 (Status-Line) HTTP/1.1 200 OK
3 Date Sun, 08 Jan 2012 23:57:29 GMT
4 Server asfep/2.3.0 svn:3075
5 Content-Type text/html;charset=UTF-8
6 Transfer-Encoding chunked
7 X-Powered-By Servlet 2.5; JBoss-5.0/JBossWeb-2.1
8 Pragma no-cache
9 Cache-Control no-cache
10 Expires Wed, 31 Dec 1969 23:59:59 GMT
11 Content-Encoding gzip
12 X-Cache MISS from cache.51cdn.com
13 X-Via 1.1 hbts205:8090 (Cdn Cache Server V2.0)
14 Connection keep-alive
在這段返回頭中,你會發現不存在Content-Length協議頭,那我們如何知道返回的數據包長度呢;
如果你善於google你就會知道,這種反回是因為Keep-Live的存在 服務器使用chunked方式分多次返回數據的結果;既然服務端還是會返回數據,只是分了多次!(我們知道瀏覽器能夠正常解析哦,所以別小看瀏覽器內核哦,很強大滴~~~~)
我們首要先來解決對於反回數據的解析:接着上篇文章中的HttpHelper,大家找到如下代碼段:看看修改后的代碼;
static byte[] ReadResponse(Stream sm) { DateTime now = DateTime.Now; ArrayList array = new ArrayList(); int length = 0; while (true) { byte[] buff = new byte[1024]; try { int len = sm.Read(buff, 0, buff.Length); if (len > 0) { length += len; byte[] reads = new byte[len]; Array.Copy(buff, 0, reads, 0, len); array.Add(reads); if (len < buff.Length) { break; //fix bug } } else { break; //fix bug } } catch (Exception) { break; } finally { Thread.Sleep(200); } } byte[] bytes = new byte[length]; int index = 0; for (int i = 0; i < array.Count; i++) { byte[] temp = (byte[])array[i]; Array.Copy(temp, 0, bytes, index, temp.Length); index += temp.Length; } return bytes; }
很簡單,其實就是多次請求,直到請求異常或者實際請求數據小於緩沖區大小就OK啦(注意兩處 fix bug)
這樣,我們首先解決了解析服務端反回數據的問題,緊接着我們娶到了登錄頁面的驗證碼圖片, 如果你現在抓包看下,你又會發現,抓包的數據就是圖片數據,
而我們使用Socket請求來的數據,多出了一段數據頭,呵呵,明眼的你注意,就會發現多出的這段數據頭還包含真實數據的長度!
如圖中紅色區域;
我這里呢沒有通過解析這個長度去提取真實數據,而是通過遍歷出JPEG圖片數據頭來截取數據的,代碼如下:
1 /// <summary>
2 /// JFIF數據頭
3 /// </summary>
4 static readonly byte[] JFIF = new byte[] { 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46 };
5
6 public static Stream GetVerifyImage()
7 {
8 IPEndPoint endpoint = GetHost();
9 RequestArgs request=new RequestArgs()
10 {
11 Url = "/otsweb/passCodeAction.do?rand=lrand",
12 Host="dynamic.12306.cn",
13 Accept="image/*",
14 Referer="https://dynamic.12306.cn/otsweb/loginAction.do?method=init",
15 };
16 m_Response=HttpHelper.Get(endpoint,request,m_X509);
17 if(m_Response!=null)
18 {
19 if(m_Response.Header.StartsWith("HTTP/1.1 200"))
20 {
21 //先校正保存cookies
22 AdjustCookies();
23
24 //由於HTTP返回協議使用了 chunked,所以要手動去分析實際需要的內容
25 if (m_Response.Body != null && m_Response.Body.Length > 10)
26 {
27 int index = 0;
28 byte[] head = new byte[10];
29 while (index < m_Response.Body.Length - 10)
30 {
31 Array.Copy(m_Response.Body,index,head,0,head.Length);
32 if (head.SequenceEqual(JFIF))
33 {
34 byte[] buff = new byte[m_Response.Body.Length-index];
35 Array.Copy(m_Response.Body,
36 index,
37 buff,0,buff.Length);
38 return new MemoryStream(buff);
39 }
40 index++;
41 }
42 }
43 }
44 }
45 return null;
46 }
上面這段代碼也是我們購票程序登錄頁面的第一個服務代碼; 注意看對Repose的解析部分,根據JPEG圖片頭進行數據解析,
讀到這里,各位看官應該了解了如果使用上次文章中的HttpHelper,做一個簡單的HTTPS請求,當然這個請求是通過Socket完成的,這里我就不再解釋了!
OK,說了一些關於第一步請求的東西,接着我們先看下MVVM模式的簡單應用,
我們的Login頁面是一個UserCOntrol,整個購票程序是一個Window 當中是一個Frame,通過頁面跳轉來指導運行的,
Login頁面綁定LoginViewModel,注意看,很多朋友不知道MVVM模式中ViewModel怎么和View交互,這里你可看清楚了哦!
1 <UserControl.DataContext>
2 <vm:LoginViewModel Loaded="LoginViewModel_Loaded"
3 Loading="LoginViewModel_Loading"
4 Logined="LoginViewModel_Logined"
5 VerifyImageCompleted="LoginViewModel_VerifyImageCompleted" />
6 </UserControl.DataContext>
沒錯,事件!!! 就是痛過事件,我們簡單的讓ViewModel與View進行交互。 當然這並不是唯一的方法,方法很多,這里我就不多講了,有興趣的自己看下Prsim以及Service共享和,ICO相關知識!
至於您看到的這里,為什么會有三個事件,我需要解釋下,Logined事件無疑是登錄成功事件,其余兩個看名字判斷應該是一個登錄過程提示的事件,
我這樣做的目地是希望后台全部采用異步請求,避免程序體驗效果不好,在MVVM模式中,該VM干的事就給VM干,該View干的那就讓View去干吧!
OK,請求到了驗證碼圖片,緊接着填寫帳號信息,提交登錄請求,判斷返回結果,一步帶過:通過ViewModel來看吧!
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using SimpleMvvmToolkit;
6 using System.Windows.Input;
7 using System.Windows.Media.Imaging;
8 using BuyTicket.Service;
9 using System.IO;
10 using System.Diagnostics;
11 using System.ComponentModel;
12 using System.Windows;
13
14 namespace BuyTicket.ViewModels
15 {
16 class LoginViewModel : ViewModelBase<LoginViewModel>
17 {
18 public LoginViewModel()
19 {
20
21 }
22
23 event EventHandler<NotificationEventArgs> m_Loading;
24 public event EventHandler<NotificationEventArgs> Loading
25 {
26 add
27 {
28 m_Loading += value;
29 }
30 remove
31 {
32 m_Loading -= value;
33 }
34 }
35
36 event EventHandler<NotificationEventArgs> m_Loaded;
37 public event EventHandler<NotificationEventArgs> Loaded
38 {
39 add
40 {
41 m_Loaded += value;
42 }
43 remove
44 {
45 m_Loaded -= value;
46 }
47 }
48
49
50 event EventHandler<NotificationEventArgs> m_Logined;
51 public event EventHandler<NotificationEventArgs> Logined
52 {
53 add
54 {
55 m_Logined += value;
56 }
57 remove
58 {
59 m_Logined -= value;
60 }
61 }
62
63 event EventHandler<NotificationEventArgs<BitmapImage>> m_VerifyImageCompleted;
64 public event EventHandler<NotificationEventArgs<BitmapImage>> VerifyImageCompleted
65 {
66 add
67 {
68 m_VerifyImageCompleted += value;
69 }
70 remove
71 {
72 m_VerifyImageCompleted -= value;
73 }
74 }
75
76 ICommand m_LoginCommand = null;
77 public ICommand LoginCommand
78 {
79 get
80 {
81 return m_LoginCommand ?? (m_LoginCommand = new DelegateCommand(Login));
82 }
83 }
84
85 bool m_NotLogined = false;
86
87 void Login()
88 {
89 if(string.IsNullOrEmpty(m_UserName)
90 || string.IsNullOrEmpty(m_PassWord)
91 || string.IsNullOrEmpty(m_VerifyCode))
92 {
93 MessageBox.Show("請填寫完整的登錄數據!", "錯誤提示",
94 MessageBoxButton.OK, MessageBoxImage.Error);
95 }
96 else
97 {
98 this.Async(new Func<object>(() =>
99 {
100 string error;
101 bool logined = BuyTicketService.Login(m_UserName, m_PassWord, m_VerifyCode, out error);
102 if (!logined)
103 {
104 m_NotLogined = true;
105 if (!string.IsNullOrEmpty(error))
106 {
107 MessageBox.Show(error, "錯誤提示",
108 MessageBoxButton.OK, MessageBoxImage.Error);
109 }
110 }
111 return logined;
112 }), new Action<object>(args =>
113 {
114 bool logined = (bool)args;
115 if (logined)
116 {
117 if (m_Logined != null)
118 {
119 m_Logined(this, new NotificationEventArgs());
120 }
121 }
122 else
123 {
124 RefreshVerifyCode(); //注意:此過程中可能造成View中Loading狀態不一致
125 }
126 }));
127 }
128 }
129
130 ICommand m_RefreshVerifyCodeCommand = null;
131 public ICommand RefreshVerifyCodeCommand
132 {
133 get
134 {
135 return m_RefreshVerifyCodeCommand ?? (m_RefreshVerifyCodeCommand = new DelegateCommand(RefreshVerifyCode));
136 }
137 }
138
139 void RefreshVerifyCode()
140 {
141 this.Async(new Func<object>(() =>
142 {
143 return BuyTicketService.GetVerifyImage();
144
145 /* 過時
146 var sm = BuyTicketService.GetVerifyImage();
147 if (sm != null)
148 {
149 try
150 {
151 using (FileStream fs = new FileStream(@"d:\\temp.png", FileMode.Create))
152 {
153 sm.CopyTo(fs);
154 fs.Close();
155 }
156
157 System.Drawing.Bitmap.FromStream(sm, true)
158 .Save(@"d:\\temp.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
159
160 BitmapImage image = new BitmapImage();
161 image.BeginInit();
162 image.StreamSource = sm; //sm引用來自BackgroundWorker線程
163 image.EndInit();
164 return image;
165 }
166 catch (Exception ex)
167 {
168 Trace.WriteLine(ex);
169 }
170 }
171 return null;
172 */
173 }), new Action<object>((args) =>
174 {
175 BitmapImage image = null;
176 Stream sm = args as Stream;
177 if (sm != null)
178 {
179 try
180 {
181 image = new BitmapImage();
182 image.BeginInit();
183 image.StreamSource = sm; //sm引用來自BackgroundWorker線程
184 image.EndInit();
185 }
186 catch (Exception ex)
187 {
188 Trace.WriteLine(ex);
189 }
190 }
191 //通知到View
192 if (m_VerifyImageCompleted != null)
193 {
194 m_VerifyImageCompleted(this,
195 new NotificationEventArgs<BitmapImage>("", image));
196 }
197 this.VerifyCode = "";
198 /*
199 BitmapImage image = args as BitmapImage;
200 if (image != null)
201 {
202 if (m_VerifyImageCompleted != null)
203 {
204 m_VerifyImageCompleted(this,
205 new NotificationEventArgs<BitmapImage>("", image));
206 }
207 }*/
208 }));
209
210 }
211
212 void Async(Func<object> func, Action<object> completed)
213 {
214 using (BackgroundWorker worker = new BackgroundWorker())
215 {
216 if (m_Loading != null)
217 {
218 m_Loading(this, new NotificationEventArgs());
219 }
220 worker.DoWork += (sender, e) =>
221 {
222 e.Result = func();
223 };
224 worker.RunWorkerCompleted += (sender, e) =>
225 {
226 completed(e.Result);
227 if (m_NotLogined==false) //登錄失敗時不通知Loaded事件
228 {
229 if (m_Loaded != null)
230 {
231 m_Loaded(this, new NotificationEventArgs());
232 }
233 }else
234 m_NotLogined = false; //重新標記
235 };
236 worker.RunWorkerAsync();
237 }
238 }
239
240 string m_UserName;
241 public string UserName
242 {
243 get
244 {
245 return m_UserName;
246 }
247 set
248 {
249 m_UserName = value;
250 this.NotifyPropertyChanged(m => m.UserName);
251 }
252 }
253
254 string m_PassWord;
255 public string PassWord
256 {
257 get
258 {
259 return m_PassWord;
260 }
261 set
262 {
263 m_PassWord = value;
264 this.NotifyPropertyChanged(m => m.PassWord);
265 }
266 }
267
268 string m_VerifyCode;
269 public string VerifyCode
270 {
271 get { return m_VerifyCode; }
272 set
273 {
274 m_VerifyCode = value;
275 this.NotifyPropertyChanged(m => m.VerifyCode);
276 }
277 }
278 }
279 }
相應的View就不展示出來了,需要的自己在文章末下載代碼回去看吧!對於登錄的過程,這里我不想細說,這里是演示上篇文章的通過Socket進行HTTP操作,
並不是延時如何抓包,如何模擬HTTP請求,有興趣的可自己去查閱相關資料!
登錄成功后,我們需要先填寫購票信息,界面設計為:
請原諒我的美工水平哦,俺可不是專業的, 在這個頁面中,起始站點信息是我從12306的js上搞下來的,說實話,最近這站被人詬病很多,
我也不乏詬病一下,但也不是非常差,我想沒有點墨水可不是誰都能搞的了的。
這里還提供了一項功能就是跳轉至瀏覽器, 其實登錄成功后,Cookies信息我們也就娶到了,這時候通過瀏覽器打開12306再編輯其cookies也可以完成直接登錄,
至於我后來添加“跳轉至瀏覽器”是因為這款軟件並沒有完全完成,給需要使用的普通用戶提供一個購票入口而已!
從當前頁面進行下一步,就會根據參數提交到服務器查詢余票信息,具體的提交服務展示一下,看看如果您抓取到了數據包您會是如何請求呢,
1 public static bool Login(string name, string pass, string verifyCode, out string error)
2 {
3 error = string.Empty;
4 IPEndPoint endpoint = GetHost();
5 RequestArgs request=new RequestArgs()
6 {
7 Url = "/otsweb/loginAction.do?method=login",
8 Host="dynamic.12306.cn",
9 Accept="text/*",
10 Referer="https://dynamic.12306.cn/otsweb/loginAction.do?method=init",
11 Cookie = m_Cookies,
12 Body = string.Format("loginUser.user_name={0}&nameErrorFocus=&user.password={1}&passwordErrorFocus=&randCode={2}&randErrorFocus=",
13 name,pass,verifyCode)
14 };
15 m_Response=HttpHelper.Post(endpoint,request,m_X509);
16 if (m_Response != null)
17 {
18 if (m_Response.Header.StartsWith("HTTP/1.1 200"))
19 {
20 if (m_Response.Body != null)
21 {
22 string strHtml = Encoding.UTF8.GetString(m_Response.Body);
23 if (strHtml.Contains("我的信息") && strHtml.Contains("用戶注銷"))
24 {
25 return true;
26 }
27 else if (strHtml.Contains("當前訪問用戶過多,請稍后重試!"))
28 {
29 error = "當前訪問用戶過多,請稍后重試!";
30 }
31 else if (strHtml.Contains("請輸入正確的驗證碼!</span>"))
32 {
33 error = "請輸入正確的驗證碼!";
34 }
35 else
36 {
37 error = "各種登錄失敗,兄弟,理解萬歲吧!";
38 }
39 }
40 }
41 else
42 {
43 error = "HTTP響應錯誤:"+Utility.SplitString(m_Response.Header,
44 "HTTP/1.1 ",
45 " ");
46 }
47 }
48 else
49 {
50 error = "超時,主機沒有反映!";
51 }
52 return false;
53 }
54
55 static void AdjustCookies()
56 {
57 m_Cookies = HttpHelper.ParseCookies(m_Response.Header);
58 }
59
60 public static bool GetTickets(out string strHtml)
61 {
62 strHtml = string.Empty;
63 IPEndPoint endpoint = GetHost();
64 RequestArgs request=new RequestArgs()
65 {
66 Url = string.Format("/otsweb/order/querySingleAction.do?{0}",
67 Global.MyTicketArgs.ToString()),
68 Host="dynamic.12306.cn",
69 Accept="text/*",
70 Referer = "https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=init",
71 Cookie = m_Cookies,
72 };
73 m_Response=HttpHelper.Get(endpoint,request,m_X509);
74 if (m_Response != null)
75 {
76 if (m_Response.Header.StartsWith("HTTP/1.1 200"))
77 {
78 if (m_Response.Body != null)
79 {
80 strHtml = Encoding.UTF8.GetString(m_Response.Body);
81 return true;
82 }
83 }
84 }
85 return false;
86 }
忘記補充一點,購票信息我是存放在全局中了,服務是靜態服務;從上面2個服務中您也能夠看出對上篇文章中HttpHelper的使用,當時文章中的這個輔助類並不完善,有些BUG存在,目前我也只是簡單修復了下BUG,有需要的還是需要自己去試試調試修改哦!
OK ,訂票參數提交后,就是訂票信息板的展示,這里只展示有余票信息的列車,抓包發現查詢參數提交后服務器返回如下類型數據:
1 0,<span id='id_240000T1090C' class='base_txtdiv' onmouseover=javascript:onStopHover('240000T1090C#BJP#SHH') onmouseout='onStopOut()'>T109</span>,<img src='/otsweb/images/tips/first.gif'> 北京 <br> 19:28,<img src='/otsweb/images/tips/last.gif'> 上海 <br> 10:25,14:57,--,--,--,--,9,4,5,--,<font color='darkgray'>無</font>,<font color='darkgray'>無</font>,--,<input type='button' class='yuding_u' onmousemove=this.className='yuding_u_over' onmousedown=this.className='yuding_u_down' onmouseout=this.className='yuding_u' onclick=javascript:getSelected('T109#14:57#19:28#240000T1090C#BJP#SHH#10:25#北京#上海#10179000003031700005404990000460921000091017903000') value='預訂'></input>\n
2 1,<span id='id_240000D31100' class='base_txtdiv' onmouseover=javascript:onStopHover('240000D31100#BJP#SHH') onmouseout='onStopOut()'>D311</span>,<img src='/otsweb/images/tips/first.gif'> 北京 <br> 20:52,<img src='/otsweb/images/tips/last.gif'> 上海 <br> 08:40,11:48,--,--,--,<font color='darkgray'>無</font>,--,<font color='#008800'>有</font>,--,--,--,--,--,<input type='button' class='yuding_u' onmousemove=this.className='yuding_u_over' onmousedown=this.className='yuding_u_down' onmouseout=this.className='yuding_u' onclick=javascript:getSelected('D311#11:48#20:52#240000D31100#BJP#SHH#08:40#北京#上海#4069800047O031100000') value='預訂'></input>\n
3 2,<span id='id_240000D32110' class='base_txtdiv' onmouseover=javascript:onStopHover('240000D32110#BJP#SHH') onmouseout='onStopOut()'>D321</span>,<img src='/otsweb/images/tips/first.gif'> 北京 <br> 20:58,<img src='/otsweb/images/tips/last.gif'> 上海 <br> 08:46,11:48,--,--,--,<font color='#008800'>有</font>,--,<font color='#008800'>有</font>,--,--,--,--,--,<input type='button' class='yuding_u' onmousemove=this.className='yuding_u_over' onmousedown=this.className='yuding_u_down' onmouseout=this.className='yuding_u' onclick=javascript:getSelected('D321#11:48#20:58#240000D32110#BJP#SHH#08:46#北京#上海#4069800096O031100046') value='預訂'></input>\n
4 3,<span id='id_240000D31310' class='base_txtdiv' onmouseover=javascript:onStopHover('240000D31310#VNP#SHH') onmouseout='onStopOut()'>D313</span>,<img src='/otsweb/images/tips/first.gif'> 北京南 <br> 21:11,<img src='/otsweb/images/tips/last.gif'> 上海 <br> 08:52,11:41,--,--,--,<font color='darkgray'>無</font>,16,<font color='#008800'>有</font>,--,--,--,--,--,<input type='button' class='yuding_u' onmousemove=this.className='yuding_u_over' onmousedown=this.className='yuding_u_down' onmouseout=this.className='yuding_u' onclick=javascript:getSelected('D313#11:41#21:11#240000D31310#VNP#SHH#08:52#北京南#上海#40698002416139200016O031100000') value='預訂'></input>\n
5 4,<span id='id_330000T28407' class='base_txtdiv' onmouseover=javascript:onStopHover('330000T28407#BXP#SNH') onmouseout='onStopOut()'>T281</span>, 北京西 <br> 22:30, 上海南 <br> 13:24,14:54,--,--,--,--,--,<font color='darkgray'>無</font>,<font color='darkgray'>無</font>,--,<font color='darkgray'>無</font>,<font color='darkgray'>無</font>,--,<input type='button' class='yuding_x' value='預訂'></input>
對於這種Html格式數據的分析,有過這方面的經驗的同學可能會想到HtmlAgilityPack ,沒錯,這是一款非常不錯並且強大的分析組件,如果您還沒用過,不妨去codeplex上看看哦!
有了HtmlAgilityPack,那上面的數據分析就很簡單啦:我們看下信息板View的ViewModel中的相關代碼就很清楚了:
1 void Refresh()
2 {
3 this.Async(new Func<object>(() =>
4 {
5 List<Ticket> result = new List<Ticket>();
6 string strHtml;
7 bool geted = BuyTicketService.GetTickets(out strHtml);
8 if (geted)
9 {
10 //通過strHtml解析出Ticket
11 string[] sArry = Regex.Split(strHtml, @"\\n");
12 foreach (var strLine in sArry)
13 {
14 HtmlDocument document = new HtmlDocument();
15 document.LoadHtml(strLine);
16 var node = document.DocumentNode.SelectSingleNode("//input[@type='button']");
17 if (node != null)
18 {
19 var attribute = node.Attributes["onclick"];
20 if (attribute != null)
21 {
22 string temp = Utility.SplitString(attribute.Value,
23 "'",
24 "'");
25 sArry = Regex.Split(temp, "#");
26
27 result.Add(new Ticket(sArry[0],
28 string.Format("{0}/{1}",sArry[7],sArry[2]),
29 string.Format("{0}/{1}",sArry[8],sArry[6]),
30 temp));
31 }
32 }
33 }
34 }
35 return result;
36 }), new Action<object>(args =>
37 {
38 List<Ticket> tickets = args as List<Ticket>;
39 if (tickets != null)
40 {
41 m_Tickets = tickets;
42 this.NotifyPropertyChanged(m => m.Tickets);
43 }
44 }));
45 }
當然,這個頁面的數據請求顯示也必須要異步哦, 而且最好向我做的這樣,有個定時器間隔時間刷新,咱就算買不到票,咱看看這票源信息也是個滿足么,呵呵!
開玩笑了,主要哥哥我今年不回家,要去丈母娘加過年咯,所以俺也不擔心買票問題, 各位回家的朋友還是要上點心哦,
我寫這文章 放出程序不是為了給您回家買票,俺想 您回家不需要了,回來總還要買票吧,看看文章,學點東西,自己再去完善下,你也就沒白當程序員哦!
最后一點,點擊預定后的界面、:
做到這里我就沒做下去了,一來我沒什么時間,都是早上早點去公司擠點,晚上下班搞搞,上個周末2天還把U盤丟公司浪費了2天。
做到這里也主要是破12306太卡, 抓不到最后的數據,我也就不着急去完善了, 在前面2頁添加了個“跳轉至瀏覽器” 通過瀏覽器打開登錄成功的頁面解燃眉之需!
最后附上完整項目:有時間有興趣的可繼續完善,我有時間也會寫完,也算是個做個這東西的一個交代; 最好希望咱天朝火車有朝一日不用搶票!
代碼:
/Files/cxwx/BuyTicket.zip 可能引用的組件有點大,我只包含了項目,用到的組件都是nuget來的,缺少第三方組件的自己nuget下
最后補充下:SimpleMVVM這框架Command上有個BUG,它NND