"""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