基于puppeteer的网页截图应用之超长网页截图的思考与探索

前言

某日,由于需要对某些网页进行截图存储,于是使用nodejs+puppeteer的方案来实现,但在实验截图效果时发现一些超长的网页无法得到正确结果,由此在处理超长网页截图的调整上历经磨难。

快速实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const puppeteer = require('puppeteer')
(async () => {
const browser = await puppeteer.launch({
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-zygote',
'--no-sandbox'
],
timeout: 0,
pipe: true,
headless: true,
ignoreHTTPSErrors: true,
// executablePath: ChromiumPath
});
const page = await browser.newPage()
await page.goto('https://karoy.cn/')
await page.screenshot({path: 'karoy.png'})
await browser.close()
})()

尽量完成网页渲染

1
2
3
4
5
// 500ms内请求数为0
await page.goto('https://karoy.cn/', {waitUntil: 'networkidle0'})
// 等待1000ms
await page.waitFor(1000)

截取网页全图

1
2
3
4
5
await page.screenshot({
path: 'karoy.png',
type:'png',
fullPage:true
})

超长网页截取全图失败

这个时候在遇到超长网页的时候就开始失败了,一般表现为图片后部存在大片的空白。具体原因由于Chromium硬性实现,在网页高度达到16384px以上,将不再处理后续内容。

方案一:多次截图后拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//建议先通过自动滚屏方式,让网页完全加载后再取得相应数据

let {
pageHeight,
viewport
} = await page.evaluate(() => {
window.scrollTo(0, 0);
return {
pageHeight: document.body.scrollHeight,
viewport: {
height: document.body.clientHeight,
width: document.body.clientWidth
}
};
});

let viewHeight = viewport.height
let viewWidth = viewport.width
let maxViewHeight = 16000;
let partViewCount = Math.ceil(pageHeight / maxViewHeight);
let lastViewHeight = pageHeight - ((partViewCount - 1) * maxViewHeight);

let images = []
for (let i = 1; i <= partViewCount; i++) {
let currentViewHeight = i !== partViewCount ? maxViewHeight : lastViewHeight
let img = await page.screenshot({
fullPage: false,
// path:`f-${i}.png`,
clip: {
x: 0,
y: (partViewCount - 1) * maxViewHeight,
width: viewWidth,
height: currentViewHeight
}
})
images.push(img)
}

// 合并图片

但是该方式仍然存在问题(就我写时测试而言),在处理连续几个区域截图后,后面的区域截图开始出现大段地空白,原因未知。
在搜索相关资料后仍未解决,于是思考是否有其他办法。

方案二:改进的多次截图后拼接

虽然clip时,连续截取超过一定的高度的内容会出现问题,但是至少截取第一屏目前来看是没有问题的,那么有没一种方法把内容移到第一屏呢?
这就是下面的这个方式的解决来源,这里采用了改变bodymargin-top属性的操作来达成。
每次要截取新内容区的时候,就改变bodymargin-top属性从而达到改变截图区内容的目的。
就初步测试而言,采用该方式是能够取得正确区域截图的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let totalMarignTop = 0
let images = []
for (let i = 1; i <= partViewCount; i++) {
let currentViewHeight = i !== partViewCount ? maxViewHeight : lastViewHeight
let img = await page.screenshot({
fullPage: false,
// path:`f-${i}.png`,
clip: {
x: 0,
y: 0,
width: viewWidth,
height: currentViewHeight
}
})
images.push(img)

totalMarignTop += currentViewHeight
await page.evaluate((totalMarignTop) => {
return new Promise((resolve, reject) => {
$('body').css('margin-top', '-' + totalMarignTop + 'px')
var timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, 1000);
})
}, totalMarignTop)
}
// 合并图片

最终的超长网页全图生成就基于上面所展示的核心代码来完成处理的。

合并图片

1
2
3
4
5
6
7
8
9
if (partViewCount == 1) {
let img = await Jimp.read(images[0])
img.write(filename)
} else {
let img = await mergeImg(images, {
direction: true
})
img.write(filename)
}

完整方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

// utils.js 部分

const Jimp = require('jimp')
const mergeImg = require('merge-img')

var pageScreenshot = async function(page, filename) {
let {
pageHeight,
viewport
} = await page.evaluate(() => {
window.scrollTo(0, 0);
return {
pageHeight: document.body.scrollHeight,
viewport: {
height: document.body.clientHeight,
width: document.body.clientWidth
}
};
});
let viewHeight = viewport.height
let viewWidth = viewport.width

let maxViewHeight = viewHeight;
let partViewCount = Math.ceil(pageHeight / maxViewHeight);
let lastViewHeight = pageHeight - ((partViewCount - 1) * maxViewHeight);

let totalMarignTop = 0
let images = []
for (let i = 1; i <= partViewCount; i++) {
let currentViewHeight = i !== partViewCount ? maxViewHeight : lastViewHeight
let image = await page.screenshot({
fullPage: false,
clip: {
x: 0,
y: 0,
width: viewWidth,
height: currentViewHeight
}
})
images.push(image)
// 滚动距离
totalMarignTop += currentViewHeight
await page.evaluate((totalMarignTop) => {
return new Promise((resolve, reject) => {
$('body').css('margin-top', '-' + totalMarignTop + 'px')
var timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, 1000);
})
}, totalMarignTop)
}

return new Promise(async (resolve, reject) => {
if (partViewCount == 1) {
let img = await Jimp.read(images[0])
img.write(filename)
} else {
let img = await mergeImg(images, {
direction: true
})
img.write(filename)
}
resolve()
})
}

var autoScroll = function(page) {
return page.evaluate(() => {
return new Promise((resolve, reject) => {
var totalHeight = 0;
var distance = 100;
var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve(totalHeight);
}
}, 100);
})
});
}

module.exports = {
pageScreenshot,
autoScroll
}


// app.js 部分
const puppeteer = require('puppeteer')
const { pageScreenshot, autoScroll } = require('./utils')


//// 使用demo
(async () => {

//...
//...
let browser = await puppeteer.launch({
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-zygote',
'--no-sandbox'
],
timeout: 0,
pipe: true,
headless: true,
ignoreHTTPSErrors: true,
// executablePath: ChromiumPath,
defaultViewport: null
})
let page = await browser.newPage()
await page.setViewport({
width: 1000,
height: 1920,
deviceScaleFactor: 1
})
await page.goto('https://karoy.cn/', {
timeout: 30000,
waitUntil: ['networkidle0']
})
await autoScroll(page)
await page.evaluate(() => { window.scrollTo(0, 0) })
await page.waitFor(500)
await pageScreenshot(page, 'title.png').catch(err => console.log(err))

//...
//...

})()

结束

虽然最终的解决关键很简单,但在遇到问题的时候,尝试过很多方式,包括重新安装依赖,多次改变参数选项,搜索相关资料等。
最后另辟蹊径来解决了本次遇到的问题。