109 lines
8.7 KiB
Python
109 lines
8.7 KiB
Python
"""Extract the supplied reference; curated groupings cite its exact rows/paragraphs.
|
||
No coordinates, offers, territorial masters or product rules are created.
|
||
"""
|
||
from pathlib import Path
|
||
import argparse, hashlib, json, re, zipfile, xml.etree.ElementTree as E
|
||
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument('source', type=Path)
|
||
parser.add_argument('--out', type=Path, required=True)
|
||
args = parser.parse_args()
|
||
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
||
def clean(value):
|
||
return re.sub(r'\s+([,;)])', r'\1', re.sub(r'([(])\s+', r'\1', re.sub(r'\s+', ' ', value))).strip()
|
||
def text(node):
|
||
return clean(' '.join(t.text or '' for t in node.findall('.//w:t', ns)))
|
||
with zipfile.ZipFile(args.source) as archive:
|
||
body = E.fromstring(archive.read('word/document.xml')).find('w:body', ns)
|
||
relationships = {r.attrib['Id']: r.attrib['Target'] for r in E.fromstring(archive.read('word/_rels/document.xml.rels')) if r.attrib.get('Type','').endswith('/hyperlink')}
|
||
paragraph_nodes = [n for n in body if n.tag.endswith('}p') and ' '.join(t.text or '' for t in n.findall('.//w:t', ns))]
|
||
source_links = []
|
||
for index, node in enumerate(paragraph_nodes):
|
||
for link in node.findall('.//w:hyperlink', ns):
|
||
url = relationships.get(link.attrib.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id'), '')
|
||
if index >= 90 and url.startswith(('https://','http://')):
|
||
source_links.append({'title':text(node),'url':url,'source_paragraph':index})
|
||
# Preserve positions before whitespace normalization, including the original blank paragraph.
|
||
paragraphs = [text(n) for n in body if n.tag.endswith('}p') and ' '.join(t.text or '' for t in n.findall('.//w:t', ns))]
|
||
tables = [[[text(c) for c in r.findall('w:tc', ns)] for r in n.findall('w:tr', ns)] for n in body if n.tag.endswith('}tbl')]
|
||
assert len(paragraphs) == 114 and len(tables) == 6 and len(tables[1]) == 11
|
||
groups = [('EN-GAS','Gás natural e GNL'),('EN-POWER','Electricidade, hidroeléctricas e redes'),('EN-SOLAR','Solar, armazenamento e sistemas fora da rede'),('EN-WIND','Energia eólica'),('EN-HYDROGEN','Hidrogénio verde'),('EN-BIO','Biocombustíveis'),('EN-GREEN','Parques industriais verdes')]
|
||
areas = []
|
||
def area(number, title, group, locations, labels, chain, evidence, citations):
|
||
areas.append({'id': f'ENE-{number:02}', 'title': title, 'group_ids': [group],
|
||
'locations_text': locations, 'location_labels': labels, 'value_chain': chain,
|
||
'market_evidence': evidence, 'indicative_location': locations,
|
||
'source_row': citations, 'source_state': 'Referência para preparação',
|
||
'availability': 'reference_only', 'promotion_ready': False, 'coordinates': None})
|
||
for i, row in enumerate(tables[3][1:], 1):
|
||
locations, labels, paragraphs_used = {
|
||
1: ('Bacia do Rovuma; Buzi. A fonte não atribui todas as áreas a uma província.', ['Bacia do Rovuma','Buzi'], [34,35]),
|
||
2: ('Projectos de GNL; base logística de Pemba.', ['Pemba'], [35,58]),
|
||
3: ('Gasoduto Norte–Sul; corredor Palma–Maputo, apresentado como proposta de estudo.', ['Norte–Sul','Palma–Maputo'], [35,51]),
|
||
4: ('Corredor Palma–Maputo proposto no estudo; implantação de cada indústria por definir.', ['Palma–Maputo'], [50,51,53]),
|
||
5: ('Implantação a definir no investimento; sem localização discriminada na fonte.', [], [55,56]),
|
||
}[i]
|
||
area(i, 'Gás natural — '+row[0], 'EN-GAS', locations, labels,
|
||
row[1]+'. Perfil de investidor: '+row[2]+'.',
|
||
' '.join(paragraphs[n] for n in paragraphs_used),
|
||
f'table4:row{i}; '+','.join('paragraph:'+str(n) for n in paragraphs_used))
|
||
area(6, 'Hidroeléctricas, redes e interligações', 'EN-POWER',
|
||
'Rio Zambeze; Maputo–Temane; interligação Moçambique–Zâmbia. Âmbitos de projecto, sem coordenadas.',
|
||
['Rio Zambeze','Maputo–Temane','Moçambique–Zâmbia'],
|
||
'Fornecimento de subestações, transformadores, protecções eléctricas, SCADA e modernização de redes.',
|
||
' '.join(paragraphs[n] for n in [37,38,67,68]), 'paragraphs:37,38,67,68')
|
||
area(7, 'Solar, baterias e sistemas fora da rede', 'EN-SOLAR',
|
||
'Mocuba (Zambézia), Metoro (Cabo Delgado), Cuamba (Niassa) e Afungi. Sistemas fora da rede: implantação a definir.',
|
||
['Zambézia','Mocuba','Cabo Delgado','Metoro','Niassa','Cuamba','Afungi'],
|
||
'Produção solar, sistemas de armazenamento, fornecimento e manutenção de equipamentos e soluções fora da rede.',
|
||
' '.join(paragraphs[n] for n in [40,43,45,46]), 'paragraphs:40,43,45,46; table3:rows1–3')
|
||
area(8, 'Energia eólica — Namaacha', 'EN-WIND', 'Namaacha (Maputo, sem distinção entre província e cidade na fonte).',
|
||
['Namaacha','Maputo'], 'Desenvolvimento e fornecimento para geração eólica; obras previstas dependem do fecho financeiro.',
|
||
paragraphs[47], 'paragraph:47')
|
||
area(9, 'Hidrogénio verde — estudos e infra-estruturas', 'EN-HYDROGEN', 'Sem implantação concreta identificada na fonte.', [],
|
||
'Estudos de mercado e viabilidade, consultoria, parcerias, produção, armazenamento e transporte.',
|
||
' '.join(paragraphs[n] for n in [61,62]), 'paragraphs:61,62')
|
||
area(10, 'Biocombustíveis e respectivas cadeias de fornecimento', 'EN-BIO', 'Implantação a definir; a fonte não discrimina locais de produção.', [],
|
||
'Matérias-primas agrícolas, produção, armazenagem, transporte, distribuição e mistura de biocombustíveis.',
|
||
' '.join(paragraphs[n] for n in [64,65]), 'paragraphs:64,65')
|
||
area(11, 'Parques industriais verdes', 'EN-GREEN', 'Corredores e parques sem implantação discriminada na fonte.', [],
|
||
'Indústria e infra-estruturas abastecidas por energia limpa; preparação de parcerias e localização.',
|
||
paragraphs[69], 'paragraph:69')
|
||
group_rows = []
|
||
for key, name in groups:
|
||
matches = [a for a in areas if key in a['group_ids']]
|
||
labels = list(dict.fromkeys(label for a in matches for label in a['location_labels']))
|
||
group_rows.append({'id': key, 'name': name, 'locations_text': ' '.join(dict.fromkeys(a['locations_text'] for a in matches)), 'location_labels': labels, 'coordinates': None})
|
||
projects = []
|
||
for i, row in enumerate(tables[1][1:], 1):
|
||
solar = next((r for r in tables[2][1:] if row[0] in r[0]), None)
|
||
if solar:
|
||
description = f'{solar[0]} — {row[1]}. Capacidade: {solar[1]}. Investimento referido: {solar[2]}. Parcerias: {solar[3]}. Estado na fonte: {solar[4]}.'
|
||
else:
|
||
amount = '' if row[2] == 'n.d.' else f' Investimento estimado: {row[2]}.'
|
||
description = f'{row[0]} — {row[1]}.{amount} Estado/calendário na fonte: {row[3]}.'
|
||
projects.append(description)
|
||
result = {'format': 'investmoz-energy-reference-v1', 'source_sha256': hashlib.sha256(args.source.read_bytes()).hexdigest(),
|
||
'source_document_name': args.source.name, 'reference_date': '2026-11-11', 'title': 'Energia e cadeias de valor em Moçambique',
|
||
'groups': group_rows, 'areas': areas, 'projects': projects,
|
||
'indicators': [], 'risks': [f'{r[0]}: {r[1]} Resposta a considerar: {r[2]}' for r in tables[5][1:]],
|
||
'sources': paragraphs[90:], 'source_links': source_links,
|
||
'background': [{'title':'Financiamento referido nas fontes','paragraphs':[paragraphs[29],paragraphs[71]]},
|
||
{'title':'Enquadramento sectorial referido nas fontes','paragraphs':[paragraphs[n] for n in [74,76,78,79]]},
|
||
{'title':'Âmbito dos dados solares','paragraphs':[paragraphs[45],paragraphs[46]]}],
|
||
'original_tables': tables, 'original_paragraphs': paragraphs,
|
||
'source_instructions_are_not_product_rules': True}
|
||
assert len(projects) == 10 and len(areas) == 11 and len(result['sources']) == 24
|
||
assert len(source_links) == 24
|
||
# Editorial presentation, preserving original wording in original_paragraphs/tables.
|
||
def editorial(value):
|
||
if isinstance(value, str):
|
||
return value.replace('ainda não encontrei projectos com decisão final de investimento','o levantamento não identifica projectos com decisão final de investimento').replace('não encontrei uma confirmação directa da EDM, nem dados de 2026 sobre Metoro','a referência consultada não apresenta confirmação directa da EDM nem dados de 2026 sobre Metoro')
|
||
if isinstance(value, list): return [editorial(x) for x in value]
|
||
if isinstance(value, dict): return {k:editorial(v) for k,v in value.items()}
|
||
return value
|
||
for key in ['areas','background']: result[key]=editorial(result[key])
|
||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||
args.out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(json.dumps({'sha256':result['source_sha256'],'groups':len(groups),'areas':len(areas),'projects':len(projects),'sources':len(result['sources'])}))
|