2012年6月27日 星期三

如何取得特定網頁的文件內容

若想要透過程式取得特定網頁的內容,可以透過以下幾種元件:

  • WebBrowser:網頁瀏灠控制項。
  • WebRequest:這個類別可以對指定 URI 發送需求與接收回應。
  • WebClient:這個類別包裝 WebRequest 類別,也可以對指定的 URI 傳送與接收資料,可以說是 HttpWebRequest 的精簡版

由 WebBrowser 控制項中取得

WebBrowser 是一個用來瀏灠網頁用的控制項,當網頁下載完畢時,你只要透過 WebBrowser.DocumentText 屬性就可以取得完整的網頁內容。 或者由 WebBrowser.DocumentStream 建立資料流後,再由資料流讀取資料。 同時,為了確保網頁載入完成,上述程式碼應該寫在 DocumentCompleted 事件裡。

private void button1_Click(object sender, EventArgs e)
{
    webBrowser1.Url = new System.Uri(sUrl);
}

private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    // 直接由 WebBrowser.DocumentText 的屬性取得 html 內容
    string sContent = webBrowser1.DocumentText;

    // 使用 WebBrowser.DocumentStream 建立資料流
    // 使用 StreamReader 讀入資料流以取得網頁內容

    Stream stream = webBrowser1.DocumentStream;
    StreamReader reader ;

    // 以 UTF8 編碼方式,讀取網頁內容
    reader = new StreamReader(stream, System.Text.Encoding.UTF8);
    string htmlUTF8 = reader.ReadToEnd();

    reader.Close();
}

由 WebRequest 物件取得

WebRequest 是 .Net Framework 中的一個 abstract 類別,其主要功用是用來對特定的 URI 送出 WebRequest 要求,以及處理 WebResponse 回應。 而 HttpWebRequest 則是它的實作類別,是一個封裝了 Http 通訊協議的物件,支援 Header、Content、Cookie 等屬性。

通常要去 WebServer 提出要求,只要叫用 HttpWebRequest.GetResponse 方法即可,這是一個同步要求,也就是必須等到伺服器回應結束程式才會繼續。 若要對資源提出非同步要求,則可以使用 BeginGetResponseEndGetResponse 方法。

WebRequest 相對應的就是 WebResponse 物件,它是 .Net Framework 中用來包裝回應訊息的基底類別,你可以使用其衍生類別 HttpWebResponse 物件來操作這些訊息內容。

與 Web 伺服器進行溝通時,最常用的就是 GET 與 POST 二種方式, 這二個方法,最大的差異處在於,使用 POST 方法時必須先設定 HttpContent 的標頭資訊,再叫用 GetRequestStream 方法建立一個 Stream 物件,以便將參數資料寫入。 而使用 GET 方法,不用設定 HttpContent 的標頭資訊,且參數列資料直接附加在網址後面即可。 底下分別對這二種方式進一步說明:

HTTP GET

string url = "http://www.taifex.com.tw/chinese/3/3_1_1.asp";
    string param = "qtype=2&commodity_id=specialid&commodity_id2=DTF&syear=2013&smonth=9&sday=2";
    string uri = url + "?" + param;

    //建立 GET request 
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "GET";

    //送出 request 並取得回應內容
    string result = "";
    using (WebResponse response = request.GetResponse())
    {
        //處理回應內容
        using (StreamReader sr = new StreamReader(response.GetResponseStream()))
        {
            result = sr.ReadToEnd();
        }
    }
    Console.WriteLine(result);

HTTP POST

string url = "http://www.taifex.com.tw/chinese/3/3_1_1.asp";
    string param = "qtype=2&commodity_id=specialid&commodity_id2=DTF&syear=2013&smonth=9&sday=2";

    //建立 POST request 
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.ContentLength = param.Length;

    //如果 Request 有使用到 Cookie 驗證,就必須設定相關的 cookie
    //request.CookieContainer = cookie;

    //將參數資料以 stream 方式加入到 request 物件中
    byte[] bs = Encoding.ASCII.GetBytes(param);
    using (Stream stream = request.GetRequestStream())
    {
        stream.Write(bs, 0, bs.Length);
    }
            
    //送出 request 並取得回應內容
    string result = "";
    using (WebResponse response = request.GetResponse())
    {
        //處理回應內容
        using (StreamReader sr = new StreamReader(response.GetResponseStream()))
        {
            result = sr.ReadToEnd();
        }
    }
    Console.WriteLine(result);

對中文資料編碼

如果送出去的 request 中,包含中文的參數資料,可以先對資料進行編碼,至於應該採用何種編碼方式,在 GET 與 POST 有所不同。 若使用 GET 方式,你必須依據對方伺服器要求的編碼方式為標準; 若使用 POST 方式,你可以在所提交資料的 Header 中,透過 ContentType 屬性加以附註說明,接收資料的 Web 伺服器就能夠正確的解析。

string url = "http://www.taifex.com.tw/chinese/3/3_1_1.asp";
    string param = "syear={0}&smonth={1}&sday={2}&qtype=2&commodity_id=specialid&commodity_id2=DTF";

    Encoding encoding = Encoding.GetEncoding("utf-8");
    param = string.Format(param,
        HttpUtility.UrlEncode("2014", encoding),
        HttpUtility.UrlEncode("01", encoding),
        HttpUtility.UrlEncode("30", encoding));

    //建立 POST request 
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.ContentLength = param.Length;

    //將參數資料以 stream 方式加入到 request 物件中
    byte[] bs = Encoding.ASCII.GetBytes(param);
    using (Stream stream = request.GetRequestStream())
    {
        stream.Write(bs, 0, bs.Length);
    }

    //送出 request 並取得回應內容
    string result = "";
    using (WebResponse response = request.GetResponse())
    {
        //處理回應內容
        using (StreamReader sr = new StreamReader(response.GetResponseStream()))
        {
            result = sr.ReadToEnd();
        }
    }
    Console.WriteLine(result);

HttpWebRequest.AllowAutoRedirect

有時候 Web 主機會回應 HTTP 狀態碼 300,301,302 到用戶端,如果我們想要截取這個資訊,就必須將 AllowAutoRedirect 屬性設成 false ,它的預設值為 ture 。 如果值為 ture ,當送出 HttpWebRequest 後,伺服器會自動導向新的位置,而不會回應狀態碼給應用程式,而用戶端取得的 WebResponse 將是最後網頁的資訊。 如果值為 false ,所有具 300 至 399 之 HTTP 狀態碼的回應都會傳回至應用程式。你可以依取得的相關回應資訊,再進行其他處理。

private WebResponse GetWebResponse_POST2(string url, byte[] para, ref CookieContainer cookie)
{
    try
    {
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.Method = "POST";
        request.AllowAutoRedirect = false;

        ///如果 Request 有使用到 Cookie 驗證則必須設定相關的 cookie
        request.CookieContainer = cookie;

        //1)設定 Request 的 HTTP 標頭
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = para.Length;

        //叫用 GetRequestStream 方法建立 Stream 物件,以便寫入參數資料
        using (Stream stream = request.GetRequestStream())
        {
            stream.Write(para, 0, para.Length);  
        }

        //取得回應
        WebResponse response = request.GetResponse();

        //判斷狀態碼,再自行做適當的處理
        if (((HttpWebResponse)response).StatusCode == HttpStatusCode.Redirect)
        {
            string redirUrl = "http://" + request.Host + response.Headers["Location"]; //取得Redirect網址
            HttpWebRequest request_new = (HttpWebRequest)WebRequest.Create(redirUrl);
            request_new.Method = "GET";
            request_new.CookieContainer = cookie;
            response.Close();
            WebResponse response_new = request_new.GetResponse();
            return response_new;
        }
        else
        {
            return response;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return null;
    }
}

Cookie 的保存

關於 HTTP 的小知識

當 Client 送出一個 HTTP Request 時,這個 Request 的內容除了第一行的 Method、Path、Http Version 資訊外,其餘則分成 Head 和 Body 二個部份,彼此之間以一個空白行間隔。 每一個 Reqeust 都會用到 Head 部份,用來標示著該 Reqeust 的相關屬性。 而 Body 部份則是用來存放資料的地方,並非每一個 Request 都會使用到這個部份,必須視使用的 Method (GET,PUT,DELETE,POST...) 而定。 例如發出一個 Get Request 就不會含有 Body 資訊,而 Post Request 就一定會用到,有用到 Body 的 Request 就需要指定 Content-Type 屬性,這也是為什麼上面例子中, 當發出一個 HTTP POST Request 時,必須指定 ContentType="application/x-www-form-urlencoded" 的原因了。

底下二張圖,是由 Fiddler 截取,它是一個 Get Request 發送的內容以及它的回應內容。

底下二張圖則是一個 POST Request 發送的內容以及它的回應內容。

由 WebClient 物件取得

WebClient 類別包裝 WebRequest 類別,也可以用來對指定的 URI 傳送與接收資料。 它可以說是 HttpWebRequest 的精簡版,因此幾個常用的存取網頁方法,都可以直接由 WebClient 輕鬆完成。

WebClient GET

若要使用 WebClient 針對特定資源進行 GET 操作,有底下二個方法可用:
  • DownloadString :以 string 方式取得網頁的回應內容。
  • DownloadData :以 Byte 陣列的方式取得網頁的回應內容。

若要透過 WebClient 執行 Get 方法,只要叫用 DownloadString 方法即可。

private string GetWebResponse_GET(string url)
{
    WebClient wc = new WebClient();
    wc.Encoding = Encoding.UTF8;
    string result = wc.DownloadString(url);
    return result;
}

WebClient POST

若要使用 WebClient 針對特定資源進行 POST 操作,有底下二個方法可用:

  • UploadString :Uploads the specified string to the specified resource.
  • UploadValues :Uploads the specified name/value collection to the resource identified by the specified URI.
private string GetWebResponse_POST(string url, string parameters)
    {
        WebClient wc = new WebClient();
        wc.Encoding = Encoding.UTF8;
        wc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
        string result = wc.UploadString(url, parameters);
        return result;
    }

    private string GetWebResponse_POST(string url, NameValueCollection parameters)
    {
        WebClient wc = new WebClient();
        wc.Encoding = Encoding.UTF8;
        wc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
        byte[] buffer = wc.UploadValues(url, parameters);
        string result = MyWebDownload.EncodingData(buffer);
        return result;
    }

叫用方法:

var url = "http://www.twse.com.tw/ch/trading/exchange/MI_INDEX/MI_INDEX.php";

//叫用 UploadString
string parameters1 = "qdate=104/01/05&selectType=MS2";
string response1 = GetWebResponse_POST(url, parameters1);

//叫用 UploadValues
NameValueCollection parameters2 = new NameValueCollection();
parameters2.Add("qdate", "104/01/05");
parameters2.Add("selectType", "MS2");
string response2 = GetWebResponse_POST(url, parameters2);

服務器提交協議衝突 Section=ResponseStatusLine

有時候在下載網頁資料時,會發生「伺服器認可通訊協定違規. Section=ResponseStatusLine」錯誤訊息,在網路上找到一些原因說法,原因可能是回應的內容並沒有以 CRLF 做結束。 解決方法是在 app.config 中加入以下設定:

<system.net>
        <settings>
            <httpWebRequest useUnsafeHeaderParsing="true" />
        </settings>
    </system.net>

如果問題還是存在,將 HttpWebRequest 的 KeepAlive 屬性設成 false 。

沒有留言:

張貼留言