puppeteer+mysql—爬蟲新方法!抓取新聞&評論so easy!
Puppeteer 是 Google Chrome 團隊官方的無介面(Headless)Chrome 工具。正因為這個官方宣告,許多業內自動化測試庫都已經停止維護,包括 PhantomJS。Selenium IDE for Firefox 專案也因為缺乏維護者而終止。
Summary
本文將使用Chrome Headless,Puppeteer,Node和Mysql,爬取新浪微博。登入並爬取人民日報主頁的新聞,並儲存在Mysql資料庫中。
安裝
安裝Puppeteer會有一定機率因為無法下載Chromium驅動包而失敗。在上一篇文章中有介紹過Puppeteer安裝解決方案,本文就不多做介紹了。 puppetter安裝就踩坑-解決篇
上手
我們先從擷取頁面開始,瞭解Puppeteer啟動瀏覽器並完成工作的一些api。
screenshot.js
const puppeteer = require('puppeteer'); (async () => { const pathToExtension = require('path').join(__dirname, '../chrome-mac/Chromium.app/Contents/MacOS/Chromium'); const browser = await puppeteer.launch({ headless: false, executablePath: pathToExtension }); const page = await browser.newPage(); await page.setViewport({width: 1000, height: 500}); await page.goto('https://weibo.com/rmrb'); await page.waitForNavigation(); await page.screenshot({path: 'rmrb.png'}); await browser.close(); })(); 複製程式碼
- puppeteer.launch(當 Puppeteer 連線到一個 Chromium 例項的時候會通過
puppeteer.launch
或puppeteer.connect
建立一個 Browser 物件。)- executablePath:啟動Chromium 或者 Chrome的路徑。
- headless:是否以headless形式啟動瀏覽器。(Headless Chrome指在headless模式下執行谷歌瀏覽器。用於自動化測試和不需要視覺化使用者介面的伺服器)
- browser.newPage 新開頁面並返回一個Promise。Puppeteer api中大部分方法會返回
Promise
物件,我們需要async+await
配合使用。
執行程式碼
$ node screenshot.js 複製程式碼
截圖會被儲存至根目錄下

分析頁面結構並提取新聞
我們的目的是拿到人民日報發的微博文字和日期。
- 新聞節點dom:div[action-type=feed_list_item]
- 新聞內容在dom:div[action-type=feed_list_item]>.WB_detail>.WB_text
- 新聞釋出時間dom:div[action-type=feed_list_item]>.WB_detail>.WB_from a").eq(0).attr("date")
Puppeteer提供了頁面元素提取方法: Page.evaluate
。因為它作用於瀏覽器執行的上下文環境內。當我們載入好頁面後,使用 Page.evaluate
方法可以用來分析dom節點
page.evaluate(pageFunction, ...args)
pageFunction ...args pageFunction
如果pageFunction返回的是[Promise],page.evaluate將等待promise完成,並返回其返回值。
如果pageFunction返回的是不能序列化的值,將返回undefined
分析微博頁面資訊的程式碼如下:
const LIST_SELECTOR = 'div[action-type=feed_list_item]' return await page.evaluate((infoDiv)=> { return Array.prototype.slice.apply(document.querySelectorAll(infoDiv)) .map($userListItem => { var weiboDiv = $($userListItem) var webUrl = 'http://weibo.com' var weiboInfo = { "tbinfo": weiboDiv.attr("tbinfo"), "mid": weiboDiv.attr("mid"), "isforward": weiboDiv.attr("isforward"), "minfo": weiboDiv.attr("minfo"), "omid": weiboDiv.attr("omid"), "text": weiboDiv.find(".WB_detail>.WB_text").text().trim(), 'link': webUrl.concat(weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("href")), "sendAt": weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("date") }; if (weiboInfo.isforward) { var forward = weiboDiv.find("div[node-type=feed_list_forwardContent]"); if (forward.length > 0) { var forwardUser = forward.find("a[node-type=feed_list_originNick]"); var userCard = forwardUser.attr("usercard"); weiboInfo.forward = { name: forwardUser.attr("nick-name"), id: userCard ? userCard.split("=")[1] : "error", text: forward.find(".WB_text").text().trim(), "sendAt": weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("date") }; } } return weiboInfo }) }, LIST_SELECTOR) 複製程式碼
我們將新聞塊 LIST_SELECTOR 作為引數傳入 page.evaluate
,在 pageFunction
函式的頁面例項上下中可以使用document方法操作dom節點。 遍歷新聞塊div,分析dom結構,拿到對應的資訊。
引申一下~
因為我覺得用原生JS方法操作dom節點不習慣(jQuery慣出的低能就是我,對jQuery極度依賴...2333),所以我決定讓開發環境支援jQuery。
方法一
page.addScriptTag(options)
注入一個指定src(url)或者程式碼(content)的 script 標籤到當前頁面。
- options <[Object]>
- url <[string]> 要新增的script的src path <[string]> 要注入frame的js檔案路徑. 如果 path 是相對路徑, 那麼相對 當前路徑 解析。
- content <[string]> 要注入頁面的js程式碼(即) type <[string]> 指令碼型別。 如果要注入 ES6 module,值為'module'。點選 script 檢視詳情。
- 返回: <[Promise]<[ElementHandle]>> Promise物件,即注入完成的tag標籤。當 script 的 onload 觸發或者程式碼被注入到 frame。
所以我們直接在程式碼裡新增:
await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.2.1.min.js'}) 複製程式碼
然後就可以愉快得飛起了!
方法二
如果訪問的網頁本來就支援jQuery,那就更方便了!
await page.evaluate(()=> { var $ = window.$ }) 複製程式碼
直接pageFunction中聲名變數並用 window中的$賦值就好了。
引申結束~
注意:pageFunctin中存在頁面例項,如果在程式其他地方使用document或者jquery等方法,會提示需要document環境或者直接報錯
(node:3346) UnhandledPromiseRejectionWarning: ReferenceError: document is not defined 複製程式碼
提取每條新聞的評論
光抓取新聞還不夠,我們還需要每條新聞的熱門評論。
我們發現,點選操作欄的評論按鈕後,會去載入新聞的評論。

- 我們在分析完新聞塊dom元素後,模擬點選評論按鈕
$('.WB_handle span[node-type=comment_btn_text]').each(async(i, v)=>{ $(v).trigger('click') }) 複製程式碼
- 使用監聽事件event: 'response',監聽頁面請求
event: 'response'
- <[Response]>
當頁面的某個請求接收到對應的 [response] 時觸發。
如圖:當我們點選了評論按鈕,瀏覽器會發送很多請求,我們的目的是抽取出comment請求。

我們需要用到class Response中的幾個方法,監聽瀏覽器的的響應並分析並將評論提取出來。
- response.url() Contains the URL of the response.
- response.text()
- returns: <Promise> Promise which resolves to a text representation of response body.
page.on('response', async(res)=> { const url = res.url() if (url.indexOf('small') > -1) { let text = await res.text() var mid = getQueryVariable(res.url(), 'mid'); var delHtml = delHtmlTag(JSON.parse(text).data.html) var matchReg = /\:.*?(?= )/gi; var matchRes = delHtml.match(matchReg) if (matchRes && matchRes.length) { let comment = [] matchRes.map((v)=> { comment.push({mid, content: JSON.stringify(v.split(':')[1])}) }) pool.getConnection(function (err, connection) { save.comment({"connection": connection, "res": comment}, function () { console.log('insert success') }) }) } } }) 複製程式碼
res.url() res.text()
儲存到Mysql
使用Mysql儲存新聞和評論
$ npm i mysql -D mysql 複製程式碼
我們使用的mysql是一個node.js驅動的庫。它是用JavaScript編寫的,不需要編譯。
- 新建config.js,建立本地資料庫連線,並把配置匯出。
config.js
var mysql = require('mysql'); var ip = 'http://127.0.0.1:3000'; var host = 'localhost'; var pool = mysql.createPool({ host:'127.0.0.1', user:'root', password:'xxxx', database:'yuan_place', connectTimeout:30000 }); module.exports = { ip: ip, pool: pool, host: host, } 複製程式碼
- 在爬蟲程式中引入config.js
page.on('response', async(res)=> { ... if (matchRes && matchRes.length) { let comment = [] matchRes.map((v)=> { comment.push({mid, content: JSON.stringify(v.split(':')[1])}) }) pool.getConnection(function (err, connection) { save.comment({"connection": connection, "res": comment}, function () { console.log('insert success') }) }) } ... }) const content = await getWeibo(page) pool.getConnection(function (err, connection) { save.content({"connection": connection, "res": content}, function () { console.log('insert success') }) }) 複製程式碼
- 然後我們寫一個save.js專門處理資料插入邏輯。
兩個表的結構如下:

現在我們可以開始愉快得往資料庫塞資料了。
save.js
exports.content = function(list,callback){ console.log('save news') var connection = list.connection async.forEach(list.res,function(item,cb){ debug('save news',JSON.stringify(item)); var data = [item.tbinfo,item.mid,item.isforward,item.minfo,item.omid,item.text,new Date(parseInt(item.sendAt)),item.cid,item.clink] if(item.forward){ var fo = item.forward data = data.concat([fo.name,fo.id,fo.text,new Date(parseInt(fo.sendAt))]) }else{ data = data.concat(['','','',new Date()]) } connection.query('select * from sina_content where mid = ?',[item.mid],function (err,res) { if(err){ console.log(err) } if(res && res.length){ //console.log('has news') cb(); }else{ connection.query('insert into sina_content(tbinfo,mid,isforward,minfo,omid,text,sendAt,cid,clink,fname,fid,ftext,fsendAt) values(?,?,?,?,?,?,?,?,?,?,?,?,?)',data,function(err,result){ if(err){ console.log('kNewscom',err) } cb(); }) } }) },callback); } //把文章列表存入資料庫 exports.comment = function(list,callback){ console.log('save comment') var connection = list.connection async.forEach(list.res,function(item,cb){ debug('save comment',JSON.stringify(item)); var data = [item.mid,item.content] connection.query('select * from sina_comment where mid = ?',[item.mid],function (err,res) { if(res &&res.length){ cb(); }else{ connection.query('insert into sina_comment(mid,content) values(?,?)',data,function(err,result){ if(err){ console.log(item.mid,item.content,item) console.log('comment',err) } cb(); }); } }) },callback); } 複製程式碼
執行程式,就會發現資料已經在庫裡了。
對專案無用且麻煩的進階:模擬登入
到這裡不用登入,已經可以愉快得爬新聞和評論了。但是!追求進步的我們怎麼能就此停住。做一些對專案無用的登入小元件吧!需要就引入,不需要就保持原樣。
在專案根目錄新增一個 creds.js 檔案。
module.exports = { username: '<GITHUB_USERNAME>', password: '<GITHUB_PASSWORD>' }; 複製程式碼
- 使用
page.click
模擬頁面點選
page.click(selector[, options])
- selector A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
-
options
- button left, right, or middle, defaults to left.
- clickCount defaults to 1. See UIEvent.detail.
- delay Time to wait between mousedown and mouseup in milliseconds. Defaults to 0.
- returns: Promise which resolves when the element matching selector is successfully clicked. The Promise will be rejected if there is no element matching selector.
因為page.click返回的是Promise,所以用await暫停。
await page.click('.gn_login_list li a[node-type="loginBtn"]'); 複製程式碼
- await page.waitFor(2000) 等待2s,讓輸入框顯示出來。
- 使用
page.type
輸入使用者名稱、密碼(這裡我們為了模擬使用者輸入的速度,加了{delay:30}
引數,可以根據實際情況修改),再模擬點選登入按鈕,使用page.waitForNavigation()
等待頁面登入成功後的跳轉。
await page.type('input[name=username]',CREDS.username,{delay:30}); await page.type('input[name=password]',CREDS.password,{delay:30}); await page.click('.item_btn a'); await page.waitForNavigation(); 複製程式碼
因為我使用的測試賬號沒有繫結手機號,所以用以上的方法可以完成登入。如果綁定了手機號的小夥伴,需要用客戶端掃描二次認證。
最後
爬蟲效果圖:
爬蟲的demo在這裡: github.com/wallaceyuan…
覺得好玩就關注一下~ 歡迎大家收藏寫評論~~~