iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
0
Modern Web

寫給PHP開發者的30堂網路爬蟲開發系列 第 18

Day 18:案例研究 2-1 實做課程查詢網站爬蟲-part2

前言

從昨天我們可以知道,第一階段part1是取得需要做POST方法的相關__VIEWSTATE__EVENTVALIDATION等相關值。

接著在本日的第二階段中,要持續將part1GET方法拿到的上述這些值拿去做第二階段的POST方法並得到課程查詢結果列表清單。

實做

首先,我們還是一樣先將我們的爬蟲Docker環境給跑起來。

docker run --name=php_crawler -d -it php_crawler bash

跑起來之後,打開昨天的lab2-1.php並填上有關POST的部份,相關程式碼如下:

<?php

require_once __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;

$publicCourses = 'https://infosys.nttu.edu.tw/n_CourseBase_Select/CourseListPublic.aspx';

$headers = [
    'Host' => 'infosys.nttu.edu.tw',
    'Connection' => 'keep-alive',
    'Cache-Control' => 'max-age=0',
    'Upgrade-Insecure-Requests' => '1',
    'Sec-Fetch-Mode' => 'navigate',
    'Sec-Fetch-User' => '?1',
    'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 application/signed-exchange;v=b3',
    'Sec-Fetch-Site' => 'none',
    'Referer' => 'https://infosys.nttu.edu.tw/',
    'Accept-Encoding' => 'gzip, deflate, br',
    'Accept-Language' => 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
    'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13',
];

$client = new Client(['cookies' => true]);
$response = $client->request('GET', $publicCourses, [
    'debug' => true,
    'headers' => $headers,
]);

$publicCourseString = (string)$response->getBody();
$viewState = '__VIEWSTATE';
$eventValidation = '__EVENTVALIDATION';
$viewStateGenerator = '5D156DDA';

$crawler = new Crawler($publicCourseString);

$crawler
   ->filter('input[type="hidden"]')
   ->reduce(function (Crawler $node, $i) {
       global $viewState;
       global $eventValidation;
       if ($node->attr('name') === $viewState) {
           $viewState = $node->attr('value');
       }
       if ($node->attr('name') === $eventValidation) {
           $eventValidation = $node->attr('value');
       }
   });

$formParams = [
    'form_params' => [
        'ToolkitScriptManager1' => 'UpdatePanel1|Button3',
        'ToolkitScriptManager1_HiddenField' => '',
        '__EVENTTARGET' => '',
        '__EVENTARGUMENT' => '',
        '__LASTFOCUS' => '',
        '__VIEWSTATE' => $viewState,
       '__VIEWSTATEGENERATOR' => $viewStateGenerator,
        '__SCROLLPOSITIONX' => '0',
        '__SCROLLPOSITIONY' => '0',
        '__VIEWSTATEENCRYPTED' => '',
        '__EVENTVALIDATION' => $eventValidation,
        'DropDownList1' => '1071',
        'DropDownList6' => '1',
        'DropDownList2' => '%',
        'DropDownList3' => '%',
        'DropDownList4' => '%',
        'TextBox9' => '',
        'DropDownList5' => '%',
        'DropDownList7' => '%',
        'TextBox1' => '',
        'DropDownList8' => '%',
        'TextBox6' => '0',
        'TextBox7' => '14',
        '__ASYNCPOST' => 'true',
        'Button3' => '查詢',
    ],
    'headers' => [
        'Sec-Fetch-Mode: cors',
        'Origin: https://infosys.nttu.edu.tw',
        'Accept-Encoding: gzip, deflate, br',
        'Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'X-Requested-With: XMLHttpRequest',
        'Connection: keep-alive',
        'X-MicrosoftAjax: Delta=true',
        'Accept: */*',
        'Cache-Control: no-cache',
        'Referer: https://infosys.nttu.edu.tw/n_CourseBase_Select/CourseListPublic.aspx',
        'Sec-Fetch-Site: same-origin',
        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
    ],
];

$response = $client->request('POST', $publicCourses, $formParams);

$coursesString = (string)$response->getBody();

var_dump($coursesString);

上述程式碼包含了昨天的GET方法之外,另外還多了POST方法,那上述程式碼做法如下:

  • 先利用GET方法拿到所謂的__VIEWSTATE__EVENTVALIDATION隱藏值。
  • 拿到上述這些值之後,在POST方法中將它與其他的表單值一併送出。
  • 利用POST方法送完之後,得到的回應內容再將其印出。

那這樣就會得到第一頁的課程清單了。得到的回應結果會類似下面這樣的內容:

.......
		</tr><tr class="NTTU_GridView_Row">
			<td style="width:60px;">選修</td><td style="width:100px;">[資管系]資管三</td><td style="width:100px;">專業模組二</td><td style="width:100px;">SIM12E40C002</td><td style="width:200px;">消費者行為</td><td style="width:60px;">
                                                <a onclick="window.open(&#39;CourseInfo1.aspx?id=25200&amp;yrsem=1071&#39;,&#39;_blank&#39;,&#39;toolbar=no, scrollbars=yes, resizable=no, top=0, left=0, width=600, height=450&#39;);" id="GridView1_ctl11_LinkButton3" class="button" href="javascript:__doPostBack(&#39;GridView1$ctl11$LinkButton3&#39;,&#39;&#39;)">V</a>
                                            </td><td style="width:60px;">3</td><td style="width:60px;">45</td><td style="width:60px;">10</td><td style="width:60px;">0</td><td style="width:60px;">45</td><td style="width:200px;">林育珊</td><td style="width:100px;">27,28,29</td><td style="width:200px;">SEC103教室103(50)</td><td style="width:100px;">&nbsp;</td><td style="width:100px;">&nbsp;</td><td style="width:300px;">&nbsp;</td><td style="width:200px;">無</td><td style="width:100px;"></td>
		</tr><tr class="NTTU_GridView_Pager">
			<td colspan="19"><table border="0">
				<tr>
.......

如果回應內容中有出現像上述類似的行為,那就代表爬蟲實做上成功了。

注意地方

POST方法中,有幾個重點需要去注意:

  • 表單傳送欄位的值都一定要傳送,尤其__VIEWSTATE__EVENTVALIDATION
  • 請求標頭(header)需要送User-Agent的請求標頭過去
  • 表單欄位值中可將__ASYNCPOST欄位送過去或不送,預設是false,而值送truefalse是有差別的

上述這三點是我認為實做對ASPXC#為後端的爬蟲遇到的問題。原因就是出在於__VIEWSTATE__EVENTVALIDATION

因為這兩個值會去驗證送過來的表單內容與來源是否為同一個,若不是同一個的話,原則上系統會出500內部伺服器的錯誤出現。

例如,假設表單與標頭要的順序應該為下列這樣:

$formParams = [
    'form_params' => [
        'ToolkitScriptManager1_HiddenField' => '',
        'ToolkitScriptManager1' => 'UpdatePanel1|Button3',
        '__EVENTTARGET' => '',
        '__EVENTARGUMENT' => '',
        '__LASTFOCUS' => '',
        '__VIEWSTATE' => $viewState,
       '__VIEWSTATEGENERATOR' => $viewStateGenerator,
        '__SCROLLPOSITIONX' => '0',
        '__SCROLLPOSITIONY' => '0',
        '__VIEWSTATEENCRYPTED' => '',
        '__EVENTVALIDATION' => $eventValidation,
        'DropDownList1' => '1071',
        'DropDownList6' => '1',
        'DropDownList2' => '%',
        'DropDownList3' => '%',
        'DropDownList4' => '%',
        'TextBox9' => '',
        'DropDownList5' => '%',
        'DropDownList7' => '%',
        'TextBox1' => '',
        'DropDownList8' => '%',
        'TextBox6' => '0',
        'TextBox7' => '14',
        '__ASYNCPOST' => 'true',
        'Button3' => '查詢',
    ],
    'headers' => [
        'Sec-Fetch-Mode: cors',
        'Origin: https://infosys.nttu.edu.tw',
        'Accept-Encoding: gzip, deflate, br',
        'Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'X-Requested-With: XMLHttpRequest',
        'Connection: keep-alive',
        'X-MicrosoftAjax: Delta=true',
        'Accept: */*',
        'Cache-Control: no-cache',
        'Referer: https://infosys.nttu.edu.tw/n_CourseBase_Select/CourseListPublic.aspx',
        'Sec-Fetch-Site: same-origin',
        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
    ],
];

這時候我們把表單中的__VEIEWSTATE拿掉,得到下面這樣:

$formParams = [
    'form_params' => [
        'ToolkitScriptManager1_HiddenField' => '',
        'ToolkitScriptManager1' => 'UpdatePanel1|Button3',
        '__EVENTTARGET' => '',
        '__EVENTARGUMENT' => '',
        '__LASTFOCUS' => '',
       '__VIEWSTATEGENERATOR' => $viewStateGenerator,
        '__SCROLLPOSITIONX' => '0',
        '__SCROLLPOSITIONY' => '0',
        '__VIEWSTATEENCRYPTED' => '',
        '__EVENTVALIDATION' => $eventValidation,
        'DropDownList1' => '1071',
        'DropDownList6' => '1',
        'DropDownList2' => '%',
        'DropDownList3' => '%',
        'DropDownList4' => '%',
        'TextBox9' => '',
        'DropDownList5' => '%',
        'DropDownList7' => '%',
        'TextBox1' => '',
        'DropDownList8' => '%',
        'TextBox6' => '0',
        'TextBox7' => '14',
        '__ASYNCPOST' => 'true',
        'Button3' => '查詢',
    ],
    'headers' => [
        'Sec-Fetch-Mode: cors',
        'Origin: https://infosys.nttu.edu.tw',
        'Accept-Encoding: gzip, deflate, br',
        'Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'X-Requested-With: XMLHttpRequest',
        'Connection: keep-alive',
        'X-MicrosoftAjax: Delta=true',
        'Accept: */*',
        'Cache-Control: no-cache',
        'Referer: https://infosys.nttu.edu.tw/n_CourseBase_Select/CourseListPublic.aspx',
        'Sec-Fetch-Site: same-origin',
        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
    ],
];

這時候送出這個順序有錯誤的表單資料,就會得到下面的節錄的回應內容:

ue="/wEdAAbxmE99JhLisIrrBlSpleKvA3sa9CLAiY0NRgwF9EJQGh6kvJC1EopKW4ZDfj9Gj7oGHrYxvYrs5XDlrjyz+wVULvWz/wJ+1kADwg6S0w9SXo/Fg06KOWoBIRHuyh28DoVPLgf8rKyi7Ffc8EgW/ntaNx+wYA==" />
</div>
    <div>
        <span id="lblMsg">The error message:</span><br />
        <textarea name="txtMsg" rows="2" cols="20" id="txtMsg" class="input">
無效的 Viewstate。
	Client IP: 61.230.251.119
	Port: 38320
	Referer: 
	Path: /n_CourseBase_Select/CourseListPublic.aspx
	User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36

這代表__VIEWSTATE是需要的,抑或是將__EVENTVALIDATION拿掉,也會得到下列錯誤的節錄內容:

	<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="AB827D4F" />
	<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEdAAbxmE99JhLisIrrBlSpleKvA3sa9CLAiY0NRgwF9EJQGh6kvJC1EopKW4ZDfj9Gj7oGHrYxvYrs5XDlrjyz+wVULvWz/wJ+1kADwg6S0w9SXo/Fg06KOWoBIRHuyh28DoVPLgf8rKyi7Ffc8EgW/ntaNx+wYA==" />
</div>
    <div>
        <span id="lblMsg">The error message:</span><br />
        <textarea name="txtMsg" rows="2" cols="20" id="txtMsg" class="input">
無效的回傳或回呼引數。已在組態中使用 &lt;pages enableEventValidation=&quot;true&quot;/&gt; 或在網頁中使用 &lt;%@ Page EnableEventValidation=&quot;true&quot; %&gt; 啟用事件驗證。基於安全性理由,這項功能驗證回傳或回呼引數是來自原本呈現它們的伺服器控制項。如果資料為有效並且是必須的,請使用 ClientScriptManager.RegisterForEventValidation 方法註冊回傳或回呼資料,以進行驗證。</textarea><br />
        <span id="lblStackTrace">The error stack trace:</span><br />
        <textarea name="txtStackTrace" rows="2" cols="20" id="txtStackTrace" class="input">
   於 System.Web.UI.ClientScriptManager.ValidateEvent(String uniqueId, String argument)
   於 System.Web.UI.Control.ValidateEvent(String uniqueID, String eventArgument)
   於 System.Web.UI.WebControls.DropDownList.LoadPostData(String postDataKey, NameValueCollection postCollection)
   於 System.Web.UI.Page.ProcessPostData(NameValueCollection postData, Boolean fBeforeLoad)
   於 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)</textarea>
        <br />
        <br />
        <span id="lblDesc">Feedback description:</span><br />
        <textarea name="txtDescription" rows="2" cols="20" id="txtDescription" class="input">
</textarea><br />
        <input type="submit" name="ButtonServerInfo" value="Server Stack" id="ButtonServerInfo" class="btn_mouseout" onmouseout="this.className=&#39;btn_mouseout&#39;" onmouseover="this.className=&#39;btn_mouseover&#39;" />
        <input type="submit" name="btnFeedback" value="Feedback" id="btnFeedback" class="btn_mouseout" onmouseout="this.className=&#39;btn_mouseout&#39;" onmouseover="this.className=&#39;btn_mouseover&#39;" /></div>
    </form>
</body>

上述這意思是說,驗證到__EVENTVALIDATION中,來源跟傳送似乎不是同一個。所以就跳出這樣錯誤了。

如果不送合法的User-Agent的請求標頭過去會怎樣呢?假設我們送下列這個標頭過去:

.......
    'headers' => [
        'Sec-Fetch-Mode: cors',
        'Origin: https://infosys.nttu.edu.tw',
        'Accept-Encoding: gzip, deflate, br',
        'Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'X-Requested-With: XMLHttpRequest',
        'Connection: keep-alive',
        'X-MicrosoftAjax: Delta=true',
        'Accept: */*',
        'Cache-Control: no-cache',
        'Referer: https://infosys.nttu.edu.tw/n_CourseBase_Select/CourseListPublic.aspx',
        'Sec-Fetch-Site: same-origin',
        'User-Agent' => 'Guzzle client',
    ],
.......

接著,送出去之後,會得到下列節錄的回應內容:

    <div>
        <span id="lblMsg">The error message:</span><br />
        <textarea name="txtMsg" rows="2" cols="20" id="txtMsg" class="input">
此頁面正在執行非同步回傳,但 ScriptManager.SupportsPartialRendering 屬性卻是設定為 false。請於非同步回傳時將此屬性設定為 true。</textarea><br />
        <span id="lblStackTrace">The error stack trace:</span><br />
        <textarea name="txtStackTrace" rows="2" cols="20" id="txtStackTrace" class="input">
   於 System.Web.UI.ScriptManager.OnPageInitComplete(Object sender, EventArgs e)
   於 System.Web.UI.Page.OnInitComplete(EventArgs e)
   於 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)</textarea>

這個時候就是要改成真正瀏覽器會送的User-Agent字串了。

如果要知道網頁瀏覽器會送什麼樣的User-Agent過去,以Chrome為例子,可以打開一個分頁並輸入chrome://version/會得到下面的頁面:

https://ithelp.ithome.com.tw/upload/images/20191003/20103975P3nFAPnRvO.png

這時候把User Agent所對應的字串放到User-Agent請求標頭值中即可讓POST方法請求課程列表成功了。

那如果是表單中的__ASYNCPOST值改成truefalse時候呢?

得到的回應內容其實是大同小異的。唯一有差別是當用true傳過去之後,回應內容會多下面這一段,其中節錄如下:

......
|0|hiddenField|__EVENTTARGET||0|hiddenField|__EVENTARGUMENT||0|hiddenField|__LASTFOCUS||50504|hiddenField|__VIEWSTATE|C024QlaQNOQWl9C0Y+RsBL8UOe2DK2739QqJKZlkVjjukJg6tZTefHqOCYK6+TgwOOzn8q2tAdWQ42ycPY1H4/
......

就是會多一段用POST傳過去的參數,並用|隔開。所以建議是,可以將__ASYNCPOST設定值為false就好,或是不要在表單中,送這個欄位到C#後端。

結論

從這次實做爬蟲,可以學習到如何拿到課程查詢清單,接下來實做爬蟲部份就是拿到下一個分頁的清單了。

參考資料


上一篇
Day 17:案例研究 2-1 實做課程查詢網站爬蟲
下一篇
Day 19:案例研究 2-1 實做課程查詢網站爬蟲-part3
系列文
寫給PHP開發者的30堂網路爬蟲開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言