做者簡介 felix 螞蟻金服·數據體驗技術團隊html
咱們平常使用瀏覽器的步驟爲:啓動瀏覽器、打開一個網頁、進行交互。而無頭瀏覽器
指的是咱們使用腳原本執行以上過程的瀏覽器,能模擬真實的瀏覽器使用場景。前端
有了無頭瀏覽器,咱們就能作包括但不限於如下事情:node
無頭瀏覽器不少,包括但不限於:git
本文主要介紹 Google 提供的無頭瀏覽器(headless Chrome), 他基於 Chrome DevTools protocol 提供了很多高度封裝的接口方便咱們控制瀏覽器。github
爲了能使用
async
/await
等新特性,須要使用 v7.6.0 或更高版本的 Node.chrome
// 啓動瀏覽器
const browser = await puppeteer.launch({
// 關閉無頭模式,方便咱們看到這個無頭瀏覽器執行的過程
// headless: false,
timeout: 30000, // 默認超時爲30秒,設置爲0則表示不設置超時
});
// 打開空白頁面
const page = await browser.newPage();
// 進行交互
// ...
// 關閉瀏覽器
// await browser.close();
複製代碼
// 設置瀏覽器視窗
page.setViewport({
width: 1376,
height: 768,
});
複製代碼
// 地址欄輸入網頁地址
await page.goto('https://google.com/', {
// 配置項
// waitUntil: 'networkidle', // 等待網絡狀態爲空閒的時候才繼續執行
});
複製代碼
打開一個網頁,而後截圖保存到本地:數組
await page.screenshot({
path: 'path/to/saved.png',
});
複製代碼
完整示例代碼瀏覽器
打開一個網頁,而後保存 pdf 到本地:bash
await page.pdf({
path: 'path/to/saved.pdf',
format: 'A4', // 保存尺寸
});
複製代碼
完整示例代碼服務器
要獲取打開的網頁中的宿主環境,咱們可使用 Page.evaluate
方法:
// 獲取視窗信息
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
};
});
console.log('視窗信息:', dimensions);
// 獲取 html
// 獲取上下文句柄
const htmlHandle = await page.$('html');
// 執行計算
const html = await page.evaluate(body => body.outerHTML, htmlHandle);
// 銷燬句柄
await htmlHandle.dispose();
console.log('html:', html);
複製代碼
Page.$
能夠理解爲咱們經常使用的 document.querySelector
, 而 Page.$$
則對應 document.querySelectorAll
。
打開谷歌首頁,輸入關鍵字,回車進行搜索:
// 地址欄輸入網頁地址
await page.goto('https://google.com/', {
waitUntil: 'networkidle', // 等待網絡狀態爲空閒的時候才繼續執行
});
// 聚焦搜索框
// await page.click('#lst-ib');
await page.focus('#lst-ib');
// 輸入搜索關鍵字
await page.type('辣子雞', {
delay: 1000, // 控制 keypress 也就是每一個字母輸入的間隔
});
// 回車
await page.press('Enter');
複製代碼
每個簡單的動做鏈接起來,就是一連串複雜的交互,接下來咱們看兩個更具體的示例。
傳統的爬蟲是基於 HTTP 協議,模擬 UserAgent 發送 http 請求,獲取到 html 內容後使用正則解析出須要抓取的內容,這種方式面對服務端渲染直出 html 的網頁時很是便捷。
但遇到單頁應用(SPA)時,或遇到登陸校驗時,這種爬蟲就顯得比較無力。
而使用無頭瀏覽器,抓取網頁時徹底使用了人機交互時的操做,因此頁面的初始化徹底能使用宿主瀏覽器環境渲染完備,再也不須要關心這個單頁應用在前端初始化時須要涉及哪些 HTTP 請求。
無頭瀏覽器提供的各類點擊、輸入等指令,徹底模擬人的點擊、輸入等指令,也就不再用擔憂正則寫不出來了啊哈哈哈
固然,有些場景下,使用傳統的 HTTP 爬蟲(寫正則匹配) 仍是比較高效的。
在這裏就再也不詳細對比這些差別了,如下這個例子僅做爲展現模擬一個完整的人機交互:使用移動版餓了麼點外賣。
先看下效果:
代碼比較長就不全貼了,關鍵是幾行:
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone6 = devices['iPhone 6'];
console.log('啓動瀏覽器');
const browser = await puppeteer.launch();
console.log('打開頁面');
const page = await browser.newPage();
// 模擬移動端設備
await page.emulate(iPhone6);
console.log('地址欄輸入網頁地址');
await page.goto(url);
console.log('等待頁面準備好');
await page.waitForSelector('.search-wrapper .search');
console.log('點擊搜索框');
await page.tap('.search-wrapper .search');
await page.type('麥當勞', {
delay: 200, // 每一個字母之間輸入的間隔
});
console.log('回車開始搜索');
await page.tap('button');
console.log('等待搜素結果渲染出來');
await page.waitForSelector('[class^="index-container"]');
console.log('找到搜索到的第一家外賣店!');
await page.tap('[class^="index-container"]');
console.log('等待菜單渲染出來');
await page.waitForSelector('[class^="fooddetails-food-panel"]');
console.log('直接選一個菜品吧');
await page.tap('[class^="fooddetails-cart-button"]');
// console.log('===爲了看清楚,傲嬌地等兩秒===');
await page.waitFor(2000);
await page.tap('[class^=submit-btn-submitbutton]');
// 關閉瀏覽器
await browser.close();
複製代碼
關鍵步驟是:
關鍵的幾個指令:
page.tap
(或 page.click
) 爲點擊page.waitForSelector
意思是等待指定元素出如今網頁中,若是已經出現了,則當即繼續執行下去, 後面跟的參數爲 selector
選擇器,與咱們經常使用的 document.querySelector
接收的參數一致page.waitFor
後面能夠傳入 selector
選擇器、function
函數或 timeout
毫秒時間,如 page.waitFor(2000)
指等待2秒再繼續執行,例子中用這個函數暫停操做主要是爲了演示以上幾個指令均可接受一個 selector
選擇器做爲參數,這裏額外介紹幾個方法:
page.$(selector)
與咱們經常使用的 document.querySelector(selector)
一致,返回的是一個 ElementHandle
元素句柄page.$$(selector)
與咱們經常使用的 document.querySelectorAll(selector)
一致,返回的是一個數組在有頭瀏覽器上下文中,咱們選擇一個元素的方法是:
const body = document.querySelector('body');
const bodyInnerHTML = body.innerHTML;
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製代碼
而在無頭瀏覽器裏,咱們首先須要獲取一個句柄,經過句柄獲取到環境中的信息後,銷燬這個句柄。
// 獲取 html
// 獲取上下文句柄
const bodyHandle = await page.$('body');
// 執行計算
const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle);
// 銷燬句柄
await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);
複製代碼
除此以外,還可使用 page.$eval
:
const bodyInnerHTML = await page.$eval('body', dom => dom.innerHTML);
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製代碼
page.evaluate
意爲在瀏覽器環境執行腳本,可傳入第二個參數做爲句柄,而 page.$eval
則針對選中的一個 DOM 元素執行操做。
我在 圖靈社區 上買了很多電子書,之前支持推送到 mobi
格式到 kindle
或推送 pdf
格式到郵箱進行閱讀,不過常常會關閉這些推送渠道,只能留在網頁上看書。
對我來講不是很方便,而這些書籍的在線閱讀效果是服務器渲染出來的(帶了大量標籤,沒法簡單抽取出好的排版),最好的方式固然是直接在線閱讀並保存爲 pdf 或圖片了。
藉助瀏覽器的無頭模式,我寫了個簡單的下載已購買書籍爲 pdf
到本地的腳本,支持批量下載已購買的書籍。
使用方法,傳入賬號密碼和保存路徑,如:
$ node ./demo/download-ituring-books.js '用戶名' '密碼' './books'
複製代碼
注意:puppeteer
的 Page.pdf()
目前僅支持在無頭模式中使用,因此要想看有頭狀態的抓取過程的話,執行到 Page.pdf()
這步會先報錯:
因此啓動這個腳本時,須要保持無頭模式:
const browser = await puppeteer.launch({
// 關閉無頭模式,方便咱們看到這個無頭瀏覽器執行的過程
// 注意若調用了 Page.pdf 即保存爲 pdf,則須要保持爲無頭模式
// headless: false,
});
複製代碼
看下執行效果:
個人書架裏有20多本書,下載完後是這樣子:
無頭瀏覽器說白了就是能模擬人工在有頭瀏覽器中的各類操做。那天然不少人力活,都能使用無頭瀏覽器來作(好比上面這個下載 pdf 的過程,實際上是人力打開每個文章頁面,而後按 ctrl+p
或 command+p
保存到本地的自動化過程)。
那既然用自動化工具能解決的事情,就不該該浪費重複的人力勞動了,除此以外咱們還能夠作:
感興趣的同窗能夠關注專欄或者發送簡歷至'qingsheng.lqs####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~