Puppeteer 是 Google Chrome 團隊官方的無界面(Headless)Chrome 工具。正由於這個官方聲明,許多業內自動化測試庫都已經中止維護,包括 PhantomJS。Selenium IDE for Firefox 項目也由於缺少維護者而終止。html
本文將使用Chrome Headless,Puppeteer,Node和Mysql,爬取新浪微博。登陸並爬取人民日報主頁的新聞,並保存在Mysql數據庫中。node
安裝Puppeteer會有必定概率由於沒法下載Chromium驅動包而失敗。在上一篇文章中有介紹過Puppeteer安裝解決方案,本文就很少作介紹了。puppetter安裝就踩坑-解決篇 mysql
咱們先從截取頁面開始,瞭解Puppeteer啓動瀏覽器並完成工做的一些api。jquery
screenshot.jsgit
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.connect
建立一個 Browser 對象。)
Promise
對象,咱們須要async+await
配合使用。運行代碼github
$ node screenshot.js
複製代碼
截圖會被保存至根目錄下web
咱們的目的是拿到人民日報發的微博文字和日期。sql
Puppeteer提供了頁面元素提取方法:Page.evaluate
。由於它做用於瀏覽器運行的上下文環境內。當咱們加載好頁面後,使用 Page.evaluate
方法能夠用來分析dom節點chrome
page.evaluate(pageFunction, ...args)數據庫
pageFunction
<[function]|[string]> 要在頁面實例上下文中執行的方法...args
<...[Serializable]|[JSHandle]> 要傳給 pageFunction 的參數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 標籤到當前頁面。
因此咱們直接在代碼裏添加:
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
複製代碼
光抓取新聞還不夠,咱們還須要每條新聞的熱門評論。
咱們發現,點擊操做欄的評論按鈕後,會去加載新聞的評論。
$('.WB_handle span[node-type=comment_btn_text]').each(async(i, v)=>{
$(v).trigger('click')
})
複製代碼
event: 'response'
- <[Response]>
當頁面的某個請求接收到對應的 [response] 時觸發。
如圖:當咱們點擊了評論按鈕,瀏覽器會發送不少請求,咱們的目的是抽取出comment請求。
咱們須要用到class Response中的幾個方法,監聽瀏覽器的的響應並分析並將評論提取出來。
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()
獲取到響應的url,判斷string中是否含有small關鍵字。res.text()
獲取響應的body,並去除body.data中dom的html標籤。使用Mysql儲存新聞和評論
$ npm i mysql -D mysql
複製代碼
咱們使用的mysql是一個node.js驅動的庫。它是用JavaScript編寫的,不須要編譯。
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,
}
複製代碼
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
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])
由於page.click返回的是Promise,因此用await暫停。
await page.click('.gn_login_list li a[node-type="loginBtn"]');
複製代碼
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…
參考文檔:
github.com/GoogleChrom… github.com/GoogleChrom…