用puppeteer有一段时间,有一些心得,特在此记录一下!
puppeteer的使用,我们首先是可以看这个网站。
https://zhaoqize.github.io/puppeteer-api-zh_CN/#
虽然文档有些api没有代码示例,但是基本的demo是可以的。下面我准备按照实战代码,和一些使用上的常见问题进行介绍。
const puppeteer = require('puppeteer');
(async () => {
const args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-infobars',
'--window-position=0,0',
'--ignore-certifcate-errors',
'--ignore-certifcate-errors-spki-list',
'--user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3312.0 Safari/537.36"'
];
const options = {
args,
headless: false,
ignoreHTTPSErrors: true,
userDataDir: './tmp2',
defaultViewport:{width:1280,height:800}
};
// 后面都可以基于这些代码套一个壳
const browser = await puppeteer.launch(options);
const browserWSEndpoint = browser.wsEndpoint();
console.log(browserWSEndpoint,'site')
const page = await browser.newPage();
await page.goto('https://www.bilibili.com/v/music/original/?spm_id_from=333.5.b_6d757369635f6f726967696e616c.59#/all/click/0/1/?open=hot');
})()
//存贮节点
const browserWSEndpoint = browser.wsEndpoint();
// 使用节点来重新建立连接
const browser2 = await puppeteer.connect({browserWSEndpoint});
await page.waitForSelector('#videolist_box > div.vd-list-cnt > ul > li > div > div.r > a');
4.page 的$$ 和$
其实就是对应于document.querySelectorAll
和document.querySelector
let btn = await mpage.$('div[aria-label="Create a post"]')
let btn = await mpage.$$('div[aria-label="Create a post"]')
$$eval其实没什么区别就是可以有个第二个参数传回调函数,回调你这个元素的数组,如果没找到是 []
let title = await page.$$eval('#videolist_box > div.vd-list-cnt > ul > li > div > div.r > a',
link=>link.map(v=>{ return { title:v.innerText, href:v.href} })
)
5.等待操作
await mpage.waitForTimeout(400);
6.关于多层查找后再点击
let btn = await mpage.$('div[aria-label="Create a post"]')
await mpage.waitForTimeout(400);
let btn2= await btn.asElement().$$('div[data-visualcompletion="ignore"]')
await mpage.waitForTimeout(400);
await btn2[1].asElement().click()
我们查找的时候page. 或者 p a g e . 或者page. 或者page.$是返回jshandle对象,而点击事件是elementHandle对象才可以执行,所以需要asElement转换对象类型。
7.关于如何上传文件(图片/视频)
const uploadFile = await mpage.$$('input[type="file"]');
if(filePath){
await uploadFile[uploadFile.length-1].uploadFile(filePath);
}
filePath是你的要上传的文件目录,这个是可以提前知道的。
8.键盘操作
8.1 打一段字
let txt ='hello'
await mpage.keyboard.type( txt, { delay: 400 });
8.2 如何在脚本内部自实现control+c control+v
const puppeteer = require('puppeteer');
(async () => {
const args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-infobars',
'--window-position=0,0',
'--ignore-certifcate-errors',
'--ignore-certifcate-errors-spki-list',
'--user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3312.0 Safari/537.36"'
];
const options = {
args,
headless: false,
ignoreHTTPSErrors: true,
userDataDir: './tmp2',
defaultViewport:{width:1280,height:800}
};
const browser = await puppeteer.launch(options);
const browserWSEndpoint = browser.wsEndpoint();
console.log(browserWSEndpoint,'site')
const page = await browser.newPage();
page.on('dialog', async dialog => {
console.log(dialog.message());
await dialog.dismiss();
});
await page.goto('https://www.bilibili.com/');
await page.waitForSelector('input.nav-search-keyword')
await page.focus('input.nav-search-keyword')
await page.evaluate(async () => {
// try {
let data = '12311ff'
//创建一个input框
let input = document.createElement('input');
//设置id
input.id = "temp_copy";
//设置只读
input.setAttribute('readonly', 'readonly');
input.setAttribute('fao', 'fao');
//赋值
input.setAttribute('value', data);
//把input添加到body中
document.body.appendChild(input);
//选中input框,取值复制
console.log(input)
document.body.querySelector('input[fao="fao"]').select()
document.execCommand("copy",false,null);
//删除input框
document.body.removeChild(input);
// } catch (error) {}
});
await page.waitFor(1000)
await page.focus('input.nav-search-keyword')
// 直接粘贴
await page.keyboard.down('Control');
await page.keyboard.press('KeyV',{delay:100});
await page.keyboard.up('Control');
})();
这里的话down是代表键盘按下,up才会释放,press相当于down + up ,delay是按下和释放的间隔时间,具体可以看代码,可以直接运行。
键盘操作对应表受限于篇幅,可以自己百度
9.如何进行分页监听
await mpage.setRequestInterception(true);
mpage.on("request", async (request) => {
request.continue();
});
mpage.on("response", async (response) => {
const req = response.request();
if (req.url().indexOf("分页请求的地址") > 0) {
//操作
}
})
10.获取节点必备
当网络差时,会出现页面加载很久。那么你使用waitfor是没有用处的。所以用事件监听触发在取节点。
mpage.on('load',async ()=>{
console.log('页面加载完毕')
let re = doCloseNotification(mpage, { from: id }); // 我的操作
})
不使用文字作为该节点的判断依据
尽量不要使用文字作为节点的获取依据,有些网站允许个人设置自己首页的语言,这会导致基于文字的节点获取方式失效。
不基于类名,或者xpath
在我爬取facebook网站的时候,人家网站的类名是动态改变的,更麻烦的是dom节点的层数都是会改变,这导致你直接失效,这两种方式根本行不通。可行的替代方案是基于自定义属性查找,不过这只是通法。我不是反对这两种,只要代码易读,简单就可以使用。
节点等待
节点获取到的前提是加载完成,有时候必须进行等待,当出问题的时候,往往是由于节点没有出现,而使用了waitForSelector()导致的
foucs获取不到的问题
有些网站的某些输入框采用自定义的框架来显示。是使用div模拟的文本输入框,并不是常见的input输入框。然后这个问题可能发生在你使用page.$evalute ,基于document的focus.使用官方的.focus()方法可以很好的进行获取焦点。
模态框的节点获取失败
模态框的节点,通常是点击后动态加载来的,我们需要进行必要的等待,如果还没有,就需要逐层递进,先取到整个模态框的dom节点,在基于他在往下找。
点击节点没有效果
比较复杂的网站,往往dom节点层数很深,我们点不到,是由于获取的那一个节点不是人家代码绑定点击事件的那一层,而事件冒泡的问题他们程序员都处理的很好,基本不会存在。所以没什么其他方法,一层一层的dom去click()一下,肯定有一个是绑定监听的地方。
使用中的定时器的问题
puppeteer中使用定时器的时候,你需要考虑下你是否真的需要定时器去实现,而定时器完成的事情,往往可以使用递归去解决,定时器的关闭很容易造成问题,尤其是你需要频繁开启关闭定时器。
如何处理单页分页滚动
有些网站的分页如今日头条,首页向下滚动分页的实现。然后append节点到首页底部。所以不可避免的会出现 “滚到底” 的问题。常见就是定时器,然后判断这个分页展示区高度。但是肯定不够。而且你滚动停止的时候,你的滚动是不是由于定时器没有停止持续滚动。需要就是第一点更换定时器而使用递归的方式来进行持续滚动,更换分页完成的判断依据为判断节点数量是否增多。然后就是断点继续读取,这里就是得获取高度了
let getHeight = async (mpage) => {
return mpage.evaluate(() => {
height = window.document.body.scrollHeight;
console.log("页面高度", height);
return height;
});
};
当然仍然不是很好,这种有个弊端,你怎么去控制滚动的距离,在滚动距离过多的时候,会导致滚动进度条直接跳到最上面,有时候会卡主页面。所以可以使用监听分页请求来获取想要的数据。
9 关于如何多浏览器多页并行执行
let gutherWrapper = async () => {
const browseArr = await getBrowseData();
let promiseFunc = [];
if (browseArr.length == 0) {
console.log("无浏览器打开");
}
browseArr.forEach(async (browse) => {
promiseFunc.push(async () => {
return new Promise(async (resolve, reject) => {
try {
let mpages = await browse.pages();
for (var key in mpages) {
var mpage = mpages[key];
let u = await mpage.url();
// 你的主逻辑代码。。。
}
resolve("start guther");
} catch (error) {
reject("error gutherWrapper");
}
});
});
});
return promiseFunc;
};
let start = async () => {
let func = await gutherWrapper();
func.forEach((fun) => {
Promise.all([fun()]).then((res) => {
console.log("Execute completed:", res.pop());
}).catch(e=>{
clearInterval(timer)
});
});
};
start();
getBrowseData方法是返回我保存的浏览器节点所对应的browse对象。就是你写浏览器重连的代码返回的browse数组。基于promise的封装可以并行运行,你写好一个。可以直接用我这个代码把你的逻辑套一下就可以了。
10.关于iframe内嵌页面的元素
有时候,你明明页面是出现了我们所期待的元素,但是就是获取不到,但是如果你使用检查元素,在检查下那一块的页面内容
在获取的时候,就可以获取到元素,出现这种情况,需要看看是不是iframe。
获取方式,先获取iframe,在根据iframe继续获取
let iframe = document.querySelector('iframe[tabindex="-1"]').contentWindow.document;
let doms = iframe.querySelectorAll('i[data-visualcompletion="css-img"]')
这种获取建议使用page.evaluate, page. e v a l 里面在写一个 p a g e . eval里面在写一个page. eval里面在写一个page.eval会有问题
还有什么问题,想到了在更新!