内容简介:Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具。正因为这个官方声明,许多业内自动化测试库都已经停止维护,包括 PhantomJS。Selenium IDE for Firefox 项目也因为缺乏维护者而终止。本文将使用Chrome Headless,Puppeteer,Node和Mysql,爬取新浪微博。登录并爬取人民日报主页的新闻,并保存在Mysql数据库中。安装Puppeteer会有一定几率因为无法下载Chromium驱动包而失败。在上一篇文
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…
觉得好玩就关注一下~ 欢迎大家收藏写评论~~~
以上所述就是小编给大家介绍的《puppeteer+mysql—爬虫新方法!抓取新闻&评论so easy!》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Python爬虫:抓取新浪新闻数据
- Python爬虫:抓取新浪新闻数据
- python爬虫-- 抓取网页、图片、文章
- Python爬虫抓取技术的门道
- 如何使用代理IP进行数据抓取,PHP爬虫抓取亚马逊商品数据
- 利用Python网络爬虫抓取网易云歌词
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Design Handbook
Baeck, Philippe de 编 / 2009-12 / $ 22.54
This non-technical book brings together contemporary web design's latest and most original creative examples in the areas of services, media, blogs, contacts, links and jobs. It also traces the latest......一起来看看 《Web Design Handbook》 这本书的介绍吧!