前不久,在学校仿微博鲜知微信小程序的时候,正愁数据从哪来,翻到了数据同样的页面微博新鲜事(需退出登陆状态),接着用cheerio爬取数据。结果翻车了,检查了一下发现发出请求拿到的body是空的,到微博新鲜事的网页源代码一看,发现...人家的html是js渲染的,应该是还有一次跳转。哇,好狠!javascript
本着只要思想不滑坡,办法总比困难多的精神,我用上了puppeteer。php
就个人使用体验来说,puppeteer就像是一个完整的浏览器同样,它真正的去解析、渲染页面,因此上面提到由于跳转拿不到页面结构的问题也能够解决。html
话很少说,先来安装试试吧,由于puppeteer还挺大的,安装就用cnpm吧(淘宝镜像的快不少)。vue
安装cnpm,有的话直接跳过java
npm install -g cnpm --registry=https://registry.npm.taobao.org
node
安装puppeteerreact
cnpm i puppeteer -S
git
Hello Worldgithub
举一个栗子试试就爬微博新鲜事的第一个选项的标题吧 npm
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);
})
复制代码
结果以下
经过 puppeteer.launch([object]) 建立一个 Browser 对象,它经过接收一个非必须的对象参数进行配置。
能够设置字段包括 defaultViewport (默认视口大小), ignoreHTTPSErrors (是否在导航期间忽略 HTTPS 错误), timeout (超时时限)等。
经过 browser.newPage() 建立一个新的 Page 对象,在浏览器中会打开一个新的标签页。
根据传入的url,页面导航去相应的页面,它也经过接收一个非必须的对象参数进行配置。
能够设置字段包括 timeout (跳转等待时限), waitUntil (知足什么条件认为页面跳转完成,默认为load)。
可是从这个demo的逻辑来讲,只有第二次跳转 passport.weibo.com/visitor/vis… 并渲染完成才认为页面跳转完成,而这第二次跳转是人为设计的,因此直接访问微博新鲜事未跳转完成时返回的状态码还是200而不是3开头的,使得难以区分是否跳转完成。
selector是选择器,如'.class', '#id', 'a[href]'等
pageFunction是在浏览器实例上下文中要执行的方法
...args是要传给 pageFunction 的参数。
其做用至关于在页面上执行 Array.from(document.querySelectorAll(selector)),而后把匹配到的元素数组做为第一个参数传给 pageFunction 并执行,返回的结果也是 pageFunction 返回的。
而 page.evaluate(pageFunction) 有大体相同的功能,还更灵活。
这个没什么好说的,就是关闭浏览器,毕竟谷歌浏览器占用内存仍是很多的,要是家里有矿的当我没说。
更多详细信息可查询文档
需求是抓取微博新鲜事页面的标题、头图、做者、时间等信息。
须要暂存爬下来的url地址,并遍历存下爬取的信息。
并且微博设置的障碍还不止有二次跳转,还有随机跳到到访问过于频繁,请24小时后试的页面和未登陆状态下随机跳转到 weibo.com/login.php 以及 504,这个只要用 page.url() 获取当前网址比对处理便可。
小项目我就不分目录了,直接上代码吧
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都写过,设计模式、函数式编程、懒加载杂七杂八之类啥的平时逮到啥学啥。其实也挺开心走了编程,一步一步实现也感受不错,秋招我应该也会去找实习,在这问问大佬们去实习前还有没有啥要注意的,第一次准备有点无从下手的感受。
有啥错误请务必指出来互相交流学习,毕竟我还菜嘛,若是方便的话,能留个赞么,谢谢啦。