// 演示自动访问百度网站并抓取相关搜索关键词 const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch( {executablePath: './chromium/chrome', headless: false}); const page = await browser.newPage(); await page.goto('https://www.baidu.com/'); await page.focus('#kw'); // 焦点到搜索关键字输入框 await page.type('Chrome Headless', {delay: 100}); // 输入关键字 Chrome Headless await page.click('#su'); // 点击“百度一下”提交按钮 const waitForElement = page.waitForSelector('#rs > table > tbody > tr:nth-child(3) > th:nth-child(5) > a', {visible:true,timeout: 3000}); // 等待 3 秒或者页面显示完成 注释1 try {// 此处是可能产生例外的语句 await waitForElement; var kw = []; // 注释2 kw[0] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(1) > a', el => el.innerHTML); //注释3 kw[1] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(3) > a', el => el.innerHTML); kw[2] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(5) > a', el => el.innerHTML); kw[3] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(1) > a', el => el.innerHTML); kw[4] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(3) > a', el => el.innerHTML); kw[5] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(5) > a', el => el.innerHTML); kw[6] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(1) > a', el => el.innerHTML); kw[7] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(3) > a', el => el.innerHTML); kw[8] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(5) > a', el => el.innerHTML); console.log('相关搜索关键词:'); //遍历相关搜索关键词. for(var i=0;i<kw.length;i++){ console.log(kw[i]); } } catch (error) { // 此处是负责例外处理的语句 console.log('网页中没有找到相关搜索'); } finally { // 此处是出口语句 } browser.close(); // 关闭退出。可注释掉此行代码,便于观察最后的结果 })();
延时
await page.waitForTimeout(3000);
常用的元素函数选择
page.$(selector) // document.querySelector page.$$(selector) // document.querySelectorAll page.$x(expression) // xpath
模拟输入
page.mouse page.keyboard page.click(selector[, options]) //在被选择元素上模拟点击 page.type(selector, text[, options]) //在被选择的输入框中输入 page.hover(selector) //模拟鼠标移动到被选择元素上 page.select(selector, ...values) //在被选择元素上模拟选择select选项 page.tap(selector) // 在被选择元素上模拟触摸
等待
page.waitForNavigation(options) page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]]) page.waitForSelector(selector[, options]) page.waitForXPath(xpath[, options]) page.waitForFunction(pageFunction[, options[, ...args]])
其中最常用的是page.waitForNavigation,常用于等待跳转结束,例如点击搜索按钮后,等待跳转至搜索结果页面。
const navigationPromise = page.waitForNavigation(); await page.click('a.my-link'); await navigationPromise;
配置等待时间
// Configure the navigation timeout await page.goto('https://ourcodeworld.com', { waitUntil: 'load', // Remove the timeout timeout: 0 });
信息查看
puppeteer提供了一些查看页面信息的函数,
- page.url()
- page.content()
- page.frames()
- page.mainFrame()
- page.metrics()
- page.target()
- page.title()
- page.viewport()
请求中断
page.setRequestInterception提供了中断请求的机制,例如,我们可以通过它实现一个无图模式。
await page.setRequestInterception(true); page.on('request', interceptedRequest => { if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg')) interceptedRequest.abort(); else interceptedRequest.continue(); }); await page.goto('https://example.com');
这里有一个interceptedRequest对象,它提供了三种响应模式:abort、continue和respond。
内容保存
内容保存主要包括保存为pdf和截图
page.pdf(options) page.screenshot([options])
事件通知
page.on('load', async () => { console.log('page loading done, start fetch...'); });
close | frameattached | pageerror |
---|---|---|
console | framedetached | request |
dialog | framenavigated | requestfailed |
domcontentloaded | load | requestfinished |
error | metrics | response |
执行脚本
执行脚本最常用的函数是page.evaluate,它类似于在控制台中执行指令。
console.log(await page.evaluate('1 + 2')); var title = await page.evaluate('document.title')
它也可以用来执行写好的node函数,实际上该函数是在浏览器中执行的,但可以像本地函数一样编写,还支持参数传值。
var title = await **page**.evaluate(async (i) => { return document.title + ' ' + i; }, 'hello'); console.log(title);
虽然这node函数不能调试,但仍然是有非常大的好处的,
- 不用考虑字符串转义的问题,书写起来非常直接
- 脚本在IDE中有高亮显示和智能提示的,写起来更加方便
另外,还有几个其它的执行脚本的函数,应用于不同的场合,也是非常有用的。
- page.evaluateHandle(pageFunction, ...args)
- page.evaluateOnNewDocument(pageFunction, ...args)
- page.$$eval(selector, pageFunction[, ...args])
- page.$eval(selector, pageFunction[, ...args])
例如:
const searchValue = await page.$eval('#search', el => el.value); const preloadHref = await page.$eval('link[rel=preload]', el => el.href); const html = await page.$eval('.main-container', e => e.outerHTML);
// 引入一些需要用到的库以及一些声明 import * as puppeteer from 'puppeteer' // 引入Puppeteer import mongo from '../lib/mongoDb' // 需要用到的 mongodb库,用来存取爬取的数据 import chalk from 'chalk' // 一个美化 console 输出的库 const log = console.log // 缩写 console.log const TOTAL_PAGE = 50 // 定义需要爬取的网页数量,对应页面下部的跳转链接 // 定义要爬去的数据结构 interface IWriteData { link: string // 爬取到的商品详情链接 picture: string // 爬取到的图片链接 price: number // 价格,number类型,需要从爬取下来的数据进行转型 title: string // 爬取到的商品标题 } // 格式化的进度输出 用来显示当前爬取的进度 function formatProgress (current: number): string { let percent = (current / TOTAL_PAGE) * 100 let done = ~~(current / TOTAL_PAGE * 40) let left = 40 - done let str = `当前进度:[${''.padStart(done, '=')}${''.padStart(left, '-')}] ${percent}%` return str }
// 进入代码的主逻辑 async function main() { // 首先通过Puppeteer启动一个浏览器环境 const browser = await puppeteer.launch() log(chalk.green('服务正常启动')) // 使用 try catch 捕获异步中的错误进行统一的错误处理 try { // 打开一个新的页面 const page = await browser.newPage() // 监听页面内部的console消息 page.on('console', msg => { if (typeof msg === 'object') { console.dir(msg) } else { log(chalk.blue(msg)) } }) // 打开我们刚刚看见的淘宝页面 await page.goto('https://s.taobao.com/search?q=gtx1080&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20180416&ie=utf8') log(chalk.yellow('页面初次加载完毕')) // 使用一个 for await 循环,不能一个时间打开多个网络请求,这样容易因为内存过大而挂掉 for (let i = 1; i <= TOTAL_PAGE; i++) { // 找到分页的输入框以及跳转按钮 const pageInput = await page.$(`.J_Input[type='number']`) const submit = await page.$('.J_Submit') // 模拟输入要跳转的页数 await pageInput.type('' + i) // 模拟点击跳转 await submit.click() // 等待页面加载完毕,这里设置的是固定的时间间隔,之前使用过page.waitForNavigation(),但是因为等待的时间过久导致报错(Puppeteer默认的请求超时是30s,可以修改),因为这个页面总有一些不需要的资源要加载,而我的网络最近日了狗,会导致超时,因此我设定等待2.5s就够了 await page.waitFor(2500) // 清除当前的控制台信息 console.clear() // 打印当前的爬取进度 log(chalk.yellow(formatProgress(i))) log(chalk.yellow('页面数据加载完毕')) // 处理数据,这个函数的实现在下面 await handleData() // 一个页面爬取完毕以后稍微歇歇,不然太快淘宝会把你当成机器人弹出验证码(虽然我们本来就是机器人) await page.waitFor(2500) } // 所有的数据爬取完毕后关闭浏览器 await browser.close() log(chalk.green('服务正常结束')) // 这是一个在内部声明的函数,之所以在内部声明而不是外部,是因为在内部可以获取相关的上下文信息,如果在外部声明我还要传入 page 这个对象 async function handleData() { // 现在我们进入浏览器内部搞些事情,通过page.evaluate方法,该方法的参数是一个函数,这个函数将会在页面内部运行,这个函数的返回的数据将会以Promise的形式返回到外部 const list = await page.evaluate(() => { // 先声明一个用于存储爬取数据的数组 const writeDataList: IWriteData[] = [] // 获取到所有的商品元素 let itemList = document.querySelectorAll('.item.J_MouserOnverReq') // 遍历每一个元素,整理需要爬取的数据 for (let item of itemList) { // 首先声明一个爬取的数据结构 let writeData: IWriteData = { picture: undefined, link: undefined, title: undefined, price: undefined } // 找到商品图片的地址 let img = item.querySelector('img') writeData.picture = img.src // 找到商品的链接 let link: HTMLAnchorElement = item.querySelector('.pic-link.J_ClickStat.J_ItemPicA') writeData.link = link.href // 找到商品的价格,默认是string类型 通过~~转换为整数number类型 let price = item.querySelector('strong') writeData.price = ~~price.innerText // 找到商品的标题,淘宝的商品标题有高亮效果,里面有很多的span标签,不过一样可以通过innerText获取文本信息 let title: HTMLAnchorElement = item.querySelector('.title>a') writeData.title = title.innerText // 将这个标签页的数据push进刚才声明的结果数组 writeDataList.push(writeData) } // 当前页面所有的返回给外部环境 return writeDataList }) // 得到数据以后写入到mongodb const result = await mongo.insertMany('GTX1080', list) log(chalk.yellow('写入数据库完毕')) } } catch (error) { // 出现任何错误,打印错误消息并且关闭浏览器 console.log(error) log(chalk.red('服务意外终止')) await browser.close() } finally { // 最后要退出进程 process.exit(0) } }
源码 https://github.com/MrTreasure/Algorithm/tree/master/src/Puppeteer
请求头设置
const browser = await puppeteer.launch({ executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome', headless: true, args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36');
设置Cookies
page.setCookie(options)
async function addCookies(cookies_str: string, page: puppeteer.Page, domain: string){ let cookies = cookies_str.split(';').map(pair=>{ let name = pair.trim().slice(0,pair.trim().indexOf('=')) let value = pair.trim().slice(pair.trim().indexOf('=')+1) return {name,value,domain} }); await Promise.all(cookies.map(pair=>{ return page.setCookie(pair) })) }
puppeteer 知乎
for(let answer of answerlist){ //这个answerlist就是page.$$('选择器')获得的元素列表 let data = await page.evaluate((answer)=>{ //这个一个html转码方法 如: //<script>alert(2);</script> => <script>alert(2);</script> function HTMLDecode(text:string) { let temp = document.createElement("div"); temp.innerHTML = text; let output = temp.innerText || temp.textContent; return output; } //以下就是通过选择器,获得各种元素,然后得到我们想要的数据 //如作者,回答内容,标题,多少个赞 等等 let comment = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-actions > div > a.toggle-comment'); //这里我们点击评论按钮,response事件发生,我们的函数执行,收集数据 comment.click(); let title = answer.querySelector('.zm-item-title > a').innerHTML; let author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span.name'); let author_link = ''; if(!author){ author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').innerHTML; author_link = 'https://www.zhihu.com'+answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').getAttribute('href'); }else{ author = author.innerHTML; } let content = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-rich-text > textarea.content').innerHTML; content = HTMLDecode(content); let like = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-vote > a').innerHTML; return { title, author, content, like ,author_link} },answer) await timeout(2000); if(Obj.data.length == 1){ await timeout(2000); } console.log(Obj.data) await timeout(1000) data.content = handler_content(data.content) data.comments = Obj.data; //这里我们就完成了一个回答的收集了 datas.push(data); Obj.data = []; console.log(data.title) } //保存到数据库 for(let data of datas){ let comments = data.comments; delete data.comments; let res = await db.insert( {table: 'zhihu', data} ); if(res ){ console.log(res) for(let c of comments){ c.zhihu = res.insertId; await db.insert({table: 'zhihu_comment', data: c}) } } }