一、写在前面:为什么我们要给网页"拍照"
在做站点巡检、内容合规核查、竞品比对、以及爬虫结果验证时,单纯把"标题 + 链接"存成文本是远远不够的。一张截图胜过千言万语:
- 视觉留痕:文字可能遗漏布局、广告、弹窗、样式错乱等关键信息,截图能 100% 还原用户当时看到的真实画面。
- 问题取证:当页面触发反爬验证码、被重定向、或接口超时,截图能立刻告诉我们"到底看到了什么",而不是对着一段报错日志猜测。
- 自动归档:把每天访问的页面截图按时间戳落盘,天然形成一份可视化的站点快照档案,事后回溯一目了然。
- 降低沟通成本:把截图直接贴进报告或工单,非技术的同事也能秒懂问题所在。
本篇博客不聊"怎么抓数据"(这部分我在上一篇文章讲过),而是单点突破"截图"这件事:如何用 Java 调用 Playwright,对访问的页面进行全页截图,并进一步把列表页里命中的文章详情页也逐个截图,最终把所有图片按固定目录、安全文件名、时间戳规则整齐地保存下来。
完整工程基于 Maven + Java 11+ + Playwright for Java,示例站点为腾讯云开发者社区(
https://cloud.tencent.com),配置项支持一键切换到任意站点。
二、环境准备
| 依赖 | 版本 | 说明 |
|---|---|---|
| JDK | 11+(本文 21 验证) | Playwright for Java 要求 Java 11+ |
| Maven | 3.6+(本文 3.9.12) | 编译与 fat-jar 打包 |
| 网络 | 首次需联网 | 自动下载 Chromium 内核,仅一次 |
小提示:Playwright 首次运行会下载浏览器内核(几百 MB),之后缓存复用、启动很快。截图功能对内核版本无特殊要求,开箱即用。
三、整体设计思路
我们希望截图能力具备以下特性,而不是写死一段 page.screenshot():
- 可开关:后台批量跑数据时,可能不想每次都截图,用开关一键关闭。
- 固定目录:所有图片统一落到项目下的
screenshots/文件夹,便于管理。 - 可配格式:支持
png/jpeg,按场景取舍(png 无损大、jpeg 体积小)。 - 全页截图:不仅截首屏,还截"滚到底"的完整页面。
- 文件名安全:用访问路径生成文件名,过滤非法字符,并拼接时间戳防重名覆盖。
- 失败也留证:即使页面加载超时/异常,也要尽量截一张图,方便排查。
为此,我把配置与逻辑彻底分离:配置集中在 TencentConfig,截图动作封装成 captureScreenshot,并在访问主流程与文章爬取流程里统一调用。
四、配置层:把"截到哪、截不截、截什么格式"做成旋钮
先来看配置类 TencentConfig,新增了三组与截图相关的常量,且都支持用 -D 系统属性在命令行覆盖:
/** 是否对访问的页面截图保存(可被 -Denable.screenshot 覆盖,默认 true) */
public static final boolean ENABLE_SCREENSHOT =
!"false".equalsIgnoreCase(System.getProperty("enable.screenshot", "true"));
/** 截图保存目录(固定文件夹,可被 -Dscreenshot.dir 覆盖,默认 screenshots) */
public static final String SCREENSHOT_DIR =
System.getProperty("screenshot.dir", "screenshots");
/** 截图图片类型(png / jpeg,可被 -Dscreenshot.type 覆盖,默认 png) */
public static final String SCREENSHOT_TYPE =
System.getProperty("screenshot.type", "png").toLowerCase();
这样设计的好处:写代码时几乎不用动,跑的时候用命令行就能改行为。例如:
# 默认:截图保存到 ./screenshots(png)
java -jar target/playwright-tencent.jar
# 改用 jpeg,并指定到 D 盘目录
java -Dscreenshot.type=jpeg -Dscreenshot.dir=D:/shots -jar target/playwright-tencent.jar
# 只看数据、不截图
java -Denable.screenshot=false -jar target/playwright-tencent.jar
/** 是否进一步访问并截图文章详情页(可被 -Dcrawl.articles 覆盖,默认 true) */
public static final boolean CRAWL_ARTICLES =
!"false".equalsIgnoreCase(System.getProperty("crawl.articles", "true"));
/** 文章详情页最多访问数量(可被 -Dmax.articles 覆盖,默认 5) */
public static final int MAX_ARTICLES =
Integer.parseInt(System.getProperty("max.articles", "5"));
/** 判定为文章详情页的 URL 特征(子串匹配,可被 -Darticle.pattern 覆盖) */
public static final String ARTICLE_URL_PATTERN =
System.getProperty("article.pattern", "/developer/article/");
后三项用于控制"是否顺带把列表页里命中的文章详情页也截图",这是本文的第二个重点。
五、核心实现:一行 page.screenshot() 背后的工程化封装
5.1 截图方法 captureScreenshot
下面是整个截图能力的心脏。page.screenshot(...) 本身只有一行,但我们在外面裹了"建目录、算文件名、防重名、容错"四件套:
/**
* 对页面进行全页截图,保存到固定目录 SCREENSHOT_DIR。
* 文件名由 label 安全化后拼接时间戳生成,避免重名覆盖。
*/
private String captureScreenshot(String label, Page page) {
if (!TencentConfig.ENABLE_SCREENSHOT) {
return null; // 开关关闭时直接跳过
}
try {
Path dir = Paths.get(TencentConfig.SCREENSHOT_DIR);
if (!Files.exists(dir)) {
Files.createDirectories(dir); // 目录不存在则自动创建
}
// 把 label 中的非法文件名字符替换成下划线,确保跨平台安全
String safeName = (label == null || label.isEmpty())
? "page" : label.replaceAll("[^a-zA-Z0-9._-]", "_");
// 时间戳保证同一页面多次访问不会互相覆盖
String fileName = safeName + "_" + System.currentTimeMillis()
+ "." + TencentConfig.SCREENSHOT_TYPE;
Path out = dir.resolve(fileName);
page.screenshot(new Page.ScreenshotOptions()
.setPath(out) // 直接写文件
.setFullPage(true)); // 全页截图(含首屏之外内容)
System.out.println(" 截图已保存: " + out.toAbsolutePath());
return out.toAbsolutePath().toString();
} catch (Exception e) {
System.err.println(" 截图失败: " + e.getMessage());
return null;
}
}
几个关键决策点值得展开讲:
setFullPage(true):这是全页截图开关。默认false只截当前视口(viewport),对长文/列表页会"截一半"。设true后 Playwright 会自动滚动拼接出完整页面,对内容归档非常关键。setPath(out):直接把图片写到文件,省去我们手动Files.write字节数组的麻烦。- 安全文件名:Windows/Linux 对文件名都有黑名单字符(如
/ \ : * ? " < > |),路径里天然带/,必须过滤,否则Paths.resolve会写出错位的目录或抛异常。我们用正则[^a-zA-Z0-9._-]只保留"字母数字点下划线横杠"。 - 时间戳防重名:同一页面多次访问,文件名若只依赖路径,第二次会覆盖第一次。拼上
System.currentTimeMillis()毫秒时间戳,既唯一又保留了访问先后顺序,事后按文件名排序即可还原时间线。 - 容错:截图本身也可能失败(例如页面已崩溃),用 try-catch 包住,失败仅打印日志、返回
null,绝不影响主流程。
5.2 在主流程里"顺手"截图
访问逻辑被抽成 visitUrl(url, label),无论主页还是文章页都复用它。数据提取完成后,立刻调用截图;更关键的是,catch 异常分支里也再截一次,确保"看到报错页面"也能留证:
/** 访问一个绝对 URL,提取数据并截图,返回结果。 */
private VisitResult visitUrl(String url, String label) {
Page page = context.newPage();
String screenshotPath = null;
try {
System.out.println("[访问] " + url);
page.navigate(url, new Page.NavigateOptions()
.setWaitUntil(com.microsoft.playwright.options.WaitUntilState.LOAD));
page.waitForTimeout(TencentConfig.WAIT_AFTER_LOAD);
String title = page.title();
String h1 = extractFirstHeading(page);
List<String> links = extractContentLinks(page);
System.out.println(" 标题: " + title);
System.out.println(" 主标题: " + h1);
System.out.println(" 发现站内内容链接数: " + links.size());
screenshotPath = captureScreenshot(label, page); // ✅ 正常路径截图
return new VisitResult(url, title, h1, links, screenshotPath);
} catch (Exception e) {
System.err.println(" 访问失败: " + e.getMessage());
if (screenshotPath == null) {
screenshotPath = captureScreenshot(label, page); // ✅ 异常分支也截图
}
return new VisitResult(url, null, null, new ArrayList<>(), screenshotPath, e.getMessage());
} finally {
page.close();
}
}
六、进阶:列表页命中文章详情页,也要逐个截图
只截一个主页显然不够。真实需求往往是:进到某个作者主页/专栏页,把里面列出的文章详情页一个个点进去截图。我们用 attachArticles 实现这个"二级爬取 + 截图":
/** 根据主页面结果,筛选文章详情链接并逐个访问截图,挂载到 articles 字段。 */
private VisitResult attachArticles(VisitResult main) {
List<VisitResult> articles = new ArrayList<>();
if (main.error == null) {
// 1) 从主页提取的链接里,按 URL 特征过滤出"文章详情页"
List<String> candidates = main.repoLinks.stream()
.filter(u -> u.contains(TencentConfig.ARTICLE_URL_PATTERN))
.limit(TencentConfig.MAX_ARTICLES) // 2) 限制数量,避免无限爬
.collect(Collectors.toList());
if (!candidates.isEmpty()) {
System.out.println("[文章] 命中 " + candidates.size()
+ " 篇详情页,开始访问并截图...");
for (String articleUrl : candidates) {
// 3) 用 URL 末段做文件名标签,保证简短唯一
articles.add(visitUrl(articleUrl, labelFromUrl(articleUrl)));
}
} else {
System.out.println("[文章] 未命中符合特征的文章详情页,跳过");
}
}
// 把文章结果挂回主结果,最终一并写进 JSON
return new VisitResult(main.url, main.title, main.heading,
main.repoLinks, main.screenshotPath, main.error, articles);
}
/** 由 URL 派生截图文件名标签(取最后一个路径段,保证唯一且简短) */
private String labelFromUrl(String url) {
String u = url.replaceAll("^https?://", "").replaceAll("/+$", "");
String[] parts = u.split("/");
String last = parts[parts.length - 1];
return (last == null || last.isEmpty()) ? "page" : last;
}
设计要点:
- 按特征过滤:文章详情页 URL 通常有明显规律(腾讯云是
/developer/article/<id>)。用ARTICLE_URL_PATTERN做子串匹配,比写死正则更灵活,换站只需改一个-D参数。 - 限量爬取:详情页可能成百上千,
limit(MAX_ARTICLES)默认只截前 5 篇,既能验证能力又不至于把对方站点打挂(也保护自己电脑内存)。生产环境可配合"已访问去重 + 断点续跑"进一步完善。 - 复用
visitUrl:文章页与主页共用同一套"访问 + 提取 + 截图"逻辑,零重复代码,新增行为自动惠及所有页面。 - 结果回挂:每篇文章的
VisitResult(含各自的截图路径)挂到articles字段,最终在 JSON 里以嵌套数组呈现,数据可追溯。
visit(path) 作为对外入口,把"访问主页"和"抓文章详情页"串起来:
public VisitResult visit(String path) {
String url = buildUrl(path);
String label = (path == null || path.isEmpty() || "home".equalsIgnoreCase(path))
? "home" : path;
VisitResult r = visitUrl(url, label); // 先截主页
if (TencentConfig.CRAWL_ARTICLES) {
r = attachArticles(r); // 再截命中的文章详情页
}
return r;
}

图 3:从主页提取链接、过滤文章详情页、逐篇访问截图的二级爬取结构
七、运行效果与截图归档
编译打包并运行:
cd "d:/CodeBuddy WorkSpace/playwright-demo"
mvn clean package
java -jar target/playwright-tencent.jar
控制台输出(节选):
=== Playwright 自动访问 https://cloud.tencent.com 开始 ===
[访问] https://cloud.tencent.com/developer/user/12362774
标题: 夜郎King - 个人中心 - 腾讯云开发者社区-腾讯云
主标题: 夜郎King
发现站内内容链接数: 102
截图已保存: ...\screenshots\developer_user_12362774_1790086390362.png
[文章] 命中 5 篇详情页,开始访问并截图...
[访问] https://cloud.tencent.com/developer/article/2745971
标题: 基于Java的自助多张图片合成拼接实战
截图已保存: ...\screenshots\2745971_1790086487302.png
[访问] https://cloud.tencent.com/developer/article/2745974
标题: 基于SpringBoot和PostGIS的省域"地理难抵点"检索及可视化实践
截图已保存: ...\screenshots\2745974_1790086495443.png
[访问] https://cloud.tencent.com/developer/article/2745977
标题: 在SpringBoot中使用UniHttp简化天地图路径规划调用实践
截图已保存: ...\screenshots\2745977_1790086502966.png
已导出 JSON: ...\tencent_result.json (共 1 条记录)
=== 完成 ===
运行后 screenshots/ 目录的产出(文件名已做安全化 + 时间戳):
screenshots/
├── developer_user_12362774_1790086390362.png ← 主页截图
├── 2745971_1790086487302.png ← 文章详情页截图
├── 2745974_1790086495443.png ← 文章详情页截图
└── 2745977_1790086502966.png ← 文章详情页截图

图 4:程序运行后 screenshots/ 目录的实际产出

图 5:主页(developer/user/12362774)全页示意图

图 6:文章详情页(如 2745971)全页示意图
导出的 tencent_result.json 也会把每张图路径记录下来,方便与数据联动:
{
"site": "https://cloud.tencent.com",
"count": 1,
"results": [
{
"url": "https://cloud.tencent.com/developer/user/12362774",
"title": "夜郎King - 个人中心 - 腾讯云开发者社区-腾讯云",
"heading": "夜郎King",
"linkCount": 102,
"screenshotPath": ".../screenshots/developer_user_12362774_1790086390362.png",
"error": null,
"links": ["https://cloud.tencent.com/developer/article/2745971", "..."],
"articles": [
{
"url": "https://cloud.tencent.com/developer/article/2745971",
"title": "基于Java的自助多张图片合成拼接实战",
"screenshotPath": ".../screenshots/2745971_1790086487302.png",
"error": null,
"links": ["..."],
"articles": []
}
]
}
]
}
八、踩坑记录
这部分是实战精华,建议发布时结合自己的截图重点展开。
坑 1:只截到首屏,长页面下半截全丢
一开始没加 setFullPage(true),截出来只有视口那么高,列表页、长文的下半部分全无。加上 setFullPage(true) 后,Playwright 会自动滚动并拼接完整页面。代价是图片更大、耗时略长,对"归档"场景完全值得。
坑 2:文件名带 / 导致写出错位的目录甚至抛异常
直接用访问路径当文件名,developer/user/12362774 里的 / 会被当成目录分隔符,要么创建出嵌套目录,要么在 Windows 上因路径过长/非法而失败。统一用正则 [^a-zA-Z0-9._-] 替换为 _ 后彻底解决。
坑 3:同页面多次运行,截图被覆盖
没加时间戳时,第二次跑直接覆盖第一次的图,丢失历史快照。拼上 System.currentTimeMillis() 毫秒时间戳后,每次都是新文件,按文件名即可还原访问时间线。
坑 4:详情页加载超时,但"什么都没留下"
某些文章页偶发 load 超过 30s 触发 TimeoutError。如果只在 try 正常分支截图,失败页就毫无视觉证据。我们在 catch 里再截一次,虽然可能是空白/超时页,但至少留下了"此刻浏览器状态"的物证,排查效率提升明显。
坑 5:mvn clean 失败,jar 被占用
后台运行的 java -jar 进程锁住 target/*.jar,导致 mvn clean 删不掉。结束遗留 Java 进程即可正常构建。建议打包脚本里先 Stop-Process -Name java 再构建。
坑 6:Windows 控制台中文乱码
System.out 默认跟随系统编码(GBK),中文打印乱码。入口处用 new PrintStream(System.out, true, StandardCharsets.UTF_8) 重定向后,截图路径、标题等中文都正常显示。
九、可扩展方向
在"能截图"的基础上,还能继续增强,让截图能力更专业:
- 视口截图 vs 全页截图可切换:用
-Dscreenshot.fullpage=false控制,列表巡检用视口更快,归档用全页更全。 - 按元素截图:
page.locator("css").screenshot(...)只截某个区块(如只截正文、只截导航),避免无关广告干扰。 - 移动端视口截图:
new Browser.NewContextOptions().setViewportSize(390, 844)模拟手机宽度,验证移动端排版。 - 失败重试与限流:对超时页面加指数退避重试;控制访问频率,避免被站点限流。
- 截图加水印/时间戳:截图后在图片上叠加访问时间与 URL,归档时更易辨识来源。
- 结果可视化:把
screenshots/与tencent_result.json喂给一个简单前端,做成"站点快照浏览器"。
十、总结
Playwright 让"给网页自动拍照"变成一件极其简单的事——核心就是一行 page.screenshot()。但要把它用在生产级的数据采集里,需要补齐目录管理、文件名安全化、时间戳防重名、全页拼接、失败留证、二级爬取联动这些工程细节。本文以腾讯云开发者社区为例,演示了如何从主页一直截图到文章详情页,并把所有图片整齐归档到 screenshots/,同时把每张图路径写回 JSON,形成"数据 + 视觉"双保险。
完整工程已本地可运行:
mvn clean package后java -jar target/playwright-tencent.jar即可;想换站点用-Dsite.base=... -Dsite.paths=... -Darticle.pattern=...一行覆盖,无需改代码。
如果你也在用 Playwright 做自动化,欢迎在评论区聊聊你的截图策略——是全页、视口、还是按元素截?我们互相取取经。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/166373869





