.NET基础示例系列之二十三:WebRequest、WebResponse及刷票程序

 

卧佛寺畔寻常路,萼新绿,香如故。
东君闲情有几许?犹寒燕赵,早春浪屿,南北各风物。
红墙紫禁春寒处,最是艰难返乡路。
羡煞东风无束缚,江南江北,无凭鳞羽,一夜即飞度。

送上一首以前写的〈青玉案〉,年年这个时节,总要为火车票之事焦头烂额,尤其像我这样路途遥远的,正常途径买票鲜有成功之例,往年有两个方法,一是找黄牛,然可靠的黄牛甚少,加价甚狠,十分不给力;二是网上搜索转让的车票。

其实网上转让的火车票不少,但是因为时效性的关系,需要你频繁刷新,常常是刚发布的转让信息,一分钟不到即被人抢走。尝夜间刷票5小时,才抢得一张硬卧。夜里刷票刷到眼冒金星时,曾想过自己写一个程序,定时刷新几个主要的火车票转让网站(比如赶集、58),当有新的转让信息出现时给出提醒。除了定时刷新之外,还希望能将电话直观地显示出来(因为网站的搜索结果只包含火车票的信息,需要一条条点击进入具体页面才能看到电话)。


1.
赶集网

1.1

首先找到的是赶集网的火车票转让。而在程序中,第一想法是使用WebBrowser组件,使用

this.wb.Navigate(“http://bj.ganji.com/piao/”);//wb是WebBrowser对象

载入赶集网火车票转让网页。在此页面上,主要有两个文本框与一个按钮,两个文本框用入分别输入起始站及终点站,按钮用于提交。分析页面的源文件后知道这三个HTML元素的ID分别是:

piao_from_station
piao_to_station
piao_zz_submit

于是使用以下代码进行网页的提交:

HtmlElement tbFrom = wb.Document.All["piao_from_station"];
HtmlElement tbTo 
= wb.Document.All["piao_to_station"];
HtmlElement btSubmit 
= wb.Document.All["piao_zz_submit"];
//
tbFrom.SetAttribute("value", {你的起始站名,比如北京});
tbTo.SetAttribute(
"value", {你的目标站名,比如福州});
btSubmit.InvokeMember(
"click");

至此,页面被顺利定位到搜索结果页面。

 

1.2

接着我就遇到第一个问题,在结果搜索页面,还用一个下拉框(HTMLSelect元素),用于选择火车票的发车时间,很容易得到此Select元素的ID

id_train_time_select

我尝试通过下面的代码选择指定的日期:

HtmlElement slDate = tmp.Document.All["id_train_time_select"];
string dt = {指定的日期字符串};
//
foreach (HtmlElement option in slDate.Children)
{
    
if (option.GetAttribute("value"== dt)
    {
        option.SetAttribute(
"selected""selected");
        
break;
    }
}

或是以下的代码:

HtmlElement slDate = tmp.Document.All["id_train_time_select"];
string dt = {指定的日期字符串};
int idx = 0;
//
foreach (HtmlElement option in slDate.Children)
{
    
if (option.GetAttribute("value"== dt)
    {
        slDate.SetAttribute(
"selectedIndex", idx.ToString());
        
break;
    }
    idx
++;
}

查看WebBrowser中的页面,下拉框的值已被正确设置,但页面未刷新(手工在页面上选择日期后搜索结果是会刷新的,并只显示此日期的车票),我尝试通过再次触发上一节中的piao_zz_submit按钮的click事件进行页面刷新,但这么刷新后导致刚才选中的下拉框中的日期丢失。还尝试触发此下拉框的onchange事件,无果(实际上从页面源代码看,此该下拉框也没有关于onchange事件的处理脚本)。

 

1.3

因为始终无法在选择下拉框时让页面刷新,所以只好使用其它方案,因为最终搜索页面的地址形如:

http://bj.ganji.com/piao/zz_北京-福州/20110120/

显然,我可以根据自己的需要拼出具体的Url,土是土了点,但是代码简单多了,于是顺利进行搜索页面的分析(我只是逐行读取WebBrowser中的DocumentStream,使用简单的正则进行匹配),找到每条搜索结果的具体Url,并通过WebBrowser打开这些具体Url,从中得到发贴人的电话号码。

在这里,又遇到新的问题,那就是多次使用WebBrowser之后,内存令人吃惊地上涨。因为内存的问题,最后不得不换成使用WebRequestWebResponse,好在事实上我并不需要在程序里显示真正的页面内容。使用类似下面的几行代码可以获得页面的Stream,随后便可对它进行分析。

protected StreamReader GetStreamReaderFromResponse(string pUrl)
{
    WebRequest wrq 
= WebRequest.Create(pUrl);
    WebResponse wrp 
= wrq.GetResponse();
    
return new StreamReader(wrp.GetResponseStream());
}

至此,内存问题基本解决了。

 

1.4

顺利地获得了电话号码,但是,为什么跟页面上显示的完全对应不起来?这里又遇到一个难处,从页面源代码中可以看到,从源码中抓到的电话并不是真正的电话号码,它经过一段繁琐的Javascript转换之后才能得到真实的电话号码(即页面上最终显示的号码)。

本想尝试写一个等价的C#方法,未遂。几经辗转,决定直接从C#代码中调用页面中的Javascript方法。参见如下代码片段:

wb = new WebBrowser();
wb.DocumentCompleted 
+= new System.Windows.Forms.WebBrowserDocumentCompletedEventHandler(this.wb_DocumentCompleted);
wb.Navigate(
"");
while (wb.ReadyState != WebBrowserReadyState.Complete)
{
    System.Windows.Forms.Application.DoEvents();
}
phone 
= wb.Document.InvokeScript(“F13”, new object[] { “需要处理的电话号码” }).ToString();
 
private void wb_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    WebBrowser wb 
= (WebBrowser)sender;
    HtmlElement head 
= wb.Document.GetElementsByTagName("head")[0];
    HtmlElement scriptEl 
= wb.Document.CreateElement("script");
    IHTMLScriptElement element 
= (IHTMLScriptElement)scriptEl.DomElement;
    element.text 
= strJavascript;
    head.AppendChild(scriptEl);
}

phone =..”一行是示例写法,其中的“F13”是用于处理电话号码的Javascript主方法名称,但因为这段Javascript经过混淆,每一次打开的页面背后的这段Javascript是不同的,所以并非都叫“F13”,但通过查看这段js,也很容易从中抽出主方法名称。

代码中strJavascript变量即是从页面源代码中抽取的js脚本。

另外,要使用IHTMLScriptElement,需要引用COM中的Microsoft HTML object library

至此,缝缝补补,如果加上定时器等一些处理,对赶集网火车票转让就基本可以实现预定目标了。

 

2. 58同城

58上的电话有一部分是以图片的方式显示的,就像一般的验证码那样,目前没有什么简单的处理方案,热心的同志们可以试试继续努力一把。祝同志们顺利买到火车票/机票。春节快乐!

posted @ 2011-01-19 17:40  Morven.Huang  阅读(1981)  评论(5编辑  收藏  举报