- 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
120 lines
5.1 KiB
Python
120 lines
5.1 KiB
Python
"""Registros de autoridade (ISAAR(CPF)) para produtores — elemento 2.1 e 8.1.
|
|
|
|
Junta as grafias de `dc.contributor` ("Malta, Augusto", "Malta, Augusto,
|
|
1864-1957", "Marc Ferrez" e "Ferrez, Marc") numa forma autorizada, com as
|
|
variantes como remissivas (proc. 8.1.4).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
from collections import Counter, defaultdict
|
|
|
|
ANONIMO = re.compile(r'(?i)^\s*(an[oô]nimo|autoria n[aã]o identificada|s/\s*autor|sem autor|desconhecid[oa]|'
|
|
r'n[aã]o identificad[oa]|autor n[aã]o identificado|autor desconhecido)\s*\.?$')
|
|
COLETIVA = re.compile(r'(?i)\b(escola|officina|oficina|photographia|fotografia|photo|foto|studio|est[uú]dio|atelier|'
|
|
r'casa|companhia|cia\.?|museu|instituto|minist[eé]rio|servi[cç]o|diretoria|departamento|'
|
|
r'comiss[aã]o|biblioteca|arquivo|editora|lith|litografia|tipografia|typ|&|irm[aã]os|'
|
|
r'sociedade|empresa|jornal|revista|ag[eê]ncia|governo|prefeitura|marinha|ex[eé]rcito|'
|
|
r'for[cç]a|aeron[aá]utica|avia[cç][aã]o|universidade|fundação)\b')
|
|
DATAS = re.compile(r',?\s*((?:ca\.\s*|fl\.\s*|m\.\s*|n\.\s*)?\d{4}\??(?:\s*-\s*(?:ca\.\s*)?\d{0,4}\??)?)\.?\s*$')
|
|
PARTICULAS = {'de', 'da', 'do', 'das', 'dos', 'e', 'von', 'van', 'du', 'del', 'di'}
|
|
|
|
|
|
def _chave(s: str) -> str:
|
|
s = ''.join(c for c in unicodedata.normalize('NFD', s.lower()) if unicodedata.category(c) != 'Mn')
|
|
s = re.sub(r'\(.*?\)', '', s)
|
|
return re.sub(r'[^\w,]+', ' ', s).strip(' ,')
|
|
|
|
|
|
def _separa_datas(nome: str) -> tuple[str, str | None]:
|
|
nome = nome.strip().rstrip('.')
|
|
if m := DATAS.search(nome):
|
|
if ',' in nome[:m.start()] or m[1].startswith(('fl', 'm.', 'n.', 'ca')):
|
|
return nome[:m.start()].strip(' ,'), m[1]
|
|
return nome, None
|
|
|
|
|
|
def _inverte(nome: str) -> str | None:
|
|
"""'Marc Ferrez' → 'Ferrez, Marc'. Só para nomes de pessoa sem vírgula."""
|
|
partes = nome.split()
|
|
if len(partes) < 2 or ',' in nome:
|
|
return None
|
|
i = len(partes) - 1
|
|
while i > 1 and partes[i - 1].lower() in PARTICULAS:
|
|
i -= 1
|
|
return f"{' '.join(partes[i:])}, {' '.join(partes[:i])}"
|
|
|
|
|
|
def periodo(datas: str | None) -> tuple[int, int] | None:
|
|
"""Anos prováveis de atividade: fl. A-B, ou nascimento+15 até a morte."""
|
|
if not datas:
|
|
return None
|
|
anos = [int(a) for a in re.findall(r'\d{4}', datas)]
|
|
if not anos:
|
|
return None
|
|
if datas.startswith('fl'):
|
|
return (anos[0], anos[-1]) if len(anos) > 1 else (anos[0], anos[0])
|
|
if datas.startswith('m.'):
|
|
return None
|
|
if len(anos) == 2:
|
|
return anos[0] + 15, anos[1]
|
|
return None
|
|
|
|
|
|
class Autoridades:
|
|
def __init__(self):
|
|
self.contagem: Counter = Counter()
|
|
|
|
def registra(self, nome: str):
|
|
if nome and nome.strip() and not ANONIMO.match(nome):
|
|
self.contagem[nome.strip()] += 1
|
|
|
|
def resolve(self):
|
|
"""Agrupa as grafias. Depois disso `forma(nome)` devolve o id da autoridade."""
|
|
grupos: dict[str, Counter] = defaultdict(Counter)
|
|
for nome, n in self.contagem.items():
|
|
base, _ = _separa_datas(nome)
|
|
grupos[_chave(base)][nome] += n
|
|
# grafias diretas ("Marc Ferrez") que batem com uma invertida já conhecida
|
|
for k in list(grupos):
|
|
if ',' in k:
|
|
continue
|
|
nome = next(iter(grupos[k]))
|
|
if COLETIVA.search(nome):
|
|
continue
|
|
inv = _inverte(_separa_datas(nome)[0])
|
|
if inv and _chave(inv) in grupos and _chave(inv) != k:
|
|
grupos[_chave(inv)].update(grupos.pop(k))
|
|
|
|
self.registros: dict[str, dict] = {}
|
|
self._id_de: dict[str, str] = {}
|
|
for k, variantes in sorted(grupos.items(), key=lambda kv: -sum(kv[1].values())):
|
|
# forma autorizada: a variante mais completa (com datas) e mais usada
|
|
# (datas com início e fim > só uma data > nenhuma), depois frequência
|
|
def peso(v):
|
|
dt = _separa_datas(v)[1] or ''
|
|
return (len(re.findall(r'\d{4}', dt)), variantes[v], v)
|
|
forma = max(variantes, key=peso)
|
|
forma = forma.strip().rstrip('.')
|
|
base, datas = _separa_datas(forma)
|
|
tipo = 'entidade coletiva' if COLETIVA.search(base) else ('pessoa' if ',' in base else 'pessoa ou entidade')
|
|
if re.match(r'(?i)fam[ií]lia\b', base):
|
|
tipo = 'família'
|
|
aid = 'A' + str(len(self.registros) + 1).zfill(4)
|
|
self.registros[aid] = {
|
|
'id': aid, 'forma_autorizada': forma, 'nome': base, 'datas_existencia': datas,
|
|
'tipo': tipo, 'variantes': sorted(v for v in variantes if v.strip().rstrip('.') != forma),
|
|
'ocorrencias': sum(variantes.values()), 'periodo_atividade': periodo(datas),
|
|
'fontes': [],
|
|
}
|
|
for v in variantes:
|
|
self._id_de[v] = aid
|
|
|
|
def id(self, nome: str) -> str | None:
|
|
return self._id_de.get((nome or '').strip())
|
|
|
|
def __getitem__(self, aid: str) -> dict:
|
|
return self.registros[aid]
|