puppeteer经验之谈从入门到进阶

puppeteer有一段时间,有一些心得,特在此记录一下!
puppeteer的使用,我们首先是可以看这个网站。

https://zhaoqize.github.io/puppeteer-api-zh_CN/#

// 主要流程:
// 1) 先通过 puppeteer.launch() 创建一个浏览器实例 Browser 对象
// 2) 然后通过 Browser 对象创建页面 Page 对象
// 3) 然后 page.goto() 跳转到指定的页面
// 4) 然后 page.$("input[name^='query']") 通过CSS选择器。选中页面元素进行后续操作
// 5) 操作选中的元素:常见操纵如下(仅供参考)

// 6) 然后 page.goto() 跳转到指定的页面
// 7) 你需要进行的后续操作逻辑
// 8) 关闭浏览器

// 常用参数:
// ignoreHTTPSErrors: true, //运行途中是否忽略报错 为true则不忽略
// headless: false, //是否以”无头”的模式运行 chrome, 也就是不显示浏览器 UI 界面 为true则不显示
// slowMo: 250, //使 Puppeteer 操作减速,单位是毫秒。如果你想看看 Puppeteer 的整个工作过程,这个参数将非常有用。
// timeout: 0 //等待 Chrome 实例启动的最长时间。默认为30000(30秒)。如果传入 0 的话则不限制时间
//了解更多,参见 官方文档或者 https://www.jb51.net/article/139808.htm 

代码部分

  1. 浏览器的创建
// 后面都可以基于这些代码套一个壳
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',
     slowMo: 250, 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 puppeteer = require('puppeteer');
puppeteer.launch({ ignoreHTTPSErrors:
true, headless: false, slowMo: 250, timeout: 0, defaultViewport: { width: 1920, height: 1080 } }).then(async browser => { let page = await browser.newPage(); await page.setJavaScriptEnabled(true); await page.goto("https://www.bilibili.com/v/music/original/?spm_id_from=333.5.b_6d757369635f6f726967696e616c.59#/all/click/0/1/?open=hot"); page.close(); browser.close(); }).catch(err => { });
  1. 浏览器节点的存储和获取
    使用过程中很多并不是运行一个,而是调用多个已经打开的浏览器进行操作,这就要做好节点信息的存储
// 存贮节点
const browserWSEndpoint = browser.wsEndpoint();
// 使用节点来重新建立连接
const browser2 = await puppeteer.connect({ browserWSEndpoint });
  1. 等待节点的出现
    await page.waitForSelector( " CSS选择器 " ); //等待元素加载之后,否则获取不异步加载的元素
// CSS选择器
await page.waitForSelector('#videolist_box > div.vd-list-cnt > ul > li > div > div.r > a');

  4.page 的$$ 和$
   其实就是对应于document.querySelectorAll
   和document.querySelector

let btn = await page.$('div[aria-label="Create a post"]')
let btn = await page.$$('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} })
)
//当你需要将inputData变量作为可选参数传递给$$ eval函数,以使其在节点上下文中可访问时应该这样
list = await page.$$eval(inputData.LIST, (lists, inputData) => lists.map(list => {
    return { title:list.innerText, href:list.href}                             
}), inputData);

5.等待操作 (暂停、等待)

 

await page.waitForTimeout(400); //类似与即将被废弃的函数 page.waitFor(400) ms

 

6.关于多层查找后再点击

 

let btn = await page.$('div[aria-label="Create a post"]')
    await page.waitForTimeout(400);
    let btn2 = await btn.asElement().$$('div[data-visualcompletion="ignore"]')
    await page.waitForTimeout(400);
    await btn2[1].asElement().click()

 

我们查找的时候 page. 或者 page.$ 是返回jshandle对象,而点击事件是 elementHandle对象 才可以执行,所以需要 asElement 转换对象类型。
7.关于如何上传文件(图片/视频)

 

const uploadFile = await page.$$('input[type="file"]');
if (filePath) {
    await uploadFile[uploadFile.length - 1].uploadFile(filePath);
}

 

filePath是你的要上传的文件目录,这个是可以提前知道的。

8.键盘操作

8.1 打一段字

 

let txt ='hello'
await page.keyboard.type('#key', 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: 1920, height: 1080 }
    };

    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) //已废弃 可由 page.waitForTimeout(1000) 替代
    await page.focus('input.nav-search-keyword') //focus 聚焦光标
    // 直接粘贴
    await page.keyboard.down('Control');
    await page.keyboard.press('KeyV', { delay: 100 });
    await page.keyboard.up('Control');
})();

 

这里的话down是代表键盘按下,up才会释放,press相当于down + up ,delay是按下和释放的间隔时间,具体可以看代码,可以直接运行。

 

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',
        slowMo: 250,
        defaultViewport: { width: 1920, height: 1080 }
    };

    const browser = await puppeteer.launch(options);
    const browserWSEndpoint = browser.wsEndpoint();
    console.log(browserWSEndpoint, 'site')
    const page = await browser.newPage();
    // page.on('dialog', async dialog => {
    //     await dialog.dismiss();
    // });
    await page.goto('https://www.bilibili.com/');

    await page.waitForSelector('#nav-searchform > .nav-search-content > .nav-search-input')
    await page.focus('input.nav-search-input')

    await page.evaluate(async () => {
        // try {
        let data = '英雄联盟'
        //创建一个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.waitForTimeout(1000) //已废弃 可由 page.waitForTimeout(1000) 替代
    await page.focus('input.nav-search-input') //focus 聚焦光标
    // 直接粘贴
    await page.keyboard.down('Control');
    await page.keyboard.press('KeyV', { delay: 100 });
    await page.keyboard.up('Control');
    let select=await page.$('.nav-search-btn');
    await select.focus();
    await page.click('.nav-search-btn');
    await page.waitForTimeout(3000)
    page.close();
    await page.waitForTimeout(2000)
    browser.close();
})();

 

9.如何进行分页监听

 

 

await mpage.setRequestInterception(true);
page.on("request", async (request) => {
    request.continue();
});
page.on("response", async (response) => {
    const req = response.request();
    if (req.url().indexOf("分页请求的地址") > 0) {
    //操作
    }
})

 

10.获取节点必备
当网络差时,会出现页面加载很久。那么你使用waitfor是没有用处的。所以用事件监听触发在取节点。

page.on('load',async ()=>{
  console.log('页面加载完毕')
   let re =  doCloseNotification(mpage, { from: id }); // 我的操作
 })

问题分析

一、获取节点

1.不使用文字作为该节点的判断依据
尽量不要使用文字作为节点的获取依据,有些网站允许个人设置自己首页的语言,这会导致基于文字的节点获取方式失效。

2.不基于类名,或者xpath
在我爬取facebook网站的时候,人家网站的类名是动态改变的,更麻烦的是dom节点的层数都是会改变,这导致你直接失效,这两种方式根本行不通。可行的替代方案是基于自定义属性查找,不过这只是通法。我不是反对这两种,只要代码易读,简单就可以使用。

3.节点等待
节点获取到的前提是加载完成,有时候必须进行等待,当出问题的时候,往往是由于节点没有出现,而使用了waitForSelector()导致的

4.foucs获取不到的问题
有些网站的某些输入框采用自定义的框架来显示。是使用div模拟的文本输入框,并不是常见的input输入框。然后这个问题可能发生在你使用page.$evalute ,基于document的focus.使用官方的.focus()方法可以很好的进行获取焦点。

5.模态框的节点获取失败
模态框的节点,通常是点击后动态加载来的,我们需要进行必要的等待,如果还没有,就需要逐层递进,先取到整个模态框的dom节点,在基于他在往下找。

6.点击节点没有效果
比较复杂的网站,往往dom节点层数很深,我们点不到,是由于获取的那一个节点不是人家代码绑定点击事件的那一层,而事件冒泡的问题他们程序员都处理的很好,基本不会存在。所以没什么其他方法,一层一层的dom去click()一下,肯定有一个是绑定监听的地方。

7.使用中的定时器的问题
puppeteer中使用定时器的时候,你需要考虑下你是否真的需要定时器去实现,而定时器完成的事情,往往可以使用递归去解决,定时器的关闭很容易造成问题,尤其是你需要频繁开启关闭定时器。

8.如何处理单页分页滚动
有些网站的分页如今日头条,首页向下滚动分页的实现。然后append节点到首页底部。所以不可避免的会出现 “滚到底” 的问题。常见就是定时器,然后判断这个分页展示区高度。但是肯定不够。而且你滚动停止的时候,你的滚动是不是由于定时器没有停止持续滚动。需要就是第一点更换定时器而使用递归的方式来进行持续滚动,更换分页完成的判断依据为判断节点数量是否增多。然后就是断点继续读取,这里就是得获取高度了

let getHeight = async (mpage) => {
  return page.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 pages = await browse.pages();
                    for (var key in pages) {
                        var page = pages[key];
                        let u = await page.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"]')

 

取材自 -》练气期小修士

 

posted @ 2022-05-27 17:11  芒果鱼  阅读(724)  评论(0编辑  收藏  举报