- 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
166 lines
7.1 KiB
Python
166 lines
7.1 KiB
Python
"""Nível 3 — séries a partir de `dc.source`.
|
|
|
|
O campo traz, misturados, o nome da coleção/fundo de origem ("Coleção Gilberto
|
|
Ferrez" e "Gilberto Ferrez"), sufixos da instituição ("… / Museu da
|
|
República"), notações de caixa ("CX 14") e lixo de extração. A regra:
|
|
|
|
1. limpa o valor até uma chave (sem prefixo Coleção/Fundo/Arquivo, sem sufixo
|
|
institucional, sem notação no fim, sem acento, sem pontuação);
|
|
2. chaves com pelo menos MINIMO itens viram série;
|
|
3. valor raro que contém a chave de uma série vai para ela (os "Negativo de
|
|
vidro MPEG639, Coleção Fotográfica do Museu Paraense…");
|
|
4. o resto vai para a série AV (Avulsos) da instituição, e o valor original
|
|
fica como notação na origem (5.1).
|
|
|
|
Correções manuais em `ajustes/series.tsv` (instituicao, dc.source, titulo)
|
|
prevalecem sobre a regra.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import re
|
|
import unicodedata
|
|
from collections import Counter, defaultdict
|
|
|
|
from .config import AJUSTES
|
|
|
|
MINIMO = 3
|
|
|
|
# variantes que a limpeza não junta sozinha: chave → chave canônica
|
|
ALIAS = {
|
|
'd teresa cristina maria': 'thereza christina maria',
|
|
'teresa cristina maria': 'thereza christina maria',
|
|
'federacao brasileira para o progresso feminino fbpf': 'federacao brasileira pelo progresso feminino',
|
|
'federacao brasileira para o progresso feminino': 'federacao brasileira pelo progresso feminino',
|
|
'fotografica mpeg': 'fotografica do museu paraense emilio goeldi',
|
|
'brascan cem anos no brasil': 'brascan cem anos no brasil',
|
|
'escola de ciencias sociais fgv cpdoc': 'escola de ciencias sociais fgv cpdoc',
|
|
'escola de ciencias sociais': 'escola de ciencias sociais fgv cpdoc',
|
|
'dos eventos institucionais': 'eventos institucionais',
|
|
'mestres do sec xix': 'mestres do seculo xix',
|
|
'cristovao barcellos': 'cristovao barcelos',
|
|
}
|
|
|
|
# notação de caixa/negativo/foto: é localização na origem (5.1), não série
|
|
NOTACAO = re.compile(r'(?i)^\s*(cx|caixas?|nv|foto|negativo|estampa)\b')
|
|
|
|
PREFIXO = re.compile(r'^(convenio|colecao particular|colecao|fundo|arquivo|acervo|serie)\s+(d[aeo]s?\s+)?')
|
|
LIXO = re.compile(r'[<>{}]|ns:|^[0-9a-f]{12,}')
|
|
|
|
|
|
def sem_acento(s: str) -> str:
|
|
return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
|
|
|
|
|
def limpa(v: str) -> str:
|
|
s = v.strip()
|
|
s = re.sub(r'\s*/\s*(acervo|museu|ibge|instituto moreira).*$', '', s, flags=re.I)
|
|
s = re.sub(r'\s+-\s+(museu|funda|fgv|ibge).*$', '', s, flags=re.I)
|
|
s = re.sub(r'\s*\((museu|ibram)[^)]*\)', '', s, flags=re.I)
|
|
s = re.sub(r'\s*/\s*ibge$', '', s, flags=re.I)
|
|
s = re.sub(r'\s*(?:-\s*|\()\s*\w*\d\w*\)?\s*$', '', s) # "- 1123K", "(148T)", "- EFi9"
|
|
s = sem_acento(s.lower())
|
|
s = re.sub(r'\bsec\.?\s', 'sec ', s)
|
|
s = re.sub(r'[^\w\s]', ' ', s)
|
|
s = re.sub(r'\s+', ' ', s).strip()
|
|
s = PREFIXO.sub('', s)
|
|
return ALIAS.get(s, s)
|
|
|
|
|
|
def sigla(titulo: str, usadas: set[str]) -> str:
|
|
if m := re.match(r'(?i)[áa]lbum\s+(\d+)', titulo):
|
|
base = f'A{m[1]}'
|
|
usadas.add(base)
|
|
return base
|
|
miolo = PREFIXO.sub('', sem_acento(titulo.lower()))
|
|
palavras = [p for p in re.findall(r'[a-z0-9]+', miolo) if p not in {'de', 'da', 'do', 'das', 'dos', 'e', 'a', 'o'}]
|
|
base = ''.join(p[0] for p in palavras).upper()[:6] or 'S'
|
|
if len(base) < 2 and palavras:
|
|
base = palavras[0][:4].upper()
|
|
s, n = base, 2
|
|
while s in usadas or s == 'AV':
|
|
s, n = f'{base}{n}', n + 1
|
|
usadas.add(s)
|
|
return s
|
|
|
|
|
|
def _titulo(variantes: Counter) -> str:
|
|
"""A variante mais frequente; se ela não diz o que é, herda o tipo
|
|
(Coleção/Fundo/Série) de outra variante — "Thereza Christina Maria" +
|
|
"Coleção D. Teresa…" → "Coleção Thereza Christina Maria"."""
|
|
tipo_re = r'(?i)^(cole[cç][aã]o( particular)?|fundo|arquivo|s[ée]rie|conv[eê]nio|[áa]lbum)\b'
|
|
_, v = max((n, v) for v, n in variantes.items())
|
|
v = re.sub(r'\s+', ' ', v).strip()
|
|
v = re.sub(r'\s*[-/]\s*(Acervo|Museu|Fundação|Instituto Moreira|IBGE).*$', '', v)
|
|
v = re.sub(r'\s*\((Museu|IBRAM)[^)]*\)', '', v)
|
|
if not re.match(tipo_re, v):
|
|
tipos = Counter(m[1].capitalize() for x in variantes if (m := re.match(tipo_re, x.strip())))
|
|
v = f"{tipos.most_common(1)[0][0] if tipos else 'Coleção'} {v}"
|
|
return v
|
|
|
|
|
|
def _ajustes() -> dict[tuple[str, str], str]:
|
|
p = AJUSTES / 'series.tsv'
|
|
if not p.exists():
|
|
return {}
|
|
with p.open(encoding='utf-8') as f:
|
|
return {(r['instituicao'], r['dc_source']): r['titulo'] for r in csv.DictReader(f, delimiter='\t')}
|
|
|
|
|
|
def monta(fontes: list[tuple[str, str, str | None]]):
|
|
"""fontes: (handle, instituição, dc.source | None).
|
|
|
|
Devolve (por_handle, series): por_handle[handle] = (sigla, notação na origem
|
|
ou None); series[(instituição, sigla)] = {'titulo', 'variantes', 'n'}.
|
|
"""
|
|
ajuste = _ajustes()
|
|
chaves = defaultdict(Counter) # (inst, chave) → Counter(valor original)
|
|
for _, inst, v in fontes:
|
|
if v and not LIXO.search(v) and not NOTACAO.match(v) and (inst, v) not in ajuste:
|
|
chaves[(inst, limpa(v))][v] += 1
|
|
|
|
series: dict[tuple[str, str], dict] = {}
|
|
sigla_de: dict[tuple[str, str], str] = {}
|
|
usadas: dict[str, set[str]] = defaultdict(set)
|
|
fortes = defaultdict(list) # inst → [chave]
|
|
for (inst, chave), variantes in sorted(chaves.items(), key=lambda kv: -sum(kv[1].values())):
|
|
if not chave or sum(variantes.values()) < MINIMO:
|
|
continue
|
|
titulo = _titulo(variantes)
|
|
sg = sigla(titulo, usadas[inst])
|
|
sigla_de[(inst, chave)] = sg
|
|
series[(inst, sg)] = {'titulo': titulo, 'variantes': variantes, 'n': 0}
|
|
fortes[inst].append(chave)
|
|
for (inst, v), titulo in ajuste.items():
|
|
k = (inst, 'ajuste:' + titulo)
|
|
if k not in sigla_de:
|
|
sg = sigla(titulo, usadas[inst])
|
|
sigla_de[k] = sg
|
|
series[(inst, sg)] = {'titulo': titulo, 'variantes': Counter(), 'n': 0}
|
|
|
|
por_handle = {}
|
|
for handle, inst, v in fontes:
|
|
sg = None
|
|
if v and (inst, v) in ajuste:
|
|
sg = sigla_de[(inst, 'ajuste:' + ajuste[(inst, v)])]
|
|
series[(inst, sg)]['variantes'][v] += 1
|
|
elif v and not LIXO.search(v):
|
|
chave = limpa(v)
|
|
sg = sigla_de.get((inst, chave))
|
|
if sg is None:
|
|
# o valor raro contém o nome de uma série forte da mesma instituição?
|
|
for forte in sorted(fortes[inst], key=len, reverse=True):
|
|
if len(forte) > 5 and re.search(rf'\b{re.escape(forte)}\b', chave):
|
|
sg = sigla_de[(inst, forte)]
|
|
series[(inst, sg)]['variantes'][v] += 1
|
|
break
|
|
if sg is None:
|
|
sg = 'AV'
|
|
series.setdefault((inst, 'AV'), {'titulo': 'Avulsos', 'variantes': Counter(), 'n': 0})
|
|
series[(inst, sg)]['n'] += 1
|
|
# o valor original fica como notação na origem quando não é o próprio nome da série
|
|
notacao = v if v and (sg == 'AV' or limpa(v) not in {limpa(x) for x in [series[(inst, sg)]['titulo']]}) else None
|
|
por_handle[handle] = (sg, notacao)
|
|
return por_handle, series
|