内容简介:前不久,在学校仿微博鲜知微信小程序的时候,正愁数据从哪来,翻到了数据一样的页面微博新鲜事(需退出登录状态),接着用cheerio爬取数据。结果翻车了,检查了一下发现发出请求拿到的body是空的,到微博新鲜事的网页源代码一看,发现...人家的html是js渲染的,应该是还有一次跳转。哇,好狠!本着只要思想不滑坡,办法总比困难多的精神,我用上了puppeteer。就我的使用体验来讲,puppeteer就像是一个完整的浏览器一样,它真正的去解析、渲染页面,所以上面提到因为跳转拿不到页面结构的问题也可以解决。
前不久,在学校仿微博鲜知微信小程序的时候,正愁数据从哪来,翻到了数据一样的页面微博新鲜事(需退出登录状态),接着用cheerio爬取数据。结果翻车了,检查了一下发现发出请求拿到的body是空的,到微博新鲜事的网页源代码一看,发现...人家的html是js渲染的,应该是还有一次跳转。哇,好狠!
本着只要思想不滑坡,办法总比困难多的精神,我用上了puppeteer。
puppeteer
就我的使用体验来讲,puppeteer就像是一个完整的浏览器一样,它真正的去解析、渲染页面,所以上面提到因为跳转拿不到页面结构的问题也可以解决。
话不多说,先来安装试试吧,因为puppeteer还挺大的,安装就用cnpm吧(淘宝镜像的快很多)。
-
安装cnpm,有的话直接跳过
npm install -g cnpm --registry=https://registry.npm.taobao.org
-
安装puppeteer
cnpm i puppeteer -S
-
Hello World
举一个栗子试试就爬微博新鲜事的第一个选项的标题吧
const puppeteer = require('puppeteer'); const url = 'https://weibo.com/?category=novelty'; const sleep = (time) => new Promise((resolve, reject) => { // 因为中间包含一次人为设置的跳转所以只好搞一个sleep等跳转 setTimeout(() => { resolve(true); }, time); }) async function getindex(url) { const browser = await puppeteer.launch({ // 一个浏览器对象 headless: false // puppeteer的功能很强大,但这里用不到无头,就关了 }); const page = await browser.newPage(); // 创建一个新页面 await page.goto(url, { // 跳转到想要的url,并设置跳转等待时间 timeout: 60000 }); await sleep(60000); // 等待第二次跳转完成 const data = await page.$$eval('.UG_list_b', (lists) => { // 相当于document.querySelectorAll('.UG_list_b') var newarr = [Array.from(lists)[0]] // 因为只要第一个,所以把其他的去掉了,若要所有的结果直接取Array.from(lists)即可 return newarr.map(node => { // 遍历数组选择标题 const title = node.querySelector('.list_des .list_title_b a').innerText; return title; }) }); browser.close(); // 关闭浏览器 return data; } getindex(url) .then(res => { console.log(res); }) 复制代码
结果如下
简要的解释一下这里用到的API:
-
puppeteer.launch([object]):
通过 puppeteer.launch([object]) 创建一个 Browser 对象,它通过接收一个非必须的对象参数进行配置。
可以设置字段包括 defaultViewport (默认视口大小), ignoreHTTPSErrors (是否在导航期间忽略 HTTPS 错误), timeout (超时时限)等。
-
browser.newPage()
通过 browser.newPage() 创建一个新的 Page 对象,在浏览器中会打开一个新的标签页。
-
page.goto(url[, options])
根据传入的url,页面导航去相应的页面,它也通过接收一个非必须的对象参数进行配置。
可以设置字段包括 timeout (跳转等待时限), waitUntil (满足什么条件认为页面跳转完成,默认为load)。
但是从这个demo的逻辑来说,只有第二次跳转 passport.weibo.com/visitor/vis… 并渲染完成才认为页面跳转完成,而这第二次跳转是人为设计的,所以直接访问微博新鲜事未跳转完成时返回的状态码仍是200而不是3开头的,使得难以区分是否跳转完成。
-
page.$$eval(selector, pageFunction[, ...args])
selector是选择器,如'.class', '#id', 'a[href]'等
pageFunction是在浏览器实例上下文中要执行的方法
...args是要传给 pageFunction 的参数。
其作用相当于在页面上执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction 并执行,返回的结果也是 pageFunction 返回的。
而 page.evaluate(pageFunction) 有大致相同的功能,还更灵活。
-
browser.close()
这个没什么好说的,就是关闭浏览器,毕竟谷歌浏览器占用内存还是不少的,要是家里有矿的当我没说。
更多详细信息可查询文档
-
实际使用
需求是抓取微博新鲜事页面的标题、头图、作者、时间等信息。
并抓取对应话题点击进去的页面信息,包括其左边分类线戳的类别,类别对应下的所有微博,包括博文、博主、时间、转发数、评论数、点赞数。
还有,要抓取对应话题里所有对应博文的页面信息,包括博文博主,相应的转发、评论、点赞数,以及博文下的所有评论,包括评论层主头像、昵称、评论内容和点赞数。
并将信息都存成json文件。
-
分析
需要暂存爬下来的url地址,并遍历存下爬取的信息。
而且微博设置的障碍还不止有二次跳转,还有随机跳到到访问过于频繁,请24小时后试的页面和未登录状态下随机跳转到weibo.com/login.php 以及 504,这个只要用 page.url() 获取当前网址比对处理即可。
麻烦的只有页面类型复杂繁琐,话题页面有四图、一图、纯文本、视频类型,他们的DOM结构都不同,博文页面也有些许不同,但都不是技术难点。
-
实际代码
小项目我就不分目录了,直接上代码吧
const puppeteer = require('puppeteer'); const fs = require('fs'); const baseurl = 'https://weibo.com'; const Dir = './data/'; const sleep = (time) => new Promise((resolve, reject) => { setTimeout(() => { resolve(true); }, time); }) async function doSpider(url, pageFunction) { // 爬虫函数 const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); await page.goto(url, { timeout: 100000 }); await sleep(60321); if (page.url().indexOf(url) === -1) { console.log('+------------------------------------'); console.log('|失败,当前页面:' + page.url()); console.log('|再次跳转: ' + url); console.log('+------------------------------------'); browser.close(); return doSpider(url, pageFunction); } const data = await page.evaluate(pageFunction, url); browser.close(); return data; } async function runPromiseByQueue(myPromises) { // 利用数组reduce给爬虫执行的结果排序 return await myPromises.reduce( async (previousPromise, nextPromise) => previousPromise.then(await nextPromise), Promise.resolve([]) ); } function saveLocalData(name, data) {// 数据存文件 fs.writeFile(Dir + `${name}.json`, JSON.stringify(data), 'utf-8', err => { if (!err) { console.log(`${name}.json保存成功!`); } }) } const spiderIndex = () => { //新鲜事页面的选择器 const lists = document.querySelectorAll('.UG_list_b'); var newarr = [Array.from(lists)[0], Array.from(lists)[1]] return newarr.map(node => { const title = node.querySelector('.list_des .list_title_b a').innerText; const picUrl = node.querySelector('.pic.W_piccut_v img').getAttribute('src'); const newUrl = node.querySelector('.list_des .list_title_b a').getAttribute('href'); const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src'); const userName = node.querySelector('.subinfo_box a .subinfo').innerText; const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText; return { title, picUrl, newUrl, userImg, userName, time } }) } const spiderTopic = (url) => { // 话题页面的选择器 const picUrl = document.querySelector('.UG_list_e .list_nod .pic img').getAttribute('src'); const topicTitle = document.querySelector('.UG_list_e .list_title').innerText; const des = document.querySelector('.UG_list_e .list_nod .list_des').innerText; const userImg = document.querySelector('.UG_list_e .list_nod .subinfo_box a .subinfo_face img').getAttribute('src'); const userName = document.querySelector('.UG_list_e .list_nod .subinfo_box a .subinfo').innerText; const time = document.querySelector('.UG_list_e .list_nod .subinfo_box>.subinfo.S_txt2').innerText; const lists = document.querySelectorAll('.UG_content_row'); const types = Array.from(lists).map(node => { const title = node.querySelector('.UG_row_title').innerText; const v2list = node.querySelectorAll('.UG_list_v2 .list_des'); var v2Item = Array.from(v2list).map(node => { const content = node.querySelector('h3').innerText; const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src'); const userName = node.querySelector('.subinfo_box a .subinfo').innerText; const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText; const like = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(1) em:nth-of-type(2)').innerText; const comment = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(3) em:nth-of-type(2)').innerText; const relay = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(5) em:nth-of-type(2)').innerText; const newUrl = node.getAttribute('href'); return newitem = { content, userImg, userName, time, like, comment, relay, newUrl, img: [] } }) const alist = node.querySelectorAll('.UG_list_a'); var aItem = Array.from(alist).map(node => { const content = node.querySelector('h3').innerText; const newUrl = node.getAttribute('href'); const img1 = node.querySelector('.list_nod .pic:nth-of-type(1) img').getAttribute('src'); const img2 = node.querySelector('.list_nod .pic:nth-of-type(2) img').getAttribute('src'); const img3 = node.querySelector('.list_nod .pic:nth-of-type(3) img').getAttribute('src'); const img4 = node.querySelector('.list_nod .pic:nth-of-type(4) img').getAttribute('src'); const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src'); const userName = node.querySelector('.subinfo_box a .subinfo').innerText; const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText; const like = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(2) em:nth-of-type(2)').innerText; const comment = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(4) em:nth-of-type(2)').innerText; const relay = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(6) em:nth-of-type(2)').innerText; return newitem = { img: [img1, img2, img3, img4], content, userImg, userName, time, like, comment, relay, newUrl } }) const blist = node.querySelectorAll('.UG_list_b'); var bItem = Array.from(blist).map(node => { const content = node.querySelector('.list_des h3').innerText; const newUrl = node.getAttribute('href'); var img = '' if (node.querySelector('.pic img') != null) { img = node.querySelector('.pic img').getAttribute('src'); } const userImg = node.querySelector('.list_des .subinfo_box a .subinfo_face img').getAttribute('src'); const userName = node.querySelector('.list_des .subinfo_box a .subinfo').innerText; const time = node.querySelector('.list_des .subinfo_box>.subinfo.S_txt2').innerText; const like = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(2) em:nth-of-type(2)').innerText; const comment = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(4) em:nth-of-type(2)').innerText; const relay = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(6) em:nth-of-type(2)').innerText; return newitem = { img: [img], content, userImg, userName, time, like, comment, relay, newUrl } }) return { title, list: [...v2Item, ...aItem, ...bItem] } }) return { topicTitle, picUrl, des, userImg, userName, time, newUrl: url, types } } const spiderPage = (url) => { // 博文页面的选择器 var data = {} const content = document.querySelector('.WB_text.W_f14').innerText; const piclist = document.querySelectorAll('.S_bg1.S_line2.bigcursor.WB_pic'); const picUrls = Array.from(piclist).map(node => { const picUrl = node.querySelector('img').getAttribute('src'); return picUrl; }) const time = document.querySelector('.WB_detail>.WB_from.S_txt2>a').innerText; const userImg = document.querySelector('.WB_face>.face>a>img').getAttribute('src'); const userName = document.querySelector('.WB_info>a').innerText; const like = document.querySelector('.WB_row_line>li:nth-of-type(4) em:nth-of-type(2)').innerText; const comment = document.querySelector('.WB_row_line>li:nth-of-type(3) em:nth-of-type(2)').innerText; const relay = document.querySelector('.WB_row_line>li:nth-of-type(2) em:nth-of-type(2)').innerText; const lists = document.querySelectorAll('.list_box>.list_ul>.list_li[comment_id]'); const commentItems = Array.from(lists).map(node => { const userImg = node.querySelector('.WB_face>a>img').getAttribute('src'); const userName = node.querySelector('.list_con>.WB_text>a[usercard]').innerText; const [frist, ...contentkey] = [...node.querySelector('.list_con>.WB_text').innerText.split(':')] const content = [...contentkey].toString(); const time = node.querySelector('.WB_func>.WB_from').innerText const like = node.querySelector('.list_con>.WB_func [node-type=like_status]>em:nth-of-type(2)').innerText return { userImg, userName, content, time, like } }) return data = { newUrl: url, content, picUrls, time, userImg, userName, like, comment, relay, commentItems } } function start() { //主要操作,不封起来太难看了 doSpider(baseurl + '/?category=novelty', spiderIndex) //爬完新鲜事页面给数据 .then(async data => { await saveLocalData('Index', data);// 爬完新鲜事页面给的数据存成叫Index的文件 return data.map(item => item.newUrl);// 把下一次爬的url都取出来 }) .then(async (urls) => { let doSpiders = await urls.map(async url => {//把url全部爬上就绪,返回函数待 排序 处理 if (url[1] === '/') {// 有部分url只缺协议部分不缺baseurl,加个区分 let newdata = await doSpider('https:' + url, spiderTopic); return async (data) => [...data, newdata] } else { let newdata = await doSpider(baseurl + url, spiderTopic); return async (data) => [...data, newdata] } }) let datas = await runPromiseByQueue(doSpiders);//挖坑排序并执行 return datas; }) .then(async data => { saveLocalData('Topic', data);// 数据存文件 let list = []; await data.forEach(item =>// 取出下一次爬的所有url item.types.forEach(type => type.list.forEach(item => list.push(item.newUrl) ) ) ) return list; }) .then(async (urls) => { let doSpiders = await urls.map(async url => {// 重复上述的爬虫就绪 if (url[1] === '/') { let newdata = await doSpider('https:' + url, spiderPage); return (data) => [...data, newdata] } else { let newdata = await doSpider(baseurl + url, spiderPage); return (data) => [...data, newdata] } }) let datas = await runPromiseByQueue(doSpiders);// 重复一样的爬虫操作,并根据挖好的坑对结果排序 return datas; }) .then(async data => { saveLocalData('Page', data);// 数据存成文件 }) } start(); 复制代码
-
运行过程简要说明:
运行可以分三个过程( 爬 新鲜事页面获取包括多个话题页面url在内的信息并存储、 并发爬 话题页面获取包括多个博文页面url在内的信息并存储、 并发爬 博文页面包括所有评论在内的信息并存储),并且数据可以通过存下来的newUrl字段来匹配主从,建立联系。
我原来写过一个把所有爬数据的异步操作都串联起来的版本,但是效率太低了,就用数组的reduce方法挖坑排序后直接并发操作,大大提升了效率(我跟你港哦,这个reduce真的好好用豁)。
下面是控制台输出和3个爬下来的数据文件(行数太多影响观感就格式化后截成图):
这份代码只爬了新鲜事里的头两个话题,爬的页面加起来13个,并没有爬列表里所有项,要爬所有项的话,改相应那行的代码就好spiderIndex里newarr的值即可,如
var newarr = Array.from(lists)
。剩下的,就交给时间吧...建议睡个觉享受生活,起来说不定就好了。也有另一种可能,大眼怪(新浪)看你请求太多,暂时把你IP拒了。最后奉上github库 代码和数据文件都在这
写在最后
马上就要大四,学到现在,h5,小程序,vue,react,node,java都写过,设计模式、函数式编程、懒加载杂七杂八之类啥的平时逮到啥学啥。其实也挺开心走了编程,一步一步实现也感觉不错,秋招我应该也会去找实习,在这问问大佬们去实习前还有没有啥要注意的,第一次准备有点无从下手的感觉。
有啥错误请务必指出来互相交流学习,毕竟我还菜嘛,如果方便的话,能留个赞么,谢谢啦。
-
以上所述就是小编给大家介绍的《爬虫爬不到数据?试试puppeteer(Node.js)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 写爬虫还在用 python?快来试试 go 语言的爬虫框架吧
- ClickHouse 性能优化?试试物化视图
- 试试 kaggle 竞赛:辨别猫狗
- 创建复杂对象,试试建造者模式
- SpringMVC配置太多?试试SpringBoot
- 记一次前端面试试水笔记
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web2.0策略指南
艾米 / 2009-3 / 39.00元
《Web2.0策略指南》是有关战略的。书中的示例关注的是Web 2.0的效率,而不是聚焦于技术。你将了解到这样一个事实:创建Web 210业务或将Web 2.0战略整合到现在业务中,意味着创建一个吸引人们前来访问的在线站点,让人们愿意到这里来共享他们的思想、见闻和行动。当人们通过Web走到一起时,结果可能远远大于各部分的和。随着传统的“口碑传诵”助推站点高速成长,客户本身就能够帮助建立站点。 ......一起来看看 《Web2.0策略指南》 这本书的介绍吧!