- pipeline/nobrade: normalizadores de datas (ISO 8601-2), séries, autoridades, espécie/suporte, direitos, lugar e assuntos; montagem dos níveis 0–5 com código de referência BR RJXYZ.BF.<seção>.<série>.<handle>.<imagem> - validador dos elementos obrigatórios: 0 pendências em 26.454 unidades - EAD 3 e 457 EAC-CPF validados contra os XSD - mural: cartão com código, data normalizada, série e reprodução; filtro por série
178 lines
8.5 KiB
Python
178 lines
8.5 KiB
Python
"""Normalizadores de campo único: espécie/suporte (1.5), características
|
|
físicas (1.5/4.4), direitos (4.2), idioma (4.3), lugar (1.3.1 data tópica) e
|
|
assuntos (8.1)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
|
|
CORROMPIDO = re.compile(r'[<>{}]|ns:|PALAVRAS-CHAVE|^[0-9a-f]{6,}|\d[a-f]\d[a-f]\d')
|
|
|
|
|
|
def sem_acento(s: str) -> str:
|
|
return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
|
|
|
|
|
def corrompido(v: str | None) -> bool:
|
|
return bool(v) and bool(CORROMPIDO.search(v))
|
|
|
|
|
|
# ── 1.5 espécie e suporte (a partir de dc.type) ─────────────────────────────
|
|
ESPECIES = [ # (padrão, gênero, espécie)
|
|
(r'^negativo', 'iconográfico', 'negativo fotográfico'),
|
|
(r'^diapositivo', 'iconográfico', 'diapositivo'),
|
|
(r'reprodu[cç][aã]o fotomec', 'iconográfico', 'reprodução fotomecânica'),
|
|
(r'^(fotografia|vista panor)', 'iconográfico', 'fotografia'),
|
|
(r'^gravura', 'iconográfico', 'gravura'),
|
|
(r'^desenho', 'iconográfico', 'desenho'),
|
|
(r'^[áa]lbum', 'iconográfico', 'álbum fotográfico'),
|
|
(r'^manuscrito', 'textual', 'manuscrito'),
|
|
(r'^livro', 'bibliográfico', 'livro'),
|
|
(r'^artigo de peri', 'bibliográfico', 'periódico'),
|
|
]
|
|
|
|
|
|
def especie(tipo: str | None) -> dict:
|
|
"""{'genero', 'especie', 'suporte', 'presumida'}. Sem dc.type (ou com lixo),
|
|
presume fotografia — o acervo inteiro é um repositório fotográfico — e marca."""
|
|
t = (tipo or '').strip()
|
|
if t and not corrompido(t):
|
|
base, _, sup = t.partition('/')
|
|
b = sem_acento(base.strip().lower())
|
|
for pat, gen, esp in ESPECIES:
|
|
if re.search(pat, b) or re.search(pat, base.strip().lower()):
|
|
suporte = sup.strip().lower() or None
|
|
if esp == 'negativo fotográfico' and 'flex' in b:
|
|
suporte = 'flexível'
|
|
return {'genero': gen, 'especie': esp, 'suporte': suporte, 'presumida': False}
|
|
return {'genero': 'iconográfico', 'especie': 'fotografia', 'suporte': None, 'presumida': True,
|
|
'original': t or None}
|
|
|
|
|
|
# ── características físicas que algumas instituições põem em dc.description ──
|
|
FISICO = re.compile(
|
|
r'(?i)^\s*(p\s*&\s*b|p/b|preto e branco|cor(ida)?|colorid[ao]|s[ée]pia|'
|
|
r'albumina|gelatina|prata|col[oó]dio|cianotipia|platina|carv[aã]o|ambr[oó]tipo|daguerre[oó]tipo|'
|
|
r'negativo|acetato|nitrato|vidro|papel|cart[aã]o|\d+[,.]?\d*\s*x\s*\d+|processo:|segunda gera)')
|
|
|
|
|
|
def e_fisico(v: str) -> bool:
|
|
return bool(FISICO.match(v)) and len(v) < 120
|
|
|
|
|
|
def cor(fisico: list[str]) -> str | None:
|
|
t = ' '.join(fisico).lower()
|
|
if re.search(r'p\s*&\s*b|p/b|preto e branco', t):
|
|
return 'p&b'
|
|
if re.search(r'colorid|\bcor\b', t):
|
|
return 'cor'
|
|
return None
|
|
|
|
|
|
# ── 4.2 condições de reprodução ─────────────────────────────────────────────
|
|
def direitos(textos: list[str], instituicao: str) -> dict:
|
|
"""{'categoria', 'detentor', 'texto', 'originais'}."""
|
|
limpos = list(dict.fromkeys(t.strip() for t in textos if t and t.strip() and t.strip() != 'None'))
|
|
junto = sem_acento(' '.join(limpos).lower())
|
|
if re.search(r'dominio publico|omínio público|omi?nio publico', junto):
|
|
cat = 'domínio público'
|
|
elif 'liberado para uso de natureza cultural' in junto:
|
|
cat = 'uso cultural liberado'
|
|
elif re.search(r'solicitar|requer libera', junto):
|
|
cat = 'requer autorização do detentor'
|
|
else:
|
|
cat = 'consultar o detentor'
|
|
detentor = None
|
|
for t in limpos:
|
|
a = sem_acento(t.lower())
|
|
if corrompido(t) or re.search(r'dominio publico|solicitar|requer|liberado', a) or len(t) < 6:
|
|
continue
|
|
if 'bibli' in a and 'nac' in a:
|
|
detentor = 'Biblioteca Nacional (Brasil)'
|
|
elif 'moreira' in a and 'salles' in a and '/' not in t:
|
|
detentor = 'Instituto Moreira Salles'
|
|
else:
|
|
detentor = t
|
|
break
|
|
detentor = detentor or instituicao
|
|
frases = {
|
|
'domínio público': 'Imagem em domínio público.',
|
|
'uso cultural liberado': 'Uso liberado para fins de natureza cultural.',
|
|
'requer autorização do detentor': 'Reprodução sujeita a autorização do detentor dos direitos.',
|
|
'consultar o detentor': 'Condições de reprodução não informadas na origem; consultar o detentor.',
|
|
}
|
|
return {'categoria': cat, 'detentor': detentor,
|
|
'texto': f'{frases[cat]} Detentor indicado: {detentor}.',
|
|
'originais': limpos or None}
|
|
|
|
|
|
# ── 4.3 idioma ───────────────────────────────────────────────────────────────
|
|
NOMES_IDIOMA = {'por': 'português', 'fre': 'francês', 'eng': 'inglês', 'lat': 'latim', 'ger': 'alemão',
|
|
'ita': 'italiano', 'ukr': 'ucraniano', 'hun': 'húngaro', 'spa': 'espanhol'}
|
|
|
|
|
|
def idiomas(vs: list[str]) -> list[dict]:
|
|
out = []
|
|
for v in vs:
|
|
c = (v or '').strip().lower()
|
|
if c in NOMES_IDIOMA and c not in [o['codigo'] for o in out]:
|
|
out.append({'codigo': c, 'nome': NOMES_IDIOMA[c]})
|
|
return out
|
|
|
|
|
|
# ── 1.3.1 data tópica e 8.1 lugares ─────────────────────────────────────────
|
|
UF = {'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE',
|
|
'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO'}
|
|
ESTADOS = {'rio de janeiro': 'RJ', 'sao paulo': 'SP', 'bahia': 'BA', 'pernambuco': 'PE', 'minas gerais': 'MG',
|
|
'para': 'PA', 'amazonas': 'AM', 'parana': 'PR', 'rio grande do sul': 'RS', 'santa catarina': 'SC',
|
|
'ceara': 'CE', 'maranhao': 'MA', 'espirito santo': 'ES', 'goias': 'GO', 'mato grosso': 'MT',
|
|
'paraiba': 'PB', 'alagoas': 'AL', 'sergipe': 'SE', 'piaui': 'PI', 'rio grande do norte': 'RN'}
|
|
PAISES = {'brasil', 'brazil'}
|
|
|
|
|
|
def lugar(coberturas: list[str]) -> str | None:
|
|
"""A forma mais específica de cidade: 'Cidade (UF)'. Junta 'Rio de Janeiro' + 'RJ'."""
|
|
cs = [c.strip() for c in coberturas if c and c.strip() and not corrompido(c)]
|
|
for c in cs:
|
|
if m := re.fullmatch(r'(.+?)\s*(?:\(|,\s*|\s-\s*)([A-Z]{2})\)?', c):
|
|
if m[2] in UF and sem_acento(m[1].lower()) not in PAISES:
|
|
return f'{m[1].strip()} ({m[2]})'
|
|
ufs = [c for c in cs if c in UF]
|
|
for c in cs:
|
|
k = sem_acento(c.lower())
|
|
if k in PAISES or c in UF or '(estado)' in k or 'state' in k:
|
|
continue
|
|
if re.search(r'\(([^)]+)\)$', c): # "Paris (França)", "Rio de Janeiro (Brazil)"
|
|
nome, pais = re.match(r'(.+?)\s*\(([^)]+)\)$', c).groups()
|
|
if sem_acento(pais.lower()) in PAISES:
|
|
uf = ESTADOS.get(sem_acento(nome.lower()))
|
|
return f'{nome} ({uf})' if uf else nome
|
|
return c
|
|
uf = ufs[0] if ufs else ESTADOS.get(k)
|
|
return f'{c} ({uf})' if uf else c
|
|
return None
|
|
|
|
|
|
# ── 8.1 assuntos: separa as traduções inglesas que o DSpace da BN repete ─────
|
|
EN = {'the', 'and', 'of', 'in', 'on', 'women', 'men', 'children', 'streets', 'street', 'buildings', 'building',
|
|
'churches', 'church', 'views', 'view', 'slaves', 'railroads', 'railroad', 'harbors', 'harbor', 'landscapes',
|
|
'portraits', 'bridges', 'rivers', 'houses', 'squares', 'parks', 'gardens', 'mountains', 'beaches',
|
|
'ships', 'soldiers', 'history', 'photographs', 'people', 'indians', 'coffee', 'workers', 'palaces',
|
|
'monuments', 'waterfalls', 'boats', 'plantations', 'aerial', 'military', 'city', 'cities', 'public',
|
|
'social', 'life', 'customs', 'description', 'travel', 'transportation', 'agriculture', 'industry',
|
|
'architecture', 'fountains', 'hospitals', 'schools', 'markets', 'lakes', 'bays', 'islands', 'forts',
|
|
'wars', 'war', 'emperors', 'kings', 'queens', 'nobility', 'family', 'families', 'politicians'}
|
|
PT = {'de', 'da', 'do', 'das', 'dos', 'e', 'em', 'no', 'na', 'nos', 'nas', 'para', 'com'}
|
|
|
|
|
|
def idioma_termo(t: str) -> str:
|
|
if re.search(r'[ãõçáéíóúâêô]', t.lower()):
|
|
return 'por'
|
|
palavras = set(re.findall(r'[a-z]+', t.lower()))
|
|
if palavras & PT:
|
|
return 'por'
|
|
if palavras & EN or re.search(r'\((?:brazil|state)', t.lower()):
|
|
return 'eng'
|
|
return 'por'
|