Captura integral do acervo da Brasiliana Fotográfica (Biblioteca Nacional) para s3://brasiliana: 13.060 itens, 12.985 com imagem de preservação, 13.059 com miniatura derivada, 2.137 cópias legadas e 636 posts do blog. 41.444 objetos no ledger. Os 75 itens sem preservação estão em errors.md: a página do item responde 200, mas a origem serve o bitstream com HTTP 500. Nenhum fica fora do mural, porque todos têm thumbnail. Não versionado, por viver no bucket ou ser reconstruível: catálogo, posts .jsonl, atlas .webp, sqlite de estado e logs. Credenciais ficam de fora pelo .gitignore.
20 KiB
PLANO — Brasiliana Fotográfica → S3
Comece por aqui. Documento de transferência: uma sessão nova consegue retomar o trabalho só com este arquivo. Detalhe técnico completo em
README.md.
Objetivo: os 13.036 itens da Brasiliana Fotográfica — metadados e imagens
em qualidade de preservação — depositados em s3://brasiliana, organizados
como acervo arquivístico (procedência + checksums).
Autorização: a Biblioteca Nacional foi consultada. Confirmou que o material é público e que não tem equipe técnica para fornecer um dump; a coleta por scraping é o caminho combinado. Ritmo cortês e identificação continuam valendo.
Reorganização de pastas em 2026-09-10. Scripts + estado (catálogo,
.sqlite,.log,.tmp/, credenciais) mudaram de raiz parapipeline/; visualizações foram paraweb/;exemplo-item/virouexamples/exemplo-item/;censo.pyvirouarchive/censo.py. Todo comando de terminal abaixo já foi atualizado para o novo caminho. Detalhe completo emREADME.md→ "Estrutura do repositório".Mudança de plano em 2026-09-10 (importante, ver 2.6): o achado 2.4 abaixo não se sustentou. Cloudflare passou a bloquear bitstream via curl também pra itens sem cache de borda — não só HTML. Tudo (Fases 2 e 3) roda pelo navegador agora, trilha única, não mais A/B separadas por tipo de nome. A Fase 4 (legado) segue via curl normalmente — domínio diferente (
objdigital.bn.br), sem esse bloqueio.
1. Estado em 2026-09-10
| Fase 1 (metadados) | ✅ completa — 13.059 registros, 0 erros. 1 falso-positivo conhecido (.../1, é a página raiz da comunidade, não um item — excluir na contagem final) |
| Catálogo | pipeline/catalogo.jsonl, re-exportado depois dos reparos (152 itens com all_preservation_urls) |
| Download principal (1 imagem/item) | 🟡 em andamento no navegador |
| Thumbnails | 🟡 em andamento no navegador |
| Extras (itens com múltiplos bitstreams, 115 itens) | ✅ completo |
| Fase 4 (legado objdigital) | 🟡 em andamento, local (baixador.py --somente-legado), 2.707 itens com legacy_url |
subidor.py |
rodando em pipeline/, --vigiar, roteia preservacao/thumbnail/legado certo |
Não existe mais aba do Brave rodando isso — é uma aba de Chrome comum,
conectada via extensão Claude-in-Chrome. coletor.js/coletor-resume.js
grava em IndexedDB a cada item; fechar a aba não perde nada, mas o loop
para e precisa recolar o script certo pra retomar (ver tasks/TODO.md
pros comandos exatos de cada trilha ativa).
2. Cinco achados que não devem ser redescobertos
Cada um destes custou horas. Nenhum é óbvio, e vários são contraintuitivos.
2.1 O sitemap está sete anos desatualizado
htmlmap/sitemap são de abril/2019 e listam ~5.300 dos 13.036 itens. Enumerar
por eles perde 59% do acervo sem erro aparente — o crawl termina limpo, pela
metade. Fonte correta: browse?type=title&rpp=100&offset=N, percorrido por
comunidade (o que de quebra etiqueta a instituição de origem).
2.2 O arquivo de preservação está no DSpace, não no objdigital
O DC.identifier aponta para objdigital.bn.br e parece ser o master. É o
contrário: o bitstream do DSpace tem 5533×4065 (7,4 MB) contra ~2600×2100
(147 KB) do objdigital. O maior medido: 8679×6484, 34,7 MB — EXIF mostra
Hasselblad H3D II-39MS a 7216×5412.
Inverter isso arquiva o acervo a um quarto da resolução, e nada avisa.
O PDF de 71,9 MB do item 14582 no objdigital não é a mesma imagem melhor: é outro objeto, um álbum de várias páginas.
2.3 O Cloudflare bloqueia automação de navegador — não IP, não geografia
Medido na mesma máquina, mesmo IP, mesmo instante:
| Cliente | Páginas HTML |
|---|---|
| Chrome/Brave do usuário | ✅ 200 |
fetch() de dentro da página |
✅ 200 (>100 seguidas) |
| Playwright headed | ❌ 200 na 1ª, 403 depois |
| Playwright headless | ❌ 403 |
| curl / httpx | ❌ 403 |
| Servidor (Hetzner Helsinki) | ❌ 403 |
Proxies não ajudam — o bloqueio dispara na segunda requisição da mesma sessão, não por origem. Testado e descartado. Não repita.
2.4 Bitstreams passam por curl; páginas HTML não
bitstream/... responde 200 a curl -L simples, até com UA de curl. Só o HTML
é desafiado. É isso que permite baixar os 116 GB fora do navegador, num servidor.
2.5 Nome de arquivo com percent-encoding leva 403
Testado com cinco user-agents (Chrome, Safari, Firefox, Googlebot, curl):
todos 403 numa URL com %28/%20, enquanto curl/8.7.1 puro dá 200
numa URL de nome simples. O gatilho é o caminho, não o cliente — variar
user-agent não resolve. O navegador busca os dois casos sem problema.
Daí as duas trilhas — modelo original, superado, ver 2.6. Itens da BN
usam imagem.jpg (trilha A); os do IMS usam nomes descritivos com espaços
e acentos (trilha B).
2.6 O achado 2.4 não se sustentou — Cloudflare bloqueia bitstream também
Testado ao vivo em 2026-09-10: item nunca antes acessado, buscado pelo
navegador (200 OK) e imediatamente depois via curl na Finlândia — 403,
página real do Cloudflare ("Attention Required"). O único item que passava
por curl (6066) só passava porque já tinha sido baixado com sucesso antes
e ficou em cache de borda (cf-cache-status: HIT) — sobra de um caso, não
regra geral. Pré-aquecer a URL pelo navegador antes de tentar via curl
não transfere o cache pra tornar o curl aceito (testado, falha igual).
Consequência: não existe mais trilha A via servidor. Tudo — nome simples
ou com encoding — precisa passar pelo navegador vivo (fetch() de dentro da
página). baixador.py na Finlândia fica só pra Fase 4 (objdigital, domínio
diferente, sem esse bloqueio — ver 2.11).
2.7 Itens com múltiplos bitstreams reais — não é caso raro
115 itens (de ~13.058) têm 2 a 6 imagens reais no bundle ORIGINAL, não
uma. citation_pdf_url não é gerado pra esses (o gerador de citação do
DSpace não sabe qual é "a" imagem quando há mais de uma) — por isso ficam
sem preservation_url na extração simples. Sintoma: full_table tem
dc.description mencionando álbum/página (ex.: "Álbum 0259"), igual ao
caso do PDF de 71,9 MB do achado 2.2 — mesmo fenômeno, mas dentro do
próprio DSpace, não só no objdigital.
Como enumerar as imagens reais de um item, em ordem de confiabilidade:
- Contar
.file-wrapperna página normal do item (não?show=full) — bate com o número real de bitstreams. - Tentar sequências
/bitstream/{PREFIX}/{h}/{seq}/qualquer-nome.jpgpraseqde 1 até N — funciona quando as sequências são contíguas a partir de 1 (maioria dos casos). - Quando (2) falha (sequências não contíguas): pegar o nome real do
arquivo a partir do
<img src>de cada.file-wrapper(é a miniatura, nome com.jpgduplicado no final — ex.nome.jpg.jpg), tirar o último.jpg, e buscar/bitstream/handle/{PREFIX}/{h}/{nome-real}(rota por nome exato, não por sequência).
preservation_url = a maior imagem encontrada (compatibilidade com o
pipeline); all_preservation_urls/all_preservation_sizes guardam todas,
ordenadas da maior pra menor.
Exceção conhecida sem conserto: handle 20.500.12156.1/7523 — HTTP 500 do próprio servidor da BN ao buscar os bitstreams (não é problema de
nome/padrão). Provável arquivo corrompido do lado deles.
2.8b Achado 2.8 falha ~62% das vezes em alguns lotes — bitstream de LICENÇA, não thumbnail
seq_master+1 às vezes é o bitstream de LICENÇA do DSpace (texto puro,
"LICENÇA DE VEICULAÇÃO E DISTRIBUIÇÃO..."), não a miniatura — visto em
Marinha e IMS, 2.655 de 4.278 thumbnails (62%!) baixados errado num
lote, 146 já tinham subido pro bucket antes de descobrir (limpo).
O filtro de tamanho do v1 (> 2MB → rejeita) não pega isso: o texto da
licença é pequeno o bastante (781 bytes) pra passar. Fix: validar
magic bytes (JPEG FFD8/PNG) antes de aceitar, tentar seq+2/+3 se falhar,
e cair pro método do achado 2.7 (<img src> da página) como último
recurso — baixador-thumb.js v2. Também blindado no subidor.py: rejeita
qualquer upload cujo conteúdo não comece com magic bytes de imagem,
independente do nome do arquivo (defesa em camadas).
Lição: tamanho pequeno não é prova de "é a miniatura" — só magic bytes provam que é imagem de verdade.
2.8 Miniatura gerada pelo DSpace: sequência N+1 (quando contígua)
Pra item com bitstream original na sequência N, a miniatura que o DSpace
gera automaticamente costuma ficar na sequência N+1 — confirmado no handle
6066: seq 1 = master (7,7 MB), seq 2 = thumb (56 KB). Mesma rota
filename-agnostic do achado 2.4 original (o nome na URL não importa, só a
sequência). Quando as sequências não são contíguas (itens do achado 2.7 via
método 3), a miniatura de cada imagem é o próprio <img src> do
.file-wrapper correspondente — não precisa de request extra.
2.9 objdigital às vezes tem PDF (álbum), não só JPG — extração pegava o identificador errado
Alguns itens têm mais de um DC.identifier apontando pro objdigital: um
.htm (página de landing) e um .pdf (o álbum de verdade, múltiplas
páginas — mesmo fenômeno do PDF de 71,9 MB do item 14582, achado 2.2). A
extração original pegava o primeiro identificador que batesse com
"objdigital", que pra 44 itens (a maioria uma sequência do Museu
Aeroespacial, 14526-14570, mais o 584) era o .htm, não o .pdf —
perdendo o conteúdo real. De 235 itens com PDF disponível, 191 já estavam
certos por acaso (o PDF vinha primeiro na lista); só 44 precisaram de
correção manual (feita em catalogo.jsonl e no IndexedDB, antes do
baixador.py --somente-legado alcançar esses handles — nada foi
contaminado no bucket). Vale re-checar outros itens com múltiplos
DC.identifier de objdigital por precaução, não só os que têm .pdf.
Recheque feito (2026-09-10): mapeamos todos os padrões de extensão em 2.231 itens com 2+ identifiers objdigital. Achados adicionais:
- 5 itens (
1081, 1082, 1084, 4077, 4084) têm só HTML, sem.jpg/.pdfnenhum — são páginas de um visualizador de zoom profundo (OpenSeadragon/DZI, pirâmide de blocos, não um arquivo único). 3 já tinham sido baixados e subidos como se fossem imagem (arquivos de ~2,4 KB, só a página HTML do visualizador) — limpo do bucket,legacy_urlzerado pros 5 (não vale a complexidade de espelhar a pirâmide de tiles pra tão poucos itens). - 11 itens têm
.sid(MrSID, formato comprimido de imagem grande) além do.jpg— não capturado, ficou como gap conhecido (baixo volume, não priorizado).
2.10 O crawl da Fase 1 não é 100% completo — achado via referência cruzada com o WordPress
Cruzando os handles citados nos 636 posts do WordPress contra o catálogo,
2 itens reais (1799 "Princesa Isabel menina", 4798 "Vista panorâmica de
Canudos...") existem no site mas não foram enumerados pela Fase 1 — ambos
sem citation_pdf_url, formato de metadado antigo (um é de 2015-04-16,
uma das primeiras entradas do sistema). Adicionados manualmente ao
catálogo depois de descobertos.
Isso só foi achado por acaso (porque um post do blog citava esses
handles especificamente) — não existe verificação exaustiva de que a
enumeração por browse?type=title capturou 100% dos itens de cada
comunidade. Pode haver mais itens faltando que nenhum post referencia.
Não investigado a fundo: seria preciso comparar a contagem de cada
comunidade contra alguma fonte independente (a BN não expõe OAI-PMH nem
count via API, até onde sabemos).
2.11 O objdigital só responde em HTTPS — não era bloqueio
legacy_url na fonte vem como http://objdigital.bn.br/.... Conexão HTTP
simples (porta 80) é resetada (Recv failure: Connection reset by peer),
local e da Finlândia. HTTPS funciona liso com curl puro, local (não
testado da Finlândia — motivo original de "não alcançável" pode ser outro,
não investigado). Fix: forçar https:// no legacy_url antes de baixar
(já aplicado em baixador.py).
2.12 Caractere # no nome do arquivo corrompe URL no IndexedDB
78 itens (mesmo álbum "Album 0035/0259/0260") têm # literal no nome real
do arquivo (ex.: Alb 0260 199 CT #.jpg). Algum processo que passou esse
nome por new URL(...).pathname sofreu o efeito clássico: # não
percentado é tratado como início de fragmento da URL, e tudo depois
dele é descartado — a URL salva no IndexedDB virou .../Alb%20...%20CT%20
(sem sequência, sem #, sem .jpg), 404 garantido. O catalogo.jsonl
local nunca foi tocado por esse bug (tem a URL certa, com %23) — fix é
só reescrever o campo no IndexedDB a partir do catálogo
(pipeline/reparo-hash-url.js).
Lição: nunca usar new URL()/.pathname pra manipular nomes de
arquivo vindos de fonte externa sem antes confirmar que não têm #, ?
ou outros caracteres especiais de URL não codificados.
2.13 O mesmo # quebra de novo — desta vez no redirect do próprio DSpace
Mesmo com a URL do achado 2.12 corrigida no IndexedDB (%23 correto, forma
antiga /bitstream/PREFIX/ID/SEQ/arquivo), os 78 itens continuavam dando
404. O DSpace responde a essa forma antiga com um redirect para a forma
canônica nova (/bitstream/handle/PREFIX/ID/arquivo, sem número de
sequência) — e o Location do redirect tem o # sem escapar de novo,
então o fetch() do navegador trunca no fragmento ao seguir o redirect
(finalUrl chega faltando #.jpg, 404). Fix: pedir direto a forma
canônica (/bitstream/handle/...), pulando o redirect — testado, 200,
JPEG válido. preservation_url desses 78 itens reescrito no IndexedDB
para a forma canônica.
Lição: com # no nome do arquivo, não confiar em nenhum redirect do
servidor — ele pode ter o mesmo bug do lado de lá. Preferir sempre a URL
final/canônica quando ela for conhecida.
2.14 Dá pra ler o progresso do BAIXA/BAIXA_THUMB sem o Chrome conectado
O progresso das trilhas via navegador (localStorage.bras_baixados,
bras_thumbs_v2) sobrevive num arquivo leveldb comum no disco —
~/Library/Application Support/Google/Chrome/Default/Local Storage/leveldb
— mesmo com a extensão Claude-in-Chrome desconectada (ex.: conflito de conta
OAuth, sessão anterior perdida). Basta copiar o diretório (evita brigar com
o LOCK que o Chrome mantém aberto) e ler com uma lib leveldb qualquer
(classic-level via npm, sem dependência nativa externa). O valor vem com
1 byte de prefixo indicando encoding (0x00=UTF-16LE, 0x01=UTF-8/Latin1)
antes do JSON de verdade. Script pronto: pipeline/tools/ler-localstorage.js.
Lição: perder a conexão com a extensão do navegador não significa perder visibilidade do progresso — boa parte do estado do app mora em arquivos comuns no disco do usuário, não só "dentro do navegador".
3. O acervo
DSpace 4.2, handles 20.500.12156.1/*. 13.058 itens reais (13.059
descobertos no crawl, menos 1 falso-positivo — .../1 é a página raiz da
comunidade, não um item). Contagem original por comunidade abaixo era de
13.036; a diferença é ruído de borda (comunidades ganham/perdem item ao
longo do tempo), não erro de coleta.
| Instituição | Itens | Instituição | Itens | |
|---|---|---|---|---|
| Instituto Moreira Salles | 5.133 | Fiocruz | 548 | |
| Biblioteca Nacional | 2.726 | Marinha (DPHDM) | 456 | |
| Museu Aeroespacial | 1.143 | Arquivo Nacional | 383 | |
| Museu da República | 906 | Arq. Geral da Cidade do RJ | 248 | |
| Museu Histórico Nacional | 649 | Leibniz-Institut, Leipzig | 216 | |
| IHGB | 206 | Fund. Joaquim Nabuco | 190 | |
| Museu Goeldi | 95 | FGV CPDOC | 79 | |
| IBGE | 58 |
~139,5 GB projetados (recalculado em 2026-09-10 com médias reais de 357-474 arquivos já no bucket por categoria, não mais os 6 originais): preservação+extras ~136,9 GB (13.152 arquivos, 10,7 MB médio), thumbnails ~0,9 GB, legado ~1,5 GB (taxa de sucesso ~75%, muito 404 genuíno), metadados+WordPress+catálogo <0,2 GB combinados.
4. Runbook
Fase 1 — metadados (navegador) ✅ COMPLETA
Cole pipeline/coletor-resume.js (pendentes + erros juntos, numa passada só)
numa aba em https://brasilianafotografica.bn.gov.br/brasiliana/. Acompanhar
com await BRAS.status(). Deixe a aba ativa na janela dela: o Chromium
estrangula setTimeout em abas de fundo até 1×/min, e o loop passa a
rastejar sem erro nenhum. Ao terminar: await BRAS.exportar().
Fases 2 e 3 — trilha única via navegador (não tem mais trilha A/servidor)
Ver achado 2.6. Na mesma aba (mesmo IndexedDB bras), cole em sequência:
// pipeline/baixador-navegador.js -- imagem principal, todos os itens
BAIXA.quantos(); BAIXA.iniciar();
// pipeline/baixador-thumb.js -- miniaturas
BAIXA_THUMB.quantos(); BAIXA_THUMB.iniciar();
// pipeline/baixador-extras.js -- 2a+ imagem dos itens com multiplos bitstreams
// (rodar reparo-multibitstream.js antes, se ainda nao rodou)
BAIXA_EXTRA.quantos(); BAIXA_EXTRA.iniciar();
Em paralelo, em outro terminal:
cd pipeline
set -a; . .s3env; set +a
python3 subidor.py --vigiar --catalogo catalogo.jsonl
Downloads automáticos do Chrome exigem permissão em
chrome://settings/content/automaticDownloads (adicionar
https://brasilianafotografica.bn.gov.br) — sem isso o download "funciona"
no JS (sem erro) mas nenhum arquivo chega no disco. Vale pro domínio
inteiro, uma vez só.
Fase 4 — cópias legadas (máquina local)
cd pipeline
set -a; . .s3env; set +a
python3 baixador.py --catalogo catalogo.jsonl --somente-legado
Via curl direto, sem navegador (ver achado 2.11). ~2.707 itens têm
legacy_url, ~328 KB em média.
5. Arquivos e credenciais
Layout do repo (detalhe em README.md → "Estrutura do repositório"):
scripts + estado ao vivo em pipeline/, visualizações em web/, item de
referência em examples/, código morto em archive/.
| Arquivo | O quê |
|---|---|
tasks/PLANO.md |
Este documento |
README.md |
Referência técnica completa (raiz do projeto) |
pipeline/coletor.js |
Fase 1 — console do navegador, IndexedDB, autoinício |
pipeline/baixador.py |
Fases 2 e 4 — curl → S3, um arquivo por vez |
pipeline/baixador-navegador.js |
Fase 3 — navegador → pasta Downloads |
pipeline/subidor.py |
Fase 3 — Downloads → S3, apaga na hora |
archive/censo.py |
Versão Playwright — não funciona, mantida pelo parsing |
pipeline/.s3env.exemplo |
Modelo; chaves reais em /Users/polux/Projetos/tocador/.env |
S3: hel1.your-objectstorage.com, bucket brasiliana (Hetzner).
Credenciais em /Users/polux/Projetos/tocador/.env
(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). Cópia local em
pipeline/.s3env. Na Finlândia: /root/.s3env.
Servidor: ssh finland → 89.167.95.136 (Hetzner Helsinki, root, 31 GB livres).
6. Estrutura no bucket
s3://brasiliana/
MANIFEST.md o que é, como foi coletado, quando
catalogo.jsonl uma linha por item
itens/20.500.12156.1_6066/
metadata.json Dublin Core + URL de origem + data UTC
preservacao/imagem.jpg 5533×4065, 7,4 MB
legado/icon843660.jpg 147 KB, referência de origem
manifest-sha256.txt checksum de cada arquivo
Não negociável: SHA-256 de tudo e URL de origem + data/hora de captura. Sem os dois é pasta de imagens, não arquivo.
7. Pendências
- Terminar a Fase 1 e exportar o catálogo.
- Medir a fração da trilha B (
BAIXA.quantos()) — de graça, sem requisição. - Confirmar a projeção de ~116 GB (hoje: 6 arquivos medidos).
- Decidir sobre os PDFs de álbum do objdigital — objetos distintos, o de 71,9 MB provavelmente não é caso isolado.
- Gerar
MANIFEST.mdecatalogo.jsonlno bucket ao final.
8. Direitos
Majoritariamente domínio público por antiguidade, mas as 15 instituições têm
políticas distintas. dc.rights e a instituição de origem são preservados
por item: a decisão de redistribuir é item a item, não do acervo em bloco.
Arquivar para si é uma coisa; republicar é outra.