Este artigo é uma tradução para o português, feita com ajuda de IA, do meu post original em inglês: Download all images from a web page with Python (srcset, lazy loading and og:image included). O código, os comandos e os números foram conferidos com o original e com o repositório antes da publicação.
"Baixar todas as imagens desta página" parece coisa de uma linha: buscar o HTML, achar cada <img src> e baixar uma por uma. Numa página de verdade, o resultado é uma pasta cheia de miniaturas pequenas, alguns placeholders cinza de lazy loading, uns pixels de rastreamento e, quem sabe, uma página de erro HTML salva como photo.jpg. As versões grandes ficam no srcset, as URLs reais ficam no data-src, e muitas vezes a melhor imagem da página só aparece numa tag <meta property="og:image">.
Escrevi uma pequena ferramenta em Python que trata esses casos usando só a biblioteca padrão: fecklesspleb/page-image-downloader (MIT, um único arquivo, sem navegador). Neste post eu mostro com o que ela precisa lidar e depois o que voltou de uma execução real.
1. Use o parser HTML do Python, não uma regex
O html.parser.HTMLParser faz parte da biblioteca padrão e lida bem com a ordem dos atributos, as aspas e as entidades (& na query string de uma URL é comum em CDNs de imagem). A ferramenta cria uma subclasse dele e registra, na ordem da página, todos os lugares onde uma imagem pode se esconder:
-
<img src>, além dedata-src,data-lazy-srcedata-original, que os scripts de lazy loading usam para guardar a URL real enquanto osrccontém um placeholder. Quando os dois existem, o atributo de lazy loading tem prioridade. -
<img srcset>e<picture><source srcset>, além dedata-srcset/data-lazy-srcset. -
<meta property="og:image">e<meta name="twitter:image">, a imagem que as redes sociais exibem quando a página é compartilhada. - Opcionalmente, links
<a href>que terminam nas extensões que você indicar, como.pdf.
Cada URL é resolvida em relação ao <base href> (se houver) e à URL da página com urllib.parse.urljoin, os #fragments são descartados e cada URL é guardada uma única vez por página.
2. Interprete o srcset direito e fique com a maior
Um srcset lista a mesma imagem em vários tamanhos:
<img src="photo-400.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w">
Separar por vírgulas quebra com URLs de CDN que contêm vírgulas (/image/upload/w_800,q_80/photo.jpg). A regra da especificação HTML é mais simples e segura: a URL vai até o primeiro espaço em branco, e o descritor vai até a próxima vírgula. A ferramenta faz o parsing desse jeito e fica com o candidato de maior w, ou de maior densidade x quando não há larguras:
def best_srcset_url(value):
best, best_key = None, None
for url, desc in parse_srcset(value):
key = (0, 1.0) # no descriptor means 1x
m = re.match(r"^(\d+)w$", desc)
if m:
key = (1, float(m.group(1))) # any width beats any density
else:
m = re.match(r"^(\d+(?:\.\d+)?)x$", desc)
if m:
key = (0, float(m.group(1)))
if best_key is None or key > best_key:
best, best_key = url, key
return best
As miniaturas da Wikipédia trazem uma versão 2x no srcset, então, para as fotos do artigo, isso significa um arquivo de 500 px em vez do de 250 px que está no src.
3. Confira os bytes, não a URL
Uma URL terminada em .jpg pode devolver uma página 404, uma tela de login ou um WebP. Por isso a ferramenta olha os primeiros bytes de cada download: \x89PNG, \xff\xd8\xff (JPEG), GIF8, RIFF....WEBP, ftypavif, BM, o cabeçalho ICO ou <svg. Qualquer outra coisa é registrada como not_an_image e não é salva, e a extensão do arquivo vem dos bytes, não da URL.
O mesmo cabeçalho informa o tamanho sem decodificar nada. Num PNG, a largura e a altura são dois inteiros big-endian logo depois de IHDR:
if data.startswith(b"\x89PNG\r\n\x1a\n") and data[12:16] == b"IHDR":
width, height = struct.unpack(">II", data[16:24])
No JPEG é preciso um pequeno loop pelos segmentos até o marcador de início de frame, e o WebP tem três variantes (lossy, lossless e estendida) com layouts diferentes; a ferramenta trata PNG, JPEG, GIF, WebP, AVIF, BMP e SVG. Sabendo a largura, --min-width 100 descarta ícones e espaçadores, e --min-kb 2 descarta pixels de rastreamento.
Uma armadilha: um SVG com width="34ex" não tem tamanho em pixels. Uma versão inicial da minha ferramenta recorria ao viewBox e informou que uma fórmula matemática tinha 5,664 px de largura. Agora, qualquer unidade que não seja pixel deixa largura e altura vazias, e o --min-width mantém esses arquivos em vez de chutar.
4. Pule duplicatas pelo hash
O mesmo logo costuma aparecer no cabeçalho e no rodapé com duas URLs diferentes. Cada arquivo recebe um hash SHA-256; um hash que já foi salvo na execução é registrado como duplicate, apontando para a primeira cópia. O nome do arquivo inclui os 10 primeiros caracteres hexadecimais do hash (logo-3f9a0c12be.png), então dois arquivos diferentes chamados logo.png nunca se sobrescrevem.
5. Seja educado
- O
urllib.robotparserverifica o robots.txt para cada página e cada imagem antes da requisição, com umUser-Agentque identifica a ferramenta e aponta para o repositório. Um robots.txt ausente (4xx) libera tudo; um que dá erro (5xx) bloqueia o host, como exige a RFC 9309. - Por padrão, as requisições ao mesmo host têm 0.5 s de intervalo.
- HTTP 429 e 5xx são repetidos até três vezes, respeitando o
Retry-After; um 404 não é repetido.
Uma execução real
Em 3 de novembro de 2026, rodei a ferramenta na página inicial do python.org, no artigo "Lighthouse" da Wikipédia em inglês e numa URL direta de SVG em upload.wikimedia.org:
python3 page_image_downloader.py -i urls.txt -o images --min-kb 2 --min-width 100
| Entrada | Candidatos | Salvos | Ignorados |
|---|---|---|---|
| Página inicial do python.org | 2 | 2 | 0 |
| Wikipédia "Lighthouse" | 41 | 27 | 7 com menos de 100 px de largura, 4 com menos de 2 KB, 3 bloqueados pelo robots.txt |
| SVG direto | 1 | 1 | 0 |
No total foram 30 arquivos e 1,799,209 bytes em 19.5 segundos, sem nenhum erro.
Algumas coisas chamaram a atenção:
-
O python.org tem duas imagens no HTML: o
<img>do logo e oog:image. Conferi o HTML bruto com grep, e ele tem exatamente uma tag<img>. Uma página pode parecer cheia de imagens enquanto as figuras vêm de CSS ou de scripts, que esta ferramenta (de propósito) não executa. -
O srcset fez a maior parte do trabalho. 22 dos 30 arquivos salvos vieram do
srcset. Na Wikipédia, 40 tags<img>, 2 tags<source>e oog:imagesomaram 43 referências, que se reduziram a 41 URLs únicas. -
A imagem de compartilhamento era o maior arquivo. O
og:imageda Wikipédia era uma foto aérea de 960 × 1415 (326,760 bytes); o corpo do artigo só aponta para uma versão de 500 px dela. Um scraper que olha apenas<img>nunca veria a grande. -
O robots.txt bloqueou três requisições, todas em
en.wikipedia.org, que proíbe/w/e/wiki/Special:: dois selos do rodapé do MediaWiki e uma imagem deSpecial:CentralAutoLogin. A ferramenta nunca chegou a requisitá-las. - O filtro de largura pesou mais que o de tamanho: 7 arquivos tinham menos de 100 px de largura, e 4 tinham menos de 2 KB.
Para validar o manifesto, abri os 25 arquivos raster com o Pillow: todas as larguras e alturas bateram com o que a ferramenta leu dos cabeçalhos, e todos os SHA-256 bateram com os arquivos em disco.
Uma segunda execução com --links pdf na página de informações da RFC 9309 no rfc-editor.org salvou 4 arquivos: o og:image da página, dois SVGs de logo e a RFC como um PDF de 177,688 bytes. Antes de salvar, a ferramenta confere se um "PDF" não é na verdade uma página HTML (uma tela de login, por exemplo).
O que ela não faz
-
Não executa JavaScript. Imagens adicionadas por script depois do carregamento não são encontradas. Páginas renderizadas no servidor, atributos de lazy loading e
srcsetfuncionam bem; para single-page apps, você precisa de um navegador headless. -
Não pega imagens de fundo do CSS nem
<video poster>. - Uma requisição por vez, o que é suficiente para algumas dezenas de páginas.
Baixe apenas imagens que você tem direito de usar, e mantenha o intervalo entre requisições ligado.
Experimente
git clone https://github.com/fecklesspleb/page-image-downloader
cd page-image-downloader
python3 page_image_downloader.py https://en.wikipedia.org/wiki/Lighthouse --min-width 100
Você recebe images/en.wikipedia.org/ com os arquivos e um images/manifest.csv que lista cada candidato com status, tamanho, largura, altura, texto alternativo e hash. Os 37 testes rodam offline contra um servidor de teste local: python3 -m unittest discover -s tests -v.
O código, os testes e a lista completa de status estão no repositório: fecklesspleb/page-image-downloader.
Sobre o uso de IA: este artigo foi traduzido do inglês com ajuda de IA; o artigo original e o código também foram escritos com ajuda de IA. Os números vêm de execuções reais em 3 de novembro de 2026. O código, os comandos e os números foram conferidos com o original e com o repositório antes da publicação. Sem afiliação com a Python Software Foundation, a Wikipédia, a Wikimedia Foundation ou o RFC Editor.
Top comments (0)