puppeteer+mysql—爬蟲新方法!抓取新聞&評論so easy!

Puppeteer 是 Google Chrome 團隊官方的無界面(Headless)Chrome 工具。正由於這個官方聲明,許多業內自動化測試庫都已經中止維護,包括 PhantomJS。Selenium IDE for Firefox 項目也由於缺少維護者而終止。html

Summary

本文將使用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 鏈接到一個 Chromium 實例的時候會經過 puppeteer.launchpuppeteer.connect 建立一個 Browser 對象。)
    • executablePath:啓動Chromium 或者 Chrome的路徑。
    • headless:是否以headless形式啓動瀏覽器。(Headless Chrome指在headless模式下運行谷歌瀏覽器。用於自動化測試和不須要可視化用戶界面的服務器)
  • browser.newPage 新開頁面並返回一個Promise。Puppeteer api中大部分方法會返回Promise對象,咱們須要async+await配合使用。

運行代碼github

$ node screenshot.js
複製代碼

截圖會被保存至根目錄下web

分析頁面結構並提取新聞

咱們的目的是拿到人民日報發的微博文字和日期。sql

  • 新聞節點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節點chrome

page.evaluate(pageFunction, ...args)數據庫

  • pageFunction <[function]|[string]> 要在頁面實例上下文中執行的方法
  • ...args<...[Serializable]|[JSHandle]> 要傳給 pageFunction 的參數
  • 返回: <[Promise]<[Serializable]>> 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
複製代碼

提取每條新聞的評論

光抓取新聞還不夠,咱們還須要每條新聞的熱門評論。

咱們發現,點擊操做欄的評論按鈕後,會去加載新聞的評論。

  1. 咱們在分析完新聞塊dom元素後,模擬點擊評論按鈕
$('.WB_handle span[node-type=comment_btn_text]').each(async(i, v)=>{
    $(v).trigger('click')
})
複製代碼
  1. 使用監聽事件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')
                })
            })
        }
    }
})
複製代碼
  1. res.url()獲取到響應的url,判斷string中是否含有small關鍵字。
  2. 截取url中的key:mid,mid用來區分評論屬於哪一條新聞。
  3. res.text()獲取響應的body,並去除body.data中dom的html標籤。
  4. 在純文字中 提取出咱們須要的評論內容。

保存到Mysql

使用Mysql儲存新聞和評論

$ npm i mysql -D mysql
複製代碼

咱們使用的mysql是一個node.js驅動的庫。它是用JavaScript編寫的,不須要編譯。

  1. 新建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,
}
複製代碼
  1. 在爬蟲程序中引入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')
    })
})
複製代碼
  1. 而後咱們寫一個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>'
};
複製代碼
  1. 使用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"]');
    複製代碼

    1. await page.waitFor(2000) 等待2s,讓輸入框顯示出來。
    2. 使用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…

    以爲好玩就關注一下~ 歡迎你們收藏寫評論~~~

相關文章
相關標籤/搜索