golang使用chromedp生成滚动更新页面的PDF

那年烟雨落申城
• 阅读 253

缘起

目前有一个Python项目,作用是打开一个报表页面,截图后生成PDF,作为邮件附件发送到指定邮箱。当前这个Python项目性能较差,目前项目组没有人会Python,于是决定使用golang重写一下

模拟滚动更新

这个页面是滚动更新的,滚动到某个地方才会加载下面的内容,光在这个滚动更新上就踩了很多坑。

  1. 踩坑window.scrollTo(x,y) 这个函数在静态页面是可以的,x代表横向偏移,y代表纵向偏移,x我设定的0,y设定的2400(通过页面某个元素可以获取到我当页面的最大值是2400),按F12打开控制台,将这段代码放进去,根本不起作用
  2. 踩坑 document.body.scrollTo(x,y) 使用方式同上,同样不起作用
  3. 踩坑 document.documentElement.scrollTo(x,y) 同样不起作用
  4. []byte转int总是0 就是下面这个代码
     bytesBuffer := bytes.NewBuffer(b)
     var x int32
     binary.Read(bytesBuffer, binary.BigEndian, &x)
  5. 真理 document.getElementById('xxxx-head').scrollTop=2400 'xxxx-head是我这个页面的最上面的可显示的<div>标签的id,这个可以将滚动条拉到最下面,我代码里面根据可视高度一点点的逼近最大值,其实就是每次滚动一屏,直到滚动到底为止,请看代码

    核心代码

    /**
    fileNameUrlMap: key为文件名 value为url
    topDivId: 页面上最上面显示第一行字的div 用来定位页面的最高处 这里传入这个div的Id
    waitVisibleExpr: 页面加载到这个选择器说明页面加载完成了
    maxHighId: 获取页面最高的大小的div的id
    chromeCtx: 谷歌浏览器实例
    goTraceId: 日志中的traceId
    

返回值: map的key为文件名,value是byte数组,返回生成的pdf的文件字节

注意:我这个是动态页面 是滚动加载的页面,需要模拟人手动去滚动滑块,然后等待加载数据,一直到底部为止 我当前的页面可以通过某个div获取到高度,如果你无法获取到高度,就往下滚动,监测本次滚动所能到达的高度和 上一次的是否一致,是的话就说明滚动到底了 */ func ScreenPdf(fileNameUrlMap map[string]string, topDivId string, waitVisibleExpr string, maxHighId string, chromeCtx context.Context, goTraceId string) map[string][]byte { var ( err error resultMap = make(map[string][]byte) ) config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始进行截图... param=%s ", fileNameUrlMap)

config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("初始化chrome完成...")

for fileName, url := range fileNameUrlMap {
    config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始截图,当前处理文件名=%s url=%s", fileName, url)
    chromeTabCtx, cancelFunc := chromedp.NewContext(chromeCtx, chromedp.WithLogf(config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof))
    //空任务触发初始化
    err = chromedp.Run(chromeTabCtx, make([]chromedp.Action, 0, 1)...)
    chromedp.Sleep(time.Second * 2)
    if err != nil {
        config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("初始化chrome并执行第一个Task失败跳过此截图 fileName=%s", fileName)
        continue
    }
    buf := make([]byte, 0)
    err = chromedp.Run(chromeTabCtx, chromedp.Tasks{
        chromedp.Navigate(url),
        chromedp.Sleep(time.Second * 10),
        chromedp.ActionFunc(func(ctx context.Context) error {
            config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始等待页面加载 检测点=%s fileName=%s", waitVisibleExpr, fileName)
            return nil
        }),
        chromedp.WaitVisible(waitVisibleExpr, chromedp.ByID),
        chromedp.ActionFunc(func(ctx context.Context) error {
            config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("页面加载完成 检测点=%s fileName=%s", waitVisibleExpr, fileName)
            var html string
            chromedp.InnerHTML(waitVisibleExpr, &html, chromedp.ByID)
            config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("获取到的页面html=%s", html)
            return nil
        }),
        chromedp.Sleep(time.Second * 15),
        chromedp.ActionFunc(func(ctx context.Context) error {
            //获取可视界面的高度
            var jsGetClientHigh = "document.body.clientHeight"
            clientHigh := getHighByJs(jsGetClientHigh, ctx)
            config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("可视高度为%d ", clientHigh)
            //获取最高的
            var jsGetMaxHigh = "document.getElementById('" + maxHighId + "').offsetHeight"
            maxHigh := getHighByJs(jsGetMaxHigh, ctx)
            config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("最大高度为%d ", maxHigh)
            var currentHigh = clientHigh
            //滚动
            for {
                if currentHigh < maxHigh {
                    jsScroll := "document.getElementById('" + topDivId + "').scrollTop=" + strconv.Itoa(currentHigh)
                    chromedp.EvalAsValue(&runtime.EvaluateParams{
                        Expression:    jsScroll,
                        ReturnByValue: false,
                    }).Do(ctx)
                    time.Sleep(time.Second * 15)
                    currentHigh += clientHigh
                } else {
                    config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("跳出高度%d fileName=%s", currentHigh, fileName)
                    break
                }
            }
            //滚动完成后滚回第一屏
            jsScroll0 := "document.getElementById('" + topDivId + "').scrollTop=0"
            chromedp.EvalAsValue(&runtime.EvaluateParams{
                Expression:    jsScroll0,
                ReturnByValue: false,
            }).Do(ctx)
            time.Sleep(time.Second * 1)
            //纸张设置为A0
            buf, _, err = page.PrintToPDF().WithPaperWidth(33.1).WithPaperHeight(46.8).WithPrintBackground(true).Do(ctx)
            return err
        }),
    })

    if err != nil {
        config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Errorf("截图出现报错 跳过当前PDF fileName=%s err=%v ", fileName, err)
        continue
    }
    config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).
        Infof("截图生成bytes完成 当前fileName=%s byteLength=%d", fileName, len(buf))
    resultMap[fileName] = buf
    cancelFunc()
}

return resultMap

}

func getHighByJs(jsGetHigh string, ctx context.Context) int { result, _, _ := chromedp.EvalAsValue(&runtime.EvaluateParams{ Expression: jsGetHigh, ReturnByValue: true, }).Do(ctx) json, _ := result.Value.MarshalJSON() clientHigh := bytesToInt(json) return clientHigh }

func bytesToInt(bys []byte) int { length := float64(len(bys)) - 1 var x float64 for _, value := range bys { tmp := math.Pow(10, length) x = x + (float64(value)-48)*tmp length-- } return int(x)

}

 上面用到了chrome的实例,实例初始化如下:
 ```go
 import (
    "context"
    "github.com/chromedp/chromedp"
    "os"
)

var ChromeCtx context.Context

/**
chrome初始化 全局使用这一个实例即可
*/
func init() {

    var headlessFlag chromedp.ExecAllocatorOption
    //headless这个默认是true,如果想要在本地调试的时候看下浏览器的行为,可以在
    //环境变量里添加headless=false 就可以在本地调试并观察浏览器被控制的行为了
    isHeadless := os.Getenv("headless")
    if isHeadless == "false" {
        headlessFlag = chromedp.Flag("headless", false)
    } else {
        headlessFlag = chromedp.Flag("headless", true)
    }
    opts := append(
        chromedp.DefaultExecAllocatorOptions[:],
        //不检查默认浏览器
        chromedp.NoDefaultBrowserCheck,
        //无头
        headlessFlag,
        //忽略错误
        chromedp.IgnoreCertErrors,
        //不加载gif图像 因为有可能会卡住
        chromedp.Flag("blink-settings", "imagesEnabled=true"),
        //关闭GPU渲染
        chromedp.DisableGPU,
        //不适用谷歌的sanbox模式运行
        chromedp.NoSandbox,
        //设置网站不是首次运行
        chromedp.NoFirstRun,
        //禁用网络安全标志
        chromedp.Flag("disable-web-security", true),
        //关闭插件支持
        chromedp.Flag("disable-extensions", true),
        //关闭默认浏览器检查
        chromedp.Flag("disable-default-apps", true),
        //初始大小
        chromedp.WindowSize(1920, 1080),
        //在呈现所有数据之前防止创建Pdf
        chromedp.Flag("run-all-compositor-stages-before-draw", true),
        //设置userAgent 不然chrome会标识自己是个chrome爬虫 会被反爬虫网页拒绝
        chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`), //设置UserAgent
    )

    ChromeCtx, _ = chromedp.NewExecAllocator(context.Background(), opts...)
}

完整项目请参考https://github.com/BLF2/go-screenshot

点赞
收藏
评论区
推荐文章
编程范儿 编程范儿
2年前
Vue刷新页面有哪几种方式
在Vue项目中,刷新当前页除了window.reload(),你还能想到什么办法?而且这种办法会重新加载资源出现短暂的空白页面。体验不是很好。在某个详情页面的时候,我们经常需要通过路由中的详情id去获取内容,当我们在不同的详情页来回切换的时候,打开的页面是同一个,只是需要通过监听路由中的参数id的变化去重新请求详情接口。如果这个详情页只需要一个接口
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
艾木酱 艾木酱
2年前
我们也从 Python 转向了 Golang -- MemFireDB
首先说明一下,Python也是我最喜欢的一门编程语言,我用Python工作了接近8年,并且会一直使用下去。我们团队在开启这个项目之初就做出了从Python往golang转换的预期,因此我们的转换过程没有任何障碍,非常顺利的就完成了。我们为什么会在项目开启之初就做出要更换编程语言的决定呢,为什么不一开始就选择Golang呢?第一个问题
Easter79 Easter79
2年前
springboot2之优雅处理返回值
前言最近项目组有个老项目要进行前后端分离改造,应前端同学的要求,其后端提供的返回值格式需形如{"status":0,"message":"success","data":{}}方便前端数据处理。要实现前端同学这个需求,其实也挺简单的,
Stella981 Stella981
2年前
Scapy 从入门到放弃
0x00前言最近闲的没事,抽空了解下地表最强的嗅探和收发包的工具:scapy。scapy是一个python模块,使用简单,并且能灵活地构造各种数据包,是进行网络安全审计的好帮手。0x01安装因为2020年python官方便不再支持python2,所以使用python3安装。!(https://oscimg.oschina.net/os
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
Framework7 + Angular 开发问题解决汇总
本篇主要汇总一下使用Framework7Angular开发中遇到的一些难点及我的解决方法,以后再遇到会在这里继续更新。一、页面表格按需加载情况描述:默认加载10条,在用户上拉页面是再进行下一页的内容加载。解决方法:利用Framework7的无限滚动。1、页面:<tbodyid"orderContent"
Stella981 Stella981
2年前
Linux日志安全分析技巧
0x00前言我正在整理一个项目,收集和汇总了一些应急响应案例(不断更新中)。GitHub地址:https://github.com/Bypass007/EmergencyResponseNotes本文主要介绍Linux日志分析的技巧,更多详细信息请访问Github地址,欢迎Star。0x01日志简介Lin
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这