前陣子工作上需要寫爬蟲撈政府機關的資料回來,一般的靜態網頁可以由URL的規則找到資料連結,而動態網頁常無法使用此方式。最常遇到的是client端按下button後送javascript的動作給後端,然後才render網頁。
這篇簡單記錄如何抓取這類型網站的資料。
我們以北市府法規局網站為例,使用chrome的開發者工具。
這裡分成2個部分:
- 檢視送出query的header帶了哪些參數
- 觀察取得下一頁資料時,header又加入哪些參數
一開始我們先測試最少需要哪些輸入才能查詢,在這個例子中至少需要勾選一個類別加上發布期間才能query資料。假設查詢營建類,從105年1月1日到105年12月31日。開啟開發人員工具,進入Network頁面,此時按”送出查詢”。
在Network頁面下,我們找到wfLaw_Interpretation_SearchResult.aspx,點進去後可看到以下畫面:
紅色框起來的部分便是query送出的表格資料,這裡可以看到有三個欄位需要控制:’TCGC’, ‘TADF’及’TADT’。程式需要二個套件: request和cheerio,前者可送出HTTP request;後者可以把回傳的網頁資料透過類似jQuery的selector抓取需要的內容
app.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const Request = require('request'); const cheerio = require('cheerio');
const request = Request.defaults({ jar: true, timeout: 60 * 1000, });
request.post({ url: 'http://www.laws.taipei.gov.tw/lawsystem/wfLaw_Interpretation_SearchResult.aspx', form: { TCGC: '003008', TLC1: 'AND', TLC2: 'AND', TADF: '1010604', TADT: '1010604' }, }, (error, response, body) => { if (error) { console.error(error); } if (response.statusCode !== 200) { console.log(response.statusCode); }
const $ = cheerio.load(body); const span = $('#ContentPlaceHolder1_gvList_lblRecordCount'); let count = $(span[0]).text(); console.log(`Query結果共${count}筆資料`); });
|
這段程式碼會印出query結果的資料數量。到此解決第一部分。
如果query數量超過一頁的上限,想抓取下一頁的資料會發現HTML並無下一頁的連結
它是透過javascript去產生下一頁的結果,我們使用開發者工具觀察點擊下一頁的動作:
Form Data有
- _EVENTTARGET
- _VIEWSTATE
- _VIEWGERNERATOR
- _EVENTVALIDATION
- ctl00$ContentPlaceHolder1$gvList$ctl01$ddlPage
- ctl00$ContentPlaceHolder1$gvList$ctl24$ddlPage
這6個值,其中_EVENTTARGET是固定的;ctl00$ContentPlaceHolder1$gvList$ctl01$ddlPage和ctl00$ContentPlaceHolder1$gvList$ctl24$ddlPage為從第幾頁過來和目前頁數-1
而2, 3, 4這三個值與cookie有關,在前一頁可取出這些值。於是程式碼需改寫成:
app.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| const Request = require('request'); const cheerio = require('cheerio');
const request = Request.defaults({ jar: true, timeout: 60 * 1000, });
request.post({ url: 'http://www.laws.taipei.gov.tw/lawsystem/wfLaw_Interpretation_SearchResult.aspx', form: { TCGC: '003010', TLC1: 'AND', TLC2: 'AND', TADF: '1050101', TADT: '1051231' }, }, (error, response, body) => { if (error) { console.error(error); } if (response.statusCode !== 200) { console.log(response.statusCode); }
const $ = cheerio.load(body); const eventValidation = $('#__EVENTVALIDATION').val(); const viewState = $('#__VIEWSTATE').val(); const viewStateGenerator = $('#__VIEWSTATEGENERATOR').val();
request.post({ url: 'http://www.laws.taipei.gov.tw/lawsystem/wfLaw_Interpretation_SearchResult.aspx', form: { __EVENTTARGET: 'ctl00$ContentPlaceHolder1$gvList$ctl01$ddlPage', __VIEWSTATE: viewState, __VIEWSTATEGENERATOR: viewStateGenerator, __EVENTVALIDATION: eventValidation, ctl00$ContentPlaceHolder1$gvList$ctl01$ddlPage: 1, ctl00$ContentPlaceHolder1$gvList$ctl24$ddlPage: 1 }, }, (err, resp, nextPage) => { if (err) { console.error(err); callback(err); return; } console.log(nextPage); }); });
|
取得第一個頁面後,記下view state等內容,再送出一個request並帶入剛才得到的狀態。如此可印出下一頁的內容。
最後給個提醒,面對這種動態網站時,如果我們使用async queue一次跑多個worker抓取資料,request的設定就不能使用default。原因是很有可能不同worker會用到同一個request object去query,假如A worker在抓取第二頁時,B worker送出另一個第二頁的query,會發生回傳內容不如預期的情況。
這時我們需要改變request object為區域變數,並且使用request.jar()取得custom cookie jar,範例如下:
app.js1 2 3 4 5 6 7
| function crawler4DynamicPage() { const customJar = request.jar(); const options = { jar: customJar, timeout: 60 * 1000, }; }
|
送query時再帶入options,這樣才能避免worker之間打架的情形
參考資料