当前位置: 首页 > news >正文

网页截图指南

截取网页截图看似是一项简单的任务,但当你真正动手去做的时候,就会发现事情远没有那么容易。我在尝试截取一篇很长的 Reddit 帖子时就深有体会。一开始我以为只要调用 browser.TakeImage() 就万事大吉,结果却陷入了浏览器视口、动态内容加载、内存占用等一系列问题。

在这篇文章中,我将分享从一开始的“想当然”到最后找到高效方案的全过程。我们将以 r/dotnet 子版块为案例,探索常见的陷阱以及如何避免这些问题。看完这篇文章后,你将掌握如何稳定、完整地截取包含动态内容与无限滚动的网页截图的方法。

基础截图方式

我们先从最基础的方法开始:不进行任何特殊处理,直接截取截图。在 DotNetBrowser 中,这非常简单:

var image = browser.TakeImage();
var bitmap = ToBitmap(image);
bitmap.Save("screenshot.png", ImageFormat.Png);

由于 DotNetBrowser 返回的是原始位图,我们需要一个工具方法将其转换为 System.Drawing.Bitmap ,以便进行标准的 .NET 操作:

public static Bitmap ToBitmap(DotNetBrowser.Ui.Bitmap bitmap)
{var width = (int)bitmap.Size.Width;var height = (int)bitmap.Size.Height;var data = bitmap.Pixels.ToArray();var bmp = new Bitmap(width, height, PixelFormat.Format32bppRgb);var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),ImageLockMode.WriteOnly, bmp.PixelFormat);Marshal.Copy(data, 0, bmpData.Scan0, data.Length);bmp.UnlockBits(bmpData);return bmp;
}

运行这段代码后,我们得到了第一张截图:

使用 DotNetBrowser 制作的简单屏幕截图

使用 [DotNetBrowser](https://teamdev.cn/dotnetbrowser/?utm_source=csdn&utm_medium=article&utm_campaign=taking-screenshots-of-web-pages) 制作的简单屏幕截图

但问题显而易见:我们只截取了页面的一小部分。

这是因为 TakeImage() 方法仅截取浏览器视口内可见的内容,而默认视口大小并不足以显示整个页面。

截取整页截图

我们需要先获取网页的尺寸,并将浏览器调整为相应大小,然后重新尝试截图。可以通过 JavaScript 获取页面的尺寸,方法是取 documentdocument.documentElement 中较大的那个值:

var widthScript = @"Math.max(document.body.scrollWidth,document.documentElement.scrollWidth)";var heightScript = @"Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)";

然后调整浏览器大小以匹配这些尺寸:

var frame = browser.MainFrame;
var width = frame.ExecuteJavaScript<double>(widthScript).Result;
var height = frame.ExecuteJavaScript<double>(heightScript).Result;browser.Size = new Size((uint)width, (uint)height);
var image = browser.TakeImage();

以下是使用这种改进方法得到的结果:

截图区域变大了,但内容尚未加载完成

截图区域变大了,但内容尚未加载完成。

查看完整图片

紧接着出现了另一个问题:页面并未完全加载。这是因为我们在调整浏览器大小后立即进行了截图,没有给浏览器足够的时间来加载和渲染所有内容。从页面底部的加载指示器可以看出这一点。

让我们填加一个暂停等待时间:

browser.Size = new Size((uint)width, (uint)height);
// 通过反复试验得出的任意数字。
Thread.Sleep(2000);
var image = browser.TakeImage();

再试一次:

包含所有内容的完整截图

包含所有内容的完整截图

查看完整图片

终于,我们得到了一张像样的截图!但位图对象变得相当大,而且 Chromium 需要大量资源来渲染大视口,这在实际应用中并不实用。我们可以做得更好。

截取分段截图

使用之前的方法,浏览器会一次性渲染整个页面,并将结果位图传递给 .NET 进程内存。在处理长页面时,这会迅速耗尽系统资源,尤其是 RAM。

与其尝试一次性截取整个页面,不如我们将其分解为更小、更易管理的部分:

  1. 对页面的各个小段进行多次截图。
  2. 在每次截取之间滚动屏幕。
  3. 将各个部分拼接成最终图像。

以下是实现这种方法的代码:

// Reddit 页面是无限滚动的,因此我们需要设置一个固定的截图次数。
var numberOfShots = 15;
var viewportHeight = 1000;
browser.Size = new Size((uint)Width, (uint)viewportHeight);var capturedHeight = 0;
for (var count = 0; count < numberOfShots; count++)
{capturedHeight += viewportHeight;frame.ExecuteJavaScript($"window.scrollTo(0, {capturedHeight})").Wait();// 为了等待内容加载而设定的任意暂停时间。Thread.Sleep(500);var image = browser.TakeImage();var bitmap = ToBitmap(image);bitmap.Save($"screenshot-{count:D3}.png", ImageFormat.Png);
}

在实际应用中,我们会在另一台服务器上的异步任务中拼接这些片段。但为了简单起见,我们选择直接使用这个辅助函数:

public static Bitmap MergeBitmapsVertically(List<Bitmap> bitmaps)var files = Directory.GetFiles("/path/to/directory", "*.png").OrderBy(Path.GetFileName).ToArray();var images = files.Select(f => Image.FromFile(f)).ToArray();int width = images.Max(img => img.Width);int totalHeight = images.Sum(img => img.Height);using var merged = new Bitmap(width, totalHeight);using (var g = Graphics.FromImage(final)){int y = 0;foreach (var img in images){g.DrawImage(img, 0, y);y += img.Height;}}merged.Save("merged.png");
}

让我们看看结果如何:

合并后的截图片段包含重复元素

合并后的截图片段包含重复元素

查看完整图片

又一个新问题出现了:页眉和导航侧边栏在每张分段截图里重复出现,这显然不是截图应有的样子!

处理固定元素

当我们滚动页面时,像页眉和侧边栏这样的固定元素会保持在原位不动。但在进行分段截图时,我们希望这些固定元素只出现在第一段截图中。

让我们通过 CSS 的 position 属性来找出这些元素,并将它们隐藏起来:

var removeFixedElements = @"(() => {document.querySelectorAll('*').forEach(el => {const pos = getComputedStyle(el).position;if (pos === 'fixed' || pos === 'sticky') {el.style.display = 'none';}});})()";for (var count = 0; count < numberOfShots; count++)
{// 仅从第二张截图开始移除固定元素,// 这样可以保留第一张截图中的页眉和导航栏。if (count == 1) {frame.ExecuteJavaScript(removeFixedElements).Wait();}// 继续进行截图操作。
}

另一个常见的固定元素是 Cookie 同意弹窗。由于每个网站的实现方式都不一样,我们需要逐个页面单独处理。

以 Reddit 为例,我们可以使用以下代码识别并隐藏 Cookie 弹窗:

var removeCookieScreen =@"(() => {const dialog = document.querySelector('reddit-cookie-banner');if (dialog) {dialog.style.display = 'none';}})()";
frame.ExecuteJavaScript(removeCookieScreen).Wait();

最终,我们以节省内存的方式截取了一张合适的网页截图。

一张干净、完整的网页截图

一张干净、完整的网页截图

查看完整图片

总结

最初只是一个简单的 browser.TakeImage() 调用,最后演变成一个需要应对无数细节的复杂解决方案。我们解决了以下挑战:

  • 一次性截取整页内容;
  • 将长页面分段截取并拼接;
  • 处理固定元素和 Cookie 弹窗;
  • 优化内存使用和实用性。

需要说明的是,本文介绍的方法并非放之四海而皆准。每个网页都有其独特之处,而优秀的截图工具之所以强大,正是因为它能处理各种边缘场景。尽管如此,本文所展示的方法已经涵盖了使用 DotNetBrowser 进行网页截图所需掌握的所有核心知识。

相关文章:

  • 存储系列知识
  • k8s node 报IPVS no destination available
  • Vue3+ Vite + Element-Plus + TypeScript 从0到1搭建
  • 卡特兰数--
  • 25_05_02Linux架构篇、第1章_03安装部署nginx
  • 【爬虫】码上爬第6题-倚天剑
  • 静态库和动态库的区别
  • SQL Server执行安装python环境
  • 用OMS从MySQL迁移到OceanBase,字符集utf8与utf8mb4的差异
  • Python实例题:高德API+Python解决租房问题
  • 室内烟雾明火检测数据集VOC+YOLO格式2469张2类别
  • 驱动开发系列57 - Linux Graphics QXL显卡驱动代码分析(四)显示区域绘制
  • 【专家库】Kuntal Chowdhury
  • 【挖洞利器】GobyAwvs解放双手
  • 基站综合测试仪核心功能详解:从射频参数到5G协议测试实战指南
  • RabbitMQ-api开发
  • 天文探秘学习小结
  • Java 函数式编程
  • 基于GA遗传优化的不同规模城市TSP问题求解算法matlab仿真
  • 删除排序链表中的重复元素:三种解法详解
  • 又一日军“慰安妇”制度受害者去世,大陆登记在册幸存者仅剩7人
  • 探索人类的心灵这件事,永远也不会过时
  • 鸿蒙概念股强势上涨,鸿蒙电脑本月正式发布,生态链即将补全
  • 机器人助力、入境游、演出引流:假期纳客千万人次城市有高招
  • 中东睿评|胡塞武装已成为楔入中东各方力量之间的钉子户
  • 李学明谈笔墨返乡:既耕春圃,念兹乡土