通过puppeteer抓取文档内容渲染

1,291 阅读4分钟

背景

需要在云市场应用中渲染文档站钟某篇文档的内容,之前由于文档站是 SSR 渲染,可以直接提供接口给云市场来调用获得 html 文本,然后将该文本渲染出来。

但是最近海外上线了新的文档站,新站点采用了 SSG 的方案,无法对外提供接口来获取内容。所以我们需要找到一种方式,能在云市场应用中展示文档站的文档。

实现过程

方案一

首先想到的方案就是iframe。在后台配置每种产品对应的文档链接,然后在文档区域直接通过iframe将文档内容渲染出来。但是很快就发现问题了:我们想渲染的仅仅是文档本身内容而已,并不需要什么导航条,菜单栏等等内容,空间利用率十分低下。然而iframe方案并不支持我们去自定义修改 dom,让海外文档站额外开发纯净模式也并不现实... image.png

方案二

iframe方案应该是破产了,那我们换一种思路:我们主动去同步海外文档。比如通过定时脚本用axios请求到 html,用cheerio在服务端解析出我们想要的文档内容 dom 以及css样式文件的地址,存在数据库中。云市场应用直接从数据库里取出 dom 附加上样式渲染即可。

说干就干,依照老传统,做好后会发现新的问题:实际渲染出来的样子与源站有差异,特别是代码模块。

我们渲染的:

image (1).png 源站:

image (2).png

那么为什么会产生这个现象呢?通过比对源站的加载过程我发现源站也会这样白屏一会儿,之后才渲染出正确的代码样式。我们知道,在服务端渲染中是可以指定一部分 dom 完全交给客户端处理的。也就是说,虽然它是 SSG,在其中也存在一部分 dom 是通过客户端渲染的,页面加载完成后会有 js 对 dom 进行操作,导致拿到的 dom 并非最终的实际 dom。啊这...我在服务端也执行不了 js 啊。后续并没有想到什么好的解决方案,放弃了方案二。

方案三

方案二其实已经很接近了,本质的冲突点在于通过axios只能得到首屏 dom,无法感知到客户端 dom 的变化,而要想要感知到 dom 的变化需要在服务端有一个环境去真正执行这些 js。说到这,思路比较清晰了,puppeteer 不正好满足我的需求么?所以说,我们在方案二的基础上,把axios替换成puppeter即可,而且puppeteer是浏览器环境,可以直接操作 dom,也不需要cheerio去解析 dom 了!于是,我们可以等页面渲染出来后,等待 15s 再抓取内容,保证 dom 是页面最终实际使用的。

await this.page.waitForSelector(selector)
await sleep(15000)

const domString = await this.page.evaluate((selector) => {
  const dom = document.querySelector(selector)
  if (!dom) return ''
  return dom.outerHTML
}, selector)

至此,dom 结构终于没有问题了。不过很快又发现了新的问题:图片几乎都没有加载出来。查看 dom 后发现,原来图片用的相对链接,自然没法正确加载,看来还需要处理相对链接的问题。相对链接除了图片应该还有链接,实验一下果然如此,点击链接直接转到我们云市场应用的域名下了。我们可以在获取 dom 的时候转换成绝对链接,而且将链接的跳转都改成新开窗口跳转了。

const domString = await this.page.evaluate((selector) => {
  const dom = document.querySelector(selector)
  if (!dom) return ''

  // 转化图片和链接的相对路径为绝对路径
  for (const item of dom.querySelectorAll('a')) {
    item.href = item.href
    item.target = '_blank'
  }
  for (const item of dom.querySelectorAll('img')) {
    item.src = item.src
  }

  return dom.outerHTML
}, selector)

本地试了很久,暂未发现新的问题。然而意料之中的是还是出现了意料之外的情况:上测试环境后脚本跑不起来。查阅资料后发现, puppeteer在服务端运行是需要运行环境的,也就是在docker打包的时候需要安装浏览器套件。不过想想也是,没有浏览器套件你怎么模拟浏览器行为呢?

# 配置安装 chromium 环境,以使用 puppeteer
ENV CHROME_BIN="/usr/bin/chromium-browser" \
    PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true"

RUN set -x \
    && apk update \
    && apk upgrade \
    && apk add --no-cache \
    udev \
    ttf-freefont \
    chromium

最终效果

云市场: image (3).png

源站: image (4).png