小技巧 - 抓取 ASP.NET WebForm 網頁 PostBack 結果
10 |
用 WebClient 爬網頁抓內容已是老生常談,但最近發現抓 ASP.NET WebForm 網頁的特殊眉角,忍不住又想分享。 (是的,「我種了一棵葱,大家快來嚐嚐」的毛病又犯了)
主要關鍵在於當 WebForm 邏輯寫在 Server-Side Event,例如 Button_OnClick(),單純用 POST Request 是不會觸發的。例如以下示範,假設有個簡單的 WebForm 網頁,有一個 DropDownList 放選項,一個 Label 顯示結果,Button 在 Server-Side OnClick 事件中依 DropDownList 選取值在 Label 顯示不同文字:
<%@Page Language="C#"%>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ddlCatg.Items.Clear();
ddlCatg.Items.Add(new ListItem("筆電", "Notebook"));
ddlCatg.Items.Add(new ListItem("手機", "Mobile"));
}
}
void btnQuery_OnClick(object sender, EventArgs e)
{
lblDisplay.Text =
ddlCatg.SelectedItem.Value == "Notebook" ? "ThinkPad X21" : "Nokia 3310";
}
</script>
<html>
<body>
<form runat="server">
<asp:DropDownList runat="server" id="ddlCatg" Height="23"></asp:DropDownList>
<asp:Button runat="server" id="btnQuery" text="查詢" OnClick="btnQuery_OnClick"></asp:Button>
<div class="result">
<asp:Label runat="server" id="lblDisplay" Text="請按查詢" ></asp:Label>
</div>
</form>
</body>
</html>
實際操作如下:
依照直覺,反正就是個 POST 傳送表單內容,用 WebClient.UploadValues 應該就可以搞定:
static void Main(string[] args)
{
var wc = new WebClient();
var form = new NameValueCollection();
form.Add("ddlCatg", "Mobile");
var resp = Encoding.UTF8.GetString(
wc.UploadValues("http://localhost/aspnet/webform.aspx", form));
var m = Regex.Match(resp,
@"(?ims)ID=""lblDisplay"">(?<t>.*?)</span>");
if (m.Success)
Console.WriteLine("Result=" + m.Groups["t"].Value);
Console.Read();
}
很不幸地,雖然成功發出 POST 請求取回網頁,但並沒有觸發 btnQuery_OnClick,只會得到 Result=請按查詢
。
原因是這樣的,ASP.NET WebForm 有自己的一套機制,網頁包含了 __VIEWSTATE、__EVENTVALIDATION 等隱藏欄位:(延伸閱讀:這也成了 WebForm 的原罪,例如 UpdatePanel招誰惹誰?)
而在 POST 送出表單時,這些欄位需一併傳送才會進入 PostBack 流程觸發 Server-Side Event:
因此網路爬蟲必須模擬相同的行為才能得到正確結果,我的解法是先發一個 GET Request 拿到 HTML 從中取出 __VIEWSTATE 及 __EVENTVALIDATION 欄位值,當成 POST Form 的一部分 。另外有一點要注意,上圖中標註為紅色的 btnQuery: "查詢",對後端執行並無作用,但對 ASP.NET WebForm 是必須的,需包含在傳送內容中。
修改後的程式如下:
static void Main(string[] args)
{
var wc = new WebClient();
var url = "http://localhost/aspnet/webform.aspx";
var html = wc.DownloadString(url);
var mViewState = Regex.Match(html, @"(?ims)id=""__VIEWSTATE"" value=""(?<v>.+?)""");
var mEventVald = Regex.Match(html, @"(?ims)id=""__EVENTVALIDation"" value=""(?<v>.+?)""");
var form = new NameValueCollection();
form.Add("__VIEWSTATE", mViewState.Groups["v"].Value);
form.Add("__EVENTVALIDATION", mEventVald.Groups["v"].Value);
form.Add("btnQuery", "查詢");
form.Add("ddlCatg", "Mobile");
var resp = Encoding.UTF8.GetString(
wc.UploadValues(url, form));
var m = Regex.Match(resp,
@"(?ims)ID=""lblDisplay"">(?<t>.*?)</span>");
if (m.Success)
Console.WriteLine("Result=" + m.Groups["t"].Value);
Console.Read();
}
實測結果為 Result=Nokia 3310
,成功!
同場加映,在 .NET Core 裡 WebClient 已被 HttpClient 取代,在此一併示範用 HttpClient 的寫法。主要差異在於 HttpClient 改用 GetStringAync()/PostAsync()+new FormUrlEncodedContent(Dictionary<string, string>) 取代 WebCient DownloadString()、UploadValues(),其餘原理相同。
static void Main(string[] args)
{
var hc = new HttpClient();
var url = "http://localhost/aspnet/webform.aspx";
var html = hc.GetStringAsync(url).Result;
var mViewState = Regex.Match(html, @"(?ims)id=""__VIEWSTATE"" value=""(?<v>.+?)""");
var mEventVald = Regex.Match(html, @"(?ims)id=""__EVENTVALIDation"" value=""(?<v>.+?)""");
var form = new Dictionary<string, string>();
form.Add("__VIEWSTATE", mViewState.Groups["v"].Value);
form.Add("__EVENTVALIDATION", mEventVald.Groups["v"].Value);
form.Add("btnQuery", "查詢");
form.Add("ddlCatg", "Mobile");
var resp = hc.PostAsync(url, new FormUrlEncodedContent(form))
.Result.Content.ReadAsStringAsync().Result;
var m = Regex.Match(resp,
@"(?ims)ID=""lblDisplay"">(?<t>.*?)</span>");
if (m.Success)
Console.WriteLine("Result=" + m.Groups["t"].Value);
Console.Read();
}
When crawling ASP.NET WebForm page, __VIEWSTATE and __EVENTVALIDATION hidden fields are required in the POST request to trigger server-side event.
Comments
# by 87
想請問一下這個沒按鈕的onchange doPostBack https://webapp.cgmh.org.tw/bed/view/domain/bed/ACS350233.aspx 也是一樣依樣畫葫蘆嗎
# by Jeffrey
to 87,你列的頁面是用 ASP.NET AJAX UpdatePanel 做的,背後是 ASP.NET AJAX 內建的 JS 程式函式,__VIEWSTATE、__EVENTVALIDation 機制就是它發明的,故可以順利運作。
# by 87
那請問dropdownlist的值該怎麼傳進去?
# by 87
謝謝您 我做出來了 但我還是不清楚正規表示法(mViewState,mEventVald ) 裡面會有個<v>..? 如果可以的話 能撥冗詳解一下嗎
# by 87
原來是群組..我看懂了 抱歉打擾
# by Jeffrey
to 87, 參考原始碼,下拉選單有ASP.NET AJAX加入的onchange事件觸發__doPostBack:<select name="ctl00$ctl00$cp...略...$DDL_LOC" onchange="javascript:setTimeout('__doPostBack(\'ctl00$ctl00$cp_...略...$DDL_LOC\',\'\')', 0)" id="cp_Content_...略..._DDL_LOC">
# by 87
2020-02-27 08:56 AM 已回說做出來啦 謝謝 反倒是正規表示法比較難懂..哈哈
# by TPur
感謝分享! 另外想問如果第一次進入頁面需先點選RadioButton才能觸發顯示某個TextBox,網頁會原地postback一次,要如何根據response出來的結果再次傳送呢? 我目前卡在點完RadioButton,並在第一次的response結果中有找到這個TextBox,但要再次執行UploadValues()會出錯,因為餵入的網址等於又重置頁面,導致TextBox再度處於隱藏狀態,會回傳找不到此控制項。 總覺得好像還差一步..可以的話再麻煩給我一些提示,感激不盡!!
# by Jeffrey
to TPur, 因為餵入的網址等於又重置頁面,導致TextBox再度處於隱藏狀態 <= 如果該 TextBox 的隱藏是透過 Server 端修改 TextBox.Visiable 控制且 __VIEWSTATE、__EVENTVALIDATION 有正確傳送,那控制項應該會處於顯示狀態。
# by TPur
我成功了!! 所以把第二次回傳回來的__VIEWSTATE、__EVENTVALIDATION再取一次,並再更新到NameValueCollection()後再傳送一次,就收到有顯示的TextBox並設定Value進去了! 非常感謝指點!!