Capturar telas de páginas web ou arquivos HTML em .NET (C#)
Use Selenium WebDriver para abrir uma página no Chrome sem interface gráfica e salvar sua área de visualização visível como PNG. A aplicação de console em C# abaixo aceita um caminho HTML local ou uma URL HTTP, aguarda um elemento que sinaliza que o conteúdo está pronto e salva a captura de tela sem substituir um arquivo existente.
Pré-requisitos
Este tutorial usa Bash no Linux, o SDK do .NET 10
e um navegador Chrome ou Chromium instalado com um ChromeDriver compatível. Ele foi testado com o
SDK 10.0.111 e o Chromium/ChromeDriver 152.0.7977.82. Instale as atualizações de manutenção atuais do
.NET 10, que é uma versão LTS com suporte ativo.
O exemplo HTTP local opcional também precisa de uma instalação do Python 3 que receba manutenção e
ofereça as opções http.server --bind e --directory; ele foi testado com
Python 3.14.7.
Renderize apenas páginas e arquivos HTML em que você confia. Um navegador pode executar o JavaScript deles e fazer requisições de rede. Este exemplo captura a área de visualização, incluindo quaisquer áreas em branco; ele não captura uma página inteira com rolagem, não valida códigos de status HTTP nem comprova que uma aplicação qualquer terminou de carregar.
Verifique as ferramentas instaladas antes de criar o projeto:
dotnet --version && chromium --version && chromedriver --version
Para o exemplo HTTP, verifique o Python e confirme que sua ajuda lista ambas as opções antes de criar o projeto:
python3 --version && python3 -m http.server --help
Se você usar o Google Chrome, substitua chromium pelo executável dele. Defina
esses caminhos de acordo com o navegador e o driver compatível na sua máquina. Estes são os caminhos
usados neste exemplo para Linux:
export CHROME_BINARY=/usr/bin/chromium &&
export CHROMEDRIVER_PATH=/usr/bin/chromedriver
Usar Selenium WebDriver
Comece em um diretório com permissão de escrita fora de um repositório .NET existente. Configurações
de SDK, NuGet e MSBuild nos diretórios ancestrais podem alterar o comportamento de um novo projeto.
O bloco de configuração recusa esses arquivos ancestrais e um destino WebpageScreenshots
existente antes de criar qualquer coisa. Execute-o no Bash:
(
cd -P . || exit 1
parent="$PWD"
while :; do
for config in global.json NuGet.Config NuGet.config nuget.config \
Directory.Build.props Directory.Build.targets Directory.Packages.props; do
if [ -e "$parent/$config" ]; then
printf 'Choose a directory outside this .NET configuration: %s\n' "$parent/$config" >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent=${parent%/*}
[ -n "$parent" ] || parent=/
done
if [ -e WebpageScreenshots ]; then
printf 'WebpageScreenshots already exists; keep it and choose a new directory.\n' >&2
exit 1
fi
dotnet new console -n WebpageScreenshots --framework net10.0 --no-restore &&
dotnet add WebpageScreenshots package Selenium.WebDriver --version 4.28.0 &&
dotnet add WebpageScreenshots package Selenium.Support --version 4.28.0
)
Os pacotes estão fixados na versão 4.28.0 do Selenium, que foi testada. Se a instalação dos pacotes falhar, mantenha o novo projeto, corrija o problema de NuGet ou de rede informado e tente novamente as etapas de dependências a partir do mesmo diretório pai:
dotnet add WebpageScreenshots package Selenium.WebDriver --version 4.28.0 &&
dotnet add WebpageScreenshots package Selenium.Support --version 4.28.0
Substitua o WebpageScreenshots/Program.cs gerado por este programa completo. Os argumentos são
input output.png width height ready-element-id timeout-seconds. A largura e a altura descrevem a
área de visualização em pixels CSS; o tempo limite se aplica separadamente à navegação e à espera
pelo conteúdo ficar pronto.
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
try
{
if (args.Length == 3 && args[0] == "cleanup")
{
int days = int.Parse(args[2], CultureInfo.InvariantCulture);
if (days < 1) throw new ArgumentException("Retention must be at least one day.");
string directory = Path.GetFullPath(args[1]);
DateTime cutoff = DateTime.UtcNow.AddDays(-days);
foreach (string file in Directory.EnumerateFiles(directory, "webshot-*.png"))
{
if (Regex.IsMatch(Path.GetFileName(file), @"\Awebshot-[A-Za-z0-9_-]+\.png\z")
&& File.GetLastWriteTimeUtc(file) < cutoff)
{
File.Delete(file);
Console.WriteLine($"Deleted {file}");
}
}
return 0;
}
if (args.Length != 6)
throw new ArgumentException("Usage: input output.png width height ready-element-id timeout-seconds; or cleanup directory days");
int width = int.Parse(args[2], CultureInfo.InvariantCulture);
int height = int.Parse(args[3], CultureInfo.InvariantCulture);
int seconds = int.Parse(args[5], CultureInfo.InvariantCulture);
if (width < 200 || width > 3840 || height < 200 || height > 2160)
throw new ArgumentException("Choose a viewport from 200x200 to 3840x2160.");
if (seconds < 1 || seconds > 60 || string.IsNullOrWhiteSpace(args[4]))
throw new ArgumentException("Supply a readiness element ID and a timeout from 1 to 60 seconds.");
string url;
if (Uri.TryCreate(args[0], UriKind.Absolute, out Uri? remote)
&& (remote.Scheme == "http" || remote.Scheme == "https"))
{
url = remote.AbsoluteUri;
}
else
{
string path = Path.GetFullPath(args[0]);
if (!File.Exists(path)) throw new FileNotFoundException("HTML file not found.", path);
// Escape each Linux path segment so literal %, #, and ? keep their filename identity.
url = "file://" + string.Join("/", path.Split('/').Select(Uri.EscapeDataString));
}
string outputPath = Path.GetFullPath(args[1]);
if (!outputPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("The output filename must end in .png.");
if (File.Exists(outputPath) || Directory.Exists(outputPath))
throw new IOException("Output already exists; choose a new filename.");
string browser = Environment.GetEnvironmentVariable("CHROME_BINARY")
?? throw new ArgumentException("Set CHROME_BINARY to your browser executable.");
string driverPath = Environment.GetEnvironmentVariable("CHROMEDRIVER_PATH")
?? throw new ArgumentException("Set CHROMEDRIVER_PATH to your driver executable.");
if (!File.Exists(browser) || !File.Exists(driverPath))
throw new FileNotFoundException("Browser or ChromeDriver executable not found.");
var options = new ChromeOptions { BinaryLocation = browser };
options.AddArgument("--headless=new");
using var service = ChromeDriverService.CreateDefaultService(
Path.GetDirectoryName(driverPath), Path.GetFileName(driverPath));
byte[] png;
object result;
using (var driver = new ChromeDriver(service, options, TimeSpan.FromSeconds(30)))
{
driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(seconds);
driver.Manage().Window.Size = new System.Drawing.Size(Math.Max(800, width), Math.Max(600, height));
driver.ExecuteCdpCommand("Emulation.setDeviceMetricsOverride", new Dictionary<string, object>
{
["width"] = width,
["height"] = height,
["deviceScaleFactor"] = 1,
["mobile"] = false
});
driver.Navigate().GoToUrl(url);
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(seconds));
wait.Until(d => d.FindElement(By.Id(args[4])).Displayed);
png = driver.TakeScreenshot().AsByteArray;
var window = driver.Manage().Window.Size;
result = new
{
url = driver.Url,
viewportWidth = driver.ExecuteScript("return window.innerWidth"),
viewportHeight = driver.ExecuteScript("return window.innerHeight"),
windowWidth = window.Width,
windowHeight = window.Height,
devicePixelRatio = driver.ExecuteScript("return window.devicePixelRatio")
};
}
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
using (var output = new FileStream(outputPath, FileMode.CreateNew, FileAccess.Write))
output.Write(png);
Console.WriteLine($"Saved {args[1]}");
Console.WriteLine(JsonSerializer.Serialize(result));
return 0;
}
catch (Exception error)
{
Console.Error.WriteLine($"Screenshot failed: {error.Message}");
return 1;
}
Os escopos using liberam o navegador e o serviço do driver tanto na conclusão
normal quanto em caso de exceções. O programa armazena o PNG em um buffer antes de abrir o destino,
portanto uma falha na navegação ou na espera pelo conteúdo não cria uma captura de tela.
FileMode.CreateNew também recusa um arquivo de destino criado desde a verificação inicial.
Uma falha de escrita em disco pode deixar um novo arquivo parcial; inspecione e remova apenas esse
arquivo antes de tentar novamente.
Compatibilidade com navegadores
Esta implementação seleciona explicitamente o Chrome e o ChromeDriver. Mantenha suas versões compatíveis; o guia de seleção de versões do ChromeDriver explica como obter um par compatível. O Selenium Manager pode localizar e baixar drivers, mas este programa usa os caminhos que você forneceu. O Firefox e o Edge precisam de suas respectivas classes de driver e de opções; eles estão fora do escopo deste tutorial.
Capturar arquivos HTML locais
Salve esta página como WebpageScreenshots/sample.html. Ela tem um fundo azul e um cartão branco com
o texto LOCAL PAGE. O marcador que sinaliza que o conteúdo está pronto está presente imediatamente:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Local screenshot example</title>
<style>
html, body { margin: 0; background: #0c5393; }
#content-loaded { margin: 32px; padding: 24px; background: white; color: black;
font: bold 32px sans-serif; }
</style>
<h1 id="content-loaded">LOCAL PAGE</h1>
</html>
Compile e execute a partir do diretório pai. && impede que uma falha na
compilação leve à execução de um binário antigo, e -- passa os argumentos
restantes para a aplicação:
dotnet build WebpageScreenshots --disable-build-servers &&
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-local.png 800 600 content-loaded 10
Em caso de sucesso, o terminal imprime Saved screenshots/webshot-local.png seguido de um JSON que descreve
a URL efetiva, a área de visualização, a janela externa e a proporção de pixels. Abra esse PNG: ele
deve ter 800 × 600 pixels, com o cartão LOCAL PAGE inteiro perto do topo e um fundo azul
que se estende até a parte inferior.
A área de visualização no JSON deve ser de 800 × 600, com uma proporção de pixels de um. Um PNG que
não esteja vazio, por si só, não comprova que você capturou a página pretendida.
Lidar com conteúdo dinâmico
A navegação aguarda o documento, mas o JavaScript pode atualizar a página depois disso. Use um elemento visível que sinalize que o conteúdo está pronto e que sua aplicação revele somente quando o conteúdo desejado estiver pronto. Um elemento contêiner visível que já existe enquanto seus dados ainda estão carregando não é um sinal adequado.
Salve esta segunda página como WebpageScreenshots/delayed.html. O cartão começa oculto e aparece após
1,5 segundos, com um fundo verde que facilita identificar uma captura feita cedo demais:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Delayed screenshot example</title>
<style>
html, body { margin: 0; background: white; }
#content-loaded { margin: 32px; padding: 24px; background: white; color: black;
font: bold 32px sans-serif; }
</style>
<h1 id="content-loaded" hidden>DELAYED PAGE</h1>
<script>
setTimeout(() => {
document.documentElement.style.background = '#198754';
document.body.style.background = '#198754';
document.getElementById('content-loaded').hidden = false;
}, 1500);
</script>
</html>
Para ter uma fonte HTTP reproduzível, verifique python3 --version e execute este servidor
em primeiro plano em um segundo terminal, a partir do mesmo diretório pai. Ele
serve o diretório selecionado na interface de loopback:
python3 -m http.server 8765 --bind 127.0.0.1 --directory WebpageScreenshots
Se essa porta estiver ocupada, escolha uma porta livre e atualize ambas as URLs abaixo. Mantenha o servidor em execução enquanto captura a página estática e a página com conteúdo atrasado:
dotnet run --project WebpageScreenshots --no-build -- \
http://127.0.0.1:8765/sample.html screenshots/webshot-http.png 800 600 content-loaded 10 &&
dotnet run --project WebpageScreenshots --no-build -- \
http://127.0.0.1:8765/delayed.html screenshots/webshot-delayed.png 800 600 content-loaded 10
Abra ambos os arquivos. A imagem estática deve corresponder à página local azul; a imagem com
conteúdo atrasado deve mostrar DELAYED PAGE em seu cartão branco sobre o fundo verde, com 800 × 600 pixels.
Uma imagem com conteúdo atrasado totalmente branca significa que você fez a captura cedo demais.
Ao terminar, pare o servidor com Ctrl+C no terminal dele.
Um marcador ausente ou permanentemente oculto faz a CLI encerrar com código de saída um, sem criar
um novo PNG, após o tempo de espera se esgotar. Para uma aplicação real, substitua
content-loaded pelo ID real do elemento que sinaliza que o conteúdo está pronto.
Gerenciar o armazenamento de capturas de tela
Faça as capturas em um diretório que pertença a você e escolha um novo nome de arquivo para cada imagem que quiser manter. Repetir um comando com um destino já existente falha e preserva esse arquivo. A captura não altera os PNGs vizinhos.
Para a retenção opcional, reserve para esta aplicação os nomes de arquivo que correspondam a
webshot-[letters, digits, underscores, or hyphens].png. A partir do diretório pai, este comando exclui os arquivos
correspondentes cuja última escrita ocorreu há mais de sete dias, sem entrar em subdiretórios:
dotnet run --project WebpageScreenshots --no-build -- cleanup screenshots 7
Os demais nomes de arquivo não são afetados. O prefixo é uma convenção, então use a limpeza apenas em um diretório sob seu controle no qual todos os arquivos correspondentes pertençam a esta aplicação. Para manter todas as capturas de tela, omita a limpeza. Um erro de exclusão retorna código de saída um; as exclusões anteriores nessa execução não são desfeitas.
Lidar com diferentes tamanhos de tela
A CLI usa a substituição de métricas de dispositivo do Chrome para definir a área de visualização e uma proporção de pixels de um. A janela externa do navegador pode ser maior, especialmente em larguras menores. Isso testa os pontos de quebra do CSS sem alterar o agente de usuário nem ativar a emulação de toque em dispositivos móveis; não reproduz um celular específico.
Para uma área de visualização estreita, mantenha a mesma entrada e o mesmo marcador que sinaliza que o conteúdo está pronto e altere as dimensões:
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-narrow.png 375 667 content-loaded 10
Verifique se o PNG tem 375 × 667 pixels e se o cartão continua legível. O programa aceita larguras de 200 a 3.840 e alturas de 200 a 2.160; capturas maiores estão fora dos limites deste exemplo.
Diagnosticar uma captura malsucedida
Use o código de saída diferente de zero e a mensagem de erro da CLI para identificar a etapa que falhou. Um arquivo HTML ausente é informado antes de o Chrome iniciar. Um navegador ou driver ausente ou incompatível impede que uma sessão seja iniciada. Se o tempo de espera pelo conteúdo se esgotar, verifique o ID e se o elemento se torna visível; aumentar o tempo limite não corrige um marcador incorreto. Em caso de erro no arquivo de destino, verifique as permissões do diretório e escolha um nome de arquivo ainda não utilizado. Preserve as capturas de tela anteriores enquanto corrige a entrada ou a configuração e execute novamente o mesmo comando com um novo destino.
