在平时使用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/ChromeDevTools/devtools-frontend/blob/f5d825ac1bb6eca13782947e30e5c5c78b9de1f0/front_end/emulation/DeviceModeModel.js#L728 其实就是可能是限制了截图高度。如果过高就可能出现类似问题。
当然,咱们搞代码的,也要注意严谨,为了证明这个观点,我用了一些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分辨率/截图大小入手去考虑,几乎大部分问题都出在这些方面。
如有遇到其他问题,评论下方可以联系我,共同学习排坑。