the_one_that_does_it.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # %%
  2. import csv
  3. import json
  4. import openpyxl as op
  5. import re
  6. # BASIC CONFIGURATION
  7. DATA_FOLDER = './data/'
  8. OUTPUT_FOLDER = './output/'
  9. ONTO_FILENAME = 'manoscritti_dariah' # No extension!
  10. ent_filename = ONTO_FILENAME + '_entities.csv'
  11. rel_filename = ONTO_FILENAME + '_relations.csv'
  12. # PART I: parse xlsx to (multiple) csv
  13. # Excel configuration
  14. XLSX_FILENAME = 'Struttura_NEW.xlsx'
  15. ENTITIES_SHEETNAME = 'Entità'
  16. RELATIONS_SHEETNAME = 'Relazioni'
  17. # %%
  18. # Import the defining xlsx through openpyxl
  19. input_data = op.load_workbook(DATA_FOLDER + XLSX_FILENAME)
  20. # Read relevant sheets
  21. entities_sheet = input_data[ENTITIES_SHEETNAME]
  22. relations_sheet = input_data[RELATIONS_SHEETNAME]
  23. # Parse sheet data into a dict (assuming the xlsx has headers)
  24. # Get non-empty headers for entities sheet
  25. entities_keys = [cell for cell in next(entities_sheet.values) if cell]
  26. # Get entity rows as list of dicts
  27. raw_entities = [{key: row[ind] for ind, key in enumerate(entities_keys)} for row in entities_sheet.values][1:]
  28. # Get non-empty headers for relations sheet
  29. relations_keys = [cell for cell in next(relations_sheet.values) if cell]
  30. # Get relation rows as list of dicts
  31. raw_relations = [{key: row[ind] for ind, key in enumerate(relations_keys)} for row in relations_sheet.values][1:]
  32. # %%
  33. # NOTE:
  34. # a. Non ci sono, al momento, _constraints_ di unicità per le relazioni che siano imposti tramite il "foglio master".
  35. # b. Non ci sono neanche constraint di esistenza, TRANNE l'id univoca, intesa NON come quella del sistema ma quella della comunità di riferimento, e tipica del settore di dominio considerato; nel caso specifico, la SEGNATURA.
  36. # c. Per identificare le informazioni 'atomiche' non si usa un campo dedicato, ma una logica. AL MOMENTO la logica è che si considera atomica una entità che non è mai 'prima' in una relazione. L'ORDINE DELLE RELAZIONI E' IMPORTANTE a differenza di quanto assumevo inizialmente. 'Atomiche' è probabilmente un _misnomer_, dato che può trattarsi di informazioni composite, come ad esempio una data. Si tratta più precisamente (forse) di ATTRIBUTI, nel senso di informazioni "figlie" che hanno significato solo in associazione all'Entità _parent_ -- proprio come nel caso delle date.
  37. # d. Si effettua un controllo di unicità sulle entità, basato sul nome normalizzato (parole trattate con: sostituzione del _whitespace_ contiguo con spazio singolo ' ', title() e strip()). Nessuna entità può avere nome vuoto. Eventuali nomi duplicati vengono segnalati per un controllo manuale.
  38. # e. Si effettua un controllo di unicità sulle relazioni, che riguarda tutta la terna SOGGETTO-RELAZIONE-OGGETTO (normalizzata in modo simile ai nomi di entità, ma nella RELAZIONE gli spazi sono underscores e si usa lower() invece che title()). Nessuno dei membri della terna può essere vuoto; il nome della relazione inversa invece è opzionale e non viene controllato.
  39. # f. Si effettuano controlli di consistenza sulle relazioni:
  40. # .f1. Nessuna relazione con entità non definite come SOGGETTO.
  41. # .f2. Warning sulle entità "orfane", ovvero non presenti in alcuna relazione.
  42. # g. Una relazione può avere un OGGETTO non definito nel foglio Entità. E' sottointeso, in questi casi, che si tratta di un'informazione 'atomica' / un attributo.
  43. #
  44. # TODO: completare secondo le specifiche sopra -> Fatto al 90%
  45. # TODO: effettuare il merge con i "miei" CSV, che hanno informazioni in più! --> Fatto, solo da riverificare
  46. # TODO: ottimizzare un po' la scrittura del codice -> Non prioritario
  47. # Process entities:
  48. def normalize_ent_name(name: str) -> str:
  49. return re.sub(r'\s+', ' ', name.strip().title())
  50. # 1. Filter out unnamed entities, normalize entity names, collect aliases, discover duplicates
  51. clean_entities = {}
  52. for ent in raw_entities:
  53. entity_names = ent['Concetto']
  54. if not isinstance(entity_names, str):
  55. continue
  56. aliases = [normalize_ent_name(al) for al in re.split(r'\r\n|\r|\n', entity_names) if al.strip()]
  57. if not aliases:
  58. continue
  59. entity_name = aliases[0]
  60. entity_same_as = aliases[1:]
  61. if clean_entities.get(entity_name):
  62. # DUPLICATE!
  63. clean_entities[entity_name].append({'Alias': entity_same_as, 'Raw': ent})
  64. else:
  65. clean_entities[entity_name] = [{'Alias': entity_same_as, 'Raw': ent}]
  66. # TEMP - filter out a few of the entities manually
  67. clean_entities.pop(normalize_ent_name('Circolazione Del Manoscritto'), None)
  68. clean_entities.pop(normalize_ent_name('Luogo Di Conservazione'), None)
  69. clean_entities.pop(normalize_ent_name('Possessore'), None)
  70. clean_entities.pop(normalize_ent_name('Volume'), None)
  71. all_entities = clean_entities.keys()
  72. duplicated_entities = [ent_name for ent_name, ent_val in clean_entities.items() if len(ent_val)>1]
  73. if duplicated_entities:
  74. print('WARNING! DUPLICATED ENTITIES:')
  75. for ent in duplicated_entities:
  76. print(ent)
  77. # %%
  78. # Process relations:
  79. # 1. Filter ill-formed relations and normalize entity names
  80. def normalize_rel_name(name: str) -> str:
  81. return re.sub(r'\s+', '_', name.strip().lower()).replace('__', '_')
  82. clean_relations = []
  83. for rel in raw_relations:
  84. subj = rel['Soggetto']
  85. obj = rel['Oggetto']
  86. if not isinstance(subj, str) or not isinstance(obj, str):
  87. continue
  88. subj = re.sub(r'\s+', ' ', subj.strip().title())
  89. obj = re.sub(r'\s+', ' ', obj.strip().title())
  90. if subj==obj:
  91. continue
  92. rel_name = rel['Relazione']
  93. if isinstance(rel_name, str):
  94. rel_name = normalize_rel_name(rel_name)
  95. rel_name_inv = rel.get('Relazione Inversa (opzionale)')
  96. if isinstance(rel_name_inv, str):
  97. rel_name_inv = normalize_rel_name(rel_name_inv)
  98. clean_rel = {'Soggetto': subj, 'Relazione': rel_name, 'Oggetto': obj, 'Relazione Inversa': rel_name_inv}
  99. clean_relations.append(clean_rel)
  100. # TEMP - filter out a few of the relations manually
  101. def manual_remove(rel: dict) -> bool:
  102. if rel['Soggetto']==normalize_ent_name('Circolazione del manoscritto'):
  103. return True
  104. if rel['Soggetto']==normalize_ent_name('Fascicolo') and rel['Oggetto']==normalize_ent_name('Data'):
  105. return True
  106. #
  107. return False
  108. clean_relations = [rel for rel in clean_relations if not manual_remove(rel)]
  109. # In this case, I don't really care if a relation appears twice
  110. # Get all Entities that appear as Subjects
  111. all_subjects = set(rel['Soggetto'] for rel in clean_relations)
  112. # Get all Entities that appear either as Subjects or Objects
  113. all_cited_entities = set(sum([[rel['Soggetto'], rel['Oggetto']] for rel in clean_relations], []))
  114. # Get entities that appear in the relations sheet but NOT in the entities sheet
  115. undefined_entities = all_cited_entities - all_entities
  116. # Get all entities that appear purely as Subjects; it is (mostly) OK if they are not defined in the Entity sheet.
  117. atomic_entities = all_cited_entities - all_subjects
  118. # Entities which are not 'atomic' and are undefined are a problem
  119. problematic_entities = undefined_entities - atomic_entities
  120. # Get entities which do not appear in any relation, and are NOT classified as Subclasses.
  121. subclass_entities = {key for key, val in clean_entities.items() if val[0]['Raw']['Sottoclasse di']}
  122. unused_entities = (all_entities - all_cited_entities) - subclass_entities
  123. # %%
  124. ####
  125. # MANUS ONLINE (MOL) API: https://api.iccu.sbn.it/devportal/apis
  126. ####
  127. # %%
  128. if problematic_entities:
  129. print('ERROR: some non-atomic entities are undefined')
  130. print()
  131. for ent in sorted(list(problematic_entities)):
  132. print(ent)
  133. # %%
  134. if unused_entities:
  135. print('Unused entities:')
  136. print()
  137. for ent in sorted(list(unused_entities)):
  138. print(ent)
  139. # %%
  140. if undefined_entities:
  141. print("Undefined 'atomic' entities:")
  142. print()
  143. for ent in sorted(list(undefined_entities)):
  144. print(ent)
  145. # %%
  146. # Export results
  147. if not problematic_entities and not duplicated_entities:
  148. nonatomic_entities = {ent: ent_val for ent, ent_val in clean_entities.items() if ent not in atomic_entities}
  149. entities_to_export = [{'ENTITÀ': ent, 'ATTRIBUTO (LITERAL)': None, 'SAME AS': ', '.join(ent_val[0]['Alias'])} for ent, ent_val in nonatomic_entities.items()]
  150. #
  151. atomic_rels = [rel for rel in clean_relations if rel['Oggetto'] in atomic_entities]
  152. entities_to_export += [{'ENTITÀ': rel['Soggetto'], 'ATTRIBUTO (LITERAL)': rel['Oggetto'], 'SAME AS': None} for rel in atomic_rels]
  153. entities_to_export = sorted(entities_to_export, key=lambda x: x['ENTITÀ'])
  154. with open(OUTPUT_FOLDER + ent_filename, 'w', encoding='utf-8', newline='\n') as ent_csv:
  155. headers = ['ENTITÀ', 'ATTRIBUTO (LITERAL)', 'SAME AS']
  156. writer = csv.DictWriter(ent_csv, fieldnames=headers)
  157. writer.writeheader()
  158. writer.writerows(entities_to_export)
  159. relations_to_export = [{'ENTITÀ 1': rel['Oggetto'], 'ENTITÀ 2': rel['Soggetto'], 'NOME RELAZIONE': rel['Relazione'], 'NOME RELAZIONE INVERSA': rel['Relazione Inversa']} for rel in clean_relations]
  160. # Add subclass relations
  161. relations_to_export += [{'ENTITÀ 1': ent, 'ENTITÀ 2': clean_entities[ent][0]['Raw']['Sottoclasse di'], 'NOME RELAZIONE': 'is_subclass_of', 'NOME RELAZIONE INVERSA': 'is_superclass_of'} for ent in subclass_entities]
  162. relations_to_export = sorted(relations_to_export, key=lambda x: (x['ENTITÀ 1'], x['ENTITÀ 2']) )
  163. with open(OUTPUT_FOLDER + rel_filename, 'w', encoding='utf-8', newline='\n') as rel_csv:
  164. headers = ['ENTITÀ 1', 'ENTITÀ 2', 'NOME RELAZIONE', 'NOME RELAZIONE INVERSA']
  165. writer = csv.DictWriter(rel_csv, fieldnames=headers)
  166. writer.writeheader()
  167. writer.writerows(relations_to_export)
  168. # %%