Puppeteer 系列踩坑日志—1—批量截图变白

在平时使用puppeteer最多的就是截图了(每日批量自动截图),我的目标是X宝的一些店铺首页,这次遇到的问题就是在一些无线端,商家的页面非常长,甚至多的达到了6-10万像素。经常出现白屏或者残缺现象。
第一坑:加载问题?
  • 遇到类似问题。首先我想到的就是页面的资源加载问题,是不是puppeteer截图的时候没有加载出来呢?因此我根据页面的滚动查看了下,的确有类似的情况。
  • 首先无线端的页面如果在无状态的登录下(没有设置userdata,cookie等),会弹出登录,简单看了下dom,其实就是一个悬浮层,清理掉就好了。
  • 第二步,截图的时间久了,发现有时候会出现页面正在loading,那么这种类型的,我让promise直接reject掉(还能怎么办,放在错误队列里面,晚点再看试咯!)
  • 第三步,当然作为一位强迫症患者,本人及其不习惯乱七八糟的字体,因此统一了一下,全部设置为微软雅黑,于是有了style的样式。
  • 第四步,在做滚动的时候,发现pc端其实很简单,body.scrollBy直接可以滚动,但是到了无线端,坑来了!这XXX的,怎么动都动不了,打开调试看了下,X宝的无线端DOM全都是DIVDIVDIVDIVDIV(写个选择器都很麻烦,等会后面会讲到怎么快速去找到自己想要的元素,道高一尺魔高一丈)。好在后来看到了曙光,其实关键的滚动就在于.rax-scrollview这个div身上,因此解决了该滚动问题。
  • 好了,说了那么多,不如来一份代码全面些:

    async function scrollToBottom(page) {
        return await page.evaluate(() => {
            return new Promise((res, rej) => {
                if (document.querySelector('body > div > span')) {
                    if (document.querySelector('body > div > span').innerText === '加载中...') {
                        rej('页面截图太快,天猫loading不出来')
                    }
                }
                var style = document.createElement('style');
                style.innerHTML = '*{font-family: "微软雅黑" !important;}';
                document.body.appendChild(style);
                var totalH = 0;
                var distance = 250;
                var clientWidth = document.body.clientWidth;
                var selectorEle = '.rax-scrollview'; //淘宝专属滑动dom
                var scrollEle = document.querySelector(selectorEle) != null ? document.querySelector(selectorEle) : document.body;
                var scrollEleS = document.querySelector(selectorEle) != null ? document.querySelector(selectorEle) : window;
                var timer = setInterval(() => {
                    var scrollHeight = scrollEle.scrollHeight;
                    scrollEleS.scrollBy(0, distance);
                    totalH += distance;
                    if (document.querySelector('body > div.J_MIDDLEWARE_FRAME_WIDGET > div > a')) {
                        document.querySelector('body > div.J_MIDDLEWARE_FRAME_WIDGET > div > a').click();
                    }
                    if (totalH >= scrollHeight) {
                        clearInterval(timer);
                        scrollEleS.scrollTo(0, 0); //返回到顶部,然后再截图。防止从底部开始截图,部分活动页面不回去就会出现问题
                        res({
                            w: clientWidth,
                            h: scrollHeight
                        })
                    }
                }, 250)
    
            })
        })
    }
第二坑:截图问题?
  • 在服务器跑了一遍,发现又出现了空白,这次的空白明显比上次还多...
  • 考虑了方方面面的问题后(不喜欢绕太多弯子),最后我把问题锁定在了page.screenshot这个环节上,因为headless关掉,我直接看着页面加载完完毕的!居然还出现了空白,不是你这个api,还能是谁?
  • 在一顿操作(搜索引擎+github+官网),发现了一个有趣的代码块:https://github.com/ChromeDevT... 其实就是可能是限制了截图高度。如果过高就可能出现类似问题。
  • 当然,咱们搞代码的,也要注意严谨,为了证明这个观点,我用了一些1000-2000像素高度的网站来测试。100个左右的网站,截图都是非常完美的。这也差不多证明了是这里的问题。
  • 既然问题找到了,那么就来解决问题吧。首先祭出代码,我们根据代码来“事后诸葛亮”:

        let ImageHeight = 5000; //pc限制一次截图高度
        if (!!!pageInfo.type) {
            ImageHeight = 1000; //mob限制一次截图高度
        }
        let tempLength = Math.ceil(data.h / ImageHeight)//计算下得截图多少次
        let imagesArr = []//储存一下我们截图的序列
        for (let i = 0, j = 1; i < tempLength; i++) {//for直接搞起(async await大法好!!!!)
            await page.screenshot({
                path: 你的路径,
                clip: {
                    x: 0,
                    y: i * ImageHeight,
                    width: _w,
                    height: j * ImageHeight
                }
            }).then(res => {
                console.log('截图了', i, '张');
                imagesArr.push(`你的路径`)
            }).catch(err => {
                console.log('截图失败!');
            })
        }
    • 逻辑就是 一片一片的截图,这样的话,问题基本上就解决了!
    • 代码非常简单,简单到我都觉得不需要解释,但是还是唠叨两句,我先判断下pc还是无线(pc没必要一点点截图,因为pc大概率是没啥问题的,除非特别特别长,可以根据实际情况修改,我觉得5000差不多了)。
    • 另外需要提醒一点:我这里5000和1000的阈值,需要根据你的计算机/服务器情况决定,如果配置不行的话,可能还需要降低,之前遇到过在一台很老的笔记本上测试,发现1000都会=-=白屏...(都0202年了,你问我,为什么要拿一台很老的笔记本跑这个?因为写文章要严谨啊!得多测试啊!)
    • 我们再进行一次储存到数组里(方便等会进行合并)。因为涉及到多任务(总不能真的就开一个进程吧),如果不进行储存,那么之后合并完删除这些小碎片,还要区分不同进程的碎片截图,那就怕有点脑壳疼。所以每个进程自己留个“小本本”~ 咱们等会就知道该删哪些碎片截图了。
  • 这里需要注意的是,合并图片,使用的是GM这个图像库(解决这个问题才花了我1小时不到,本来想图省事找个轻量的,结果发现都没GM简单粗暴,唉,花了我整整一下午。最后换回GM库,5分钟搞定。。)
  • 代码非常简单,但是安装GM,需要提前安装它的软件,百度一下哪里都有。windows就下一步一下步,linux也基本上差不多,wget一下需要的安装包,然后tar解压下XXXXX一顿常规操作,差不多就能搞定,这里不多讲。
  • 示范一下GM部分代码:
let deleteFile = [...imagesArr];
await gmMerge(foldname,pathName,imagesArr)


function gmMerge(foldname,pathName,imagesArr) {
    return new Promise((res, rej) => { gm(imagesArr.shift()).append(...imagesArr).write(`./screenshot/${foldname}/${pathName}.png`, function (err) {
            if (err) {
                console.log('截图合并失败!')
                rej('截图合并失败!')
            } else {
                console.log('完整截图生成成功!');
                res('完整截图生成成功!');

            }
        });

    })
}

简单解释下,要删除的复制个小本本出来,因为我们需要将第一张截图作为初始画布,然后后续的全部加进来,我这里偷懒使用了shift方法,这样的话等会再去删除 就会漏掉,因此我这样做了。你有更简单的办法也可以,这里没什么要讲。主要是我放入了promise里面,这样的话 await gm合并函数即可。

  • 这里需要提醒下,一定要封promise,否则直接在async里面执行,很有可能任务队列结束了,这些碎片截图没删掉,还留在里面,因为没有等待操作执行完就结束了。
其他坑:
  • 除了对屏幕截图puppeteer也提供了元素截图(部分dom),当页面真的非常复杂且冗余的时候,可以采用这个做法。具体可以参考以下代码:
let footer = await page.$('#footer');    //获取到DOM对象,身上都有screenshot方法
footer.screenshot({path:'demo.png'});
  • 其实关于puppeteer的截图空白问题,其实是由来已久的,偶尔你也会发现 pc明明正常的截图也会出现类似问题,如果真的出现了意外情况(screenshot抽了),建议还是从viewport分辨率/截图大小入手去考虑,几乎大部分问题都出在这些方面。

如有遇到其他问题,评论下方可以联系我,共同学习排坑。

你可能感兴趣的:(前端,node.js,chrome,网页爬虫,puppeteer)