缘起
目前有一个Python项目,作用是打开一个报表页面,截图后生成PDF,作为邮件附件发送到指定邮箱。当前这个Python项目性能较差,目前项目组没有人会Python,于是决定使用golang重写一下
模拟滚动更新
这个页面是滚动更新的,滚动到某个地方才会加载下面的内容,光在这个滚动更新上就踩了很多坑。
- 踩坑
window.scrollTo(x,y)这个函数在静态页面是可以的,x代表横向偏移,y代表纵向偏移,x我设定的0,y设定的2400(通过页面某个元素可以获取到我当页面的最大值是2400),按F12打开控制台,将这段代码放进去,根本不起作用 - 踩坑
document.body.scrollTo(x,y)使用方式同上,同样不起作用 - 踩坑
document.documentElement.scrollTo(x,y)同样不起作用 - []byte转int总是0 就是下面这个代码
1bytesBuffer := bytes.NewBuffer(b) 2var x int32 3binary.Read(bytesBuffer, binary.BigEndian, &x) - 真理
document.getElementById('xxxx-head').scrollTop=2400'xxxx-head是我这个页面的最上面的可显示的<div>标签的id,这个可以将滚动条拉到最下面,我代码里面根据可视高度一点点的逼近最大值,其实就是每次滚动一屏,直到滚动到底为止,请看代码
核心代码
1/** 2fileNameUrlMap: key为文件名 value为url 3topDivId: 页面上最上面显示第一行字的div 用来定位页面的最高处 这里传入这个div的Id 4waitVisibleExpr: 页面加载到这个选择器说明页面加载完成了 5maxHighId: 获取页面最高的大小的div的id 6chromeCtx: 谷歌浏览器实例 7goTraceId: 日志中的traceId 8 9返回值: 10map的key为文件名,value是byte数组,返回生成的pdf的文件字节 11 12注意:我这个是动态页面 是滚动加载的页面,需要模拟人手动去滚动滑块,然后等待加载数据,一直到底部为止 13我当前的页面可以通过某个div获取到高度,如果你无法获取到高度,就往下滚动,监测本次滚动所能到达的高度和 14上一次的是否一致,是的话就说明滚动到底了 15*/ 16func ScreenPdf(fileNameUrlMap map[string]string, topDivId string, waitVisibleExpr string, 17 maxHighId string, chromeCtx context.Context, goTraceId string) map[string][]byte { 18 var ( 19 err error 20 resultMap = make(map[string][]byte) 21 ) 22 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始进行截图... param=%s ", fileNameUrlMap) 23 24 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("初始化chrome完成...") 25 26 for fileName, url := range fileNameUrlMap { 27 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始截图,当前处理文件名=%s url=%s", fileName, url) 28 chromeTabCtx, cancelFunc := chromedp.NewContext(chromeCtx, chromedp.WithLogf(config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof)) 29 //空任务触发初始化 30 err = chromedp.Run(chromeTabCtx, make([]chromedp.Action, 0, 1)...) 31 chromedp.Sleep(time.Second * 2) 32 if err != nil { 33 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("初始化chrome并执行第一个Task失败跳过此截图 fileName=%s", fileName) 34 continue 35 } 36 buf := make([]byte, 0) 37 err = chromedp.Run(chromeTabCtx, chromedp.Tasks{ 38 chromedp.Navigate(url), 39 chromedp.Sleep(time.Second * 10), 40 chromedp.ActionFunc(func(ctx context.Context) error { 41 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("开始等待页面加载 检测点=%s fileName=%s", waitVisibleExpr, fileName) 42 return nil 43 }), 44 chromedp.WaitVisible(waitVisibleExpr, chromedp.ByID), 45 chromedp.ActionFunc(func(ctx context.Context) error { 46 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("页面加载完成 检测点=%s fileName=%s", waitVisibleExpr, fileName) 47 var html string 48 chromedp.InnerHTML(waitVisibleExpr, &html, chromedp.ByID) 49 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("获取到的页面html=%s", html) 50 return nil 51 }), 52 chromedp.Sleep(time.Second * 15), 53 chromedp.ActionFunc(func(ctx context.Context) error { 54 //获取可视界面的高度 55 var jsGetClientHigh = "document.body.clientHeight" 56 clientHigh := getHighByJs(jsGetClientHigh, ctx) 57 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("可视高度为%d ", clientHigh) 58 //获取最高的 59 var jsGetMaxHigh = "document.getElementById('" + maxHighId + "').offsetHeight" 60 maxHigh := getHighByJs(jsGetMaxHigh, ctx) 61 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("最大高度为%d ", maxHigh) 62 var currentHigh = clientHigh 63 //滚动 64 for { 65 if currentHigh < maxHigh { 66 jsScroll := "document.getElementById('" + topDivId + "').scrollTop=" + strconv.Itoa(currentHigh) 67 chromedp.EvalAsValue(&runtime.EvaluateParams{ 68 Expression: jsScroll, 69 ReturnByValue: false, 70 }).Do(ctx) 71 time.Sleep(time.Second * 15) 72 currentHigh += clientHigh 73 } else { 74 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Infof("跳出高度%d fileName=%s", currentHigh, fileName) 75 break 76 } 77 } 78 //滚动完成后滚回第一屏 79 jsScroll0 := "document.getElementById('" + topDivId + "').scrollTop=0" 80 chromedp.EvalAsValue(&runtime.EvaluateParams{ 81 Expression: jsScroll0, 82 ReturnByValue: false, 83 }).Do(ctx) 84 time.Sleep(time.Second * 1) 85 //纸张设置为A0 86 buf, _, err = page.PrintToPDF().WithPaperWidth(33.1).WithPaperHeight(46.8).WithPrintBackground(true).Do(ctx) 87 return err 88 }), 89 }) 90 91 if err != nil { 92 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}).Errorf("截图出现报错 跳过当前PDF fileName=%s err=%v ", fileName, err) 93 continue 94 } 95 config.LogEntry.WithFields(logrus.Fields{config.GoTraceId: goTraceId}). 96 Infof("截图生成bytes完成 当前fileName=%s byteLength=%d", fileName, len(buf)) 97 resultMap[fileName] = buf 98 cancelFunc() 99 } 100 101 return resultMap 102} 103 104func getHighByJs(jsGetHigh string, ctx context.Context) int { 105 result, _, _ := chromedp.EvalAsValue(&runtime.EvaluateParams{ 106 Expression: jsGetHigh, 107 ReturnByValue: true, 108 }).Do(ctx) 109 json, _ := result.Value.MarshalJSON() 110 clientHigh := bytesToInt(json) 111 return clientHigh 112} 113 114func bytesToInt(bys []byte) int { 115 length := float64(len(bys)) - 1 116 var x float64 117 for _, value := range bys { 118 tmp := math.Pow(10, length) 119 x = x + (float64(value)-48)*tmp 120 length-- 121 } 122 return int(x) 123 124}
上面用到了chrome的实例,实例初始化如下:
1import ( 2 "context" 3 "github.com/chromedp/chromedp" 4 "os" 5) 6 7var ChromeCtx context.Context 8 9/** 10chrome初始化 全局使用这一个实例即可 11*/ 12func init() { 13 14 var headlessFlag chromedp.ExecAllocatorOption 15 //headless这个默认是true,如果想要在本地调试的时候看下浏览器的行为,可以在 16 //环境变量里添加headless=false 就可以在本地调试并观察浏览器被控制的行为了 17 isHeadless := os.Getenv("headless") 18 if isHeadless == "false" { 19 headlessFlag = chromedp.Flag("headless", false) 20 } else { 21 headlessFlag = chromedp.Flag("headless", true) 22 } 23 opts := append( 24 chromedp.DefaultExecAllocatorOptions[:], 25 //不检查默认浏览器 26 chromedp.NoDefaultBrowserCheck, 27 //无头 28 headlessFlag, 29 //忽略错误 30 chromedp.IgnoreCertErrors, 31 //不加载gif图像 因为有可能会卡住 32 chromedp.Flag("blink-settings", "imagesEnabled=true"), 33 //关闭GPU渲染 34 chromedp.DisableGPU, 35 //不适用谷歌的sanbox模式运行 36 chromedp.NoSandbox, 37 //设置网站不是首次运行 38 chromedp.NoFirstRun, 39 //禁用网络安全标志 40 chromedp.Flag("disable-web-security", true), 41 //关闭插件支持 42 chromedp.Flag("disable-extensions", true), 43 //关闭默认浏览器检查 44 chromedp.Flag("disable-default-apps", true), 45 //初始大小 46 chromedp.WindowSize(1920, 1080), 47 //在呈现所有数据之前防止创建Pdf 48 chromedp.Flag("run-all-compositor-stages-before-draw", true), 49 //设置userAgent 不然chrome会标识自己是个chrome爬虫 会被反爬虫网页拒绝 50 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 51 ) 52 53 ChromeCtx, _ = chromedp.NewExecAllocator(context.Background(), opts...) 54}