{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## PARSER ONTOLOGIE" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### INTRODUZIONE\n", "Questo notebook (testato con Python 3.12) contiene una implementazione di base del codice che ci serve.\n", " - Input: un file .xlsx\n", " - Output: un file .rdf\n", "\n", "L'output finale è una definizione strutturata dell'ontologia descritta 'informalmente' nel file .xlsx iniziale. L'output è in formato RDF/XML invece che TTL per un discorso essenzialmente storico di implementazione. Il file .rdf può essere facilmente convertito in .ttl con strumenti esistenti, ad esempio noi lo abbiamo ottenuto usando il servizio pubblico WEBVOWL:\n", "\n", "https://service.tib.eu/webvowl/\n", "\n", "importando il file .rdf e riesportandolo come .ttl.\n", "\n", "Una breve nota riguardo alla lingua: i commenti nel codice ed i nomi delle variabili sono in inglese per un discorso di (eventuale) portabilità. I commenti e le descrizioni interne ai file sono invece in italiano per praticità, dato che tutto il personale coinvolto nel lavoro è italiano." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Il primo passo è definire alcuni import e variabili globali, in particolare i folder che devono contenere input e output e il nome file per l'ontologia." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "import csv\n", "import json\n", "\n", "# BASIC CONFIGURATION\n", "DATA_FOLDER = './data/'\n", "OUTPUT_FOLDER = './output/'\n", "ONTO_FILENAME = 'man_draft' # No extension!\n", "\n", "ent_filename = ONTO_FILENAME + '_entities.csv'\n", "rel_filename = ONTO_FILENAME + '_relations.csv'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### PARTE I: acquisizione dati\n", "Il passo successivo è la conversione del file .xlsx in due file .csv.\n", "\n", "Per descrivere l'ontologia in modo semplice, abbiamo adottato una descrizione 'a grafo': ci aspettiamo che il file .xlsx contenga (almeno) due fogli, uno che descrive le Entità (i nodi del grafo) ed uno le Relazioni (gli spigoli del grafo).\n", "\n", "La descrizione è forse ingenua, ma è semplice da capire anche per i non esperti e funzionale allo scopo.\n", "\n", "Le schede con Entità e Relazioni vengono ricercate in base al nome, che è configurabile. Tutto il loro contenuto viene esportato in file .csv senza nessuna elaborazione. La lettura usa la libreria **openpyxl**, che è l'unico pacchetto non-standard usando nel Notebook." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "# PART I: parse xlsx to (multiple) csv\n", "\n", "# CONFIGURATION\n", "XLSX_FILENAME = ONTO_FILENAME + '.xlsx'\n", "ENTITIES_SHEETNAME = 'Entità'\n", "RELATIONS_SHEETNAME = 'Relazioni'\n", "\n", "# Import xlsx through openpyxl\n", "import openpyxl as op\n", "\n", "input_data = op.load_workbook(DATA_FOLDER + XLSX_FILENAME)\n", "\n", "entities_sheet = input_data[ENTITIES_SHEETNAME]\n", "relations_sheet = input_data[RELATIONS_SHEETNAME]\n", "\n", "# Export sheet data to csv\n", "with open(DATA_FOLDER + ent_filename, 'w', encoding='utf-8') as out_file:\n", " writer = csv.writer(out_file)\n", " writer.writerows(entities_sheet.values)\n", "\n", "with open(DATA_FOLDER + rel_filename, 'w', encoding='utf-8') as out_file:\n", " writer = csv.writer(out_file)\n", " writer.writerows(relations_sheet.values)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### PARTE II: pre-processing dei dati\n", "Qui i dati dei CSV vengono letti e raccolti in una struttura leggermente più elaborata (più facile da riprocessare) che poi, per maggior visibilità, viene salvata in formato JSON. Il JSON in ogni caso è un file intermedio di lavoro che non ha nessuna importanza fondamentale, e può essere via via modificato in base alle esigenze.\n", "\n", "Non tutti i dati del CSV vengono letti, ma solo quelli corrispondenti a specifiche colonne, che possono essere definite all'inizio del parsing. I CSV possono avere o meno una intestazione (il booleano HEADER_ROW va settato di conseguenza).\n", "\n", "Se si preferisce, i CSV possono essere forniti direttamente dall'utente al posto del file .xlsx, e l'elaborazione può partire da qui saltando la Parte I." ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "# PART II: collect CSV data into a 'pre-ontology' structure\n", "\n", "# Read csv files produced by xlsx processing back in (OPTIONALLY: provide the csv files directly and start directly from here)\n", "# From here on, the main objects will be the 'entities' and 'relations' lists of dicts which arise from CSV re-reading.\n", "# To facilitate further processing, they will be arranged in a nested structure, which will be exported as a JSON file\n", "\n", "HEADER_ROW = True\n", "\n", "# Not difficult to add more keys (column names)\n", "ENTITIES_COLUMN_LABEL = 'ENTITÀ'\n", "ATTRIBUTES_COLUMN_LABEL = 'ATTRIBUTO (LITERAL)'\n", "SAMEAS_COLUMN_LABEL = 'SAME AS'\n", "#\n", "RELATION_FIRST_COLUMN_LABEL = 'ENTITÀ 1'\n", "RELATION_SECOND_COLUMN_LABEL = 'ENTITÀ 2'\n", "RELATION_NAME_COLUMN_LABEL = 'NOME RELAZIONE'\n", "INVERSE_RELATION_COLUMN_LABEL = 'NOME RELAZIONE INVERSA'\n", "#\n", "\n", "\n", "with open(DATA_FOLDER + ent_filename, 'r', encoding='utf-8') as in_file:\n", " if HEADER_ROW:\n", " reader = csv.DictReader(in_file)\n", " else:\n", " reader = csv.DictReader(in_file, fieldnames=[ENTITIES_COLUMN_LABEL, ATTRIBUTES_COLUMN_LABEL, SAMEAS_COLUMN_LABEL])\n", " entities = [row for row in reader]\n", "\n", "with open(DATA_FOLDER + rel_filename, 'r', encoding='utf-8') as in_file:\n", " if HEADER_ROW:\n", " reader = csv.DictReader(in_file)\n", " else:\n", " reader = csv.DictReader(in_file, fieldnames=[RELATION_FIRST_COLUMN_LABEL, RELATION_SECOND_COLUMN_LABEL, RELATION_NAME_COLUMN_LABEL, INVERSE_RELATION_COLUMN_LABEL])\n", " relations = [row for row in reader]\n", "\n", "\n", "# Main function for this Part\n", "def dict_lists_to_json(entities_local, relations_local):\n", "\n", " entity = {}\n", "\n", " current_entity = None\n", " for row in entities_local:\n", " entity_name = row.get(ENTITIES_COLUMN_LABEL)\n", " attribute_name = row.get(ATTRIBUTES_COLUMN_LABEL)\n", " same_as_row = row.get(SAMEAS_COLUMN_LABEL)\n", " same_as_list = same_as_row.split(',') if same_as_row else []\n", "\n", " if entity_name:\n", " current_entity = entity_name\n", " entity[current_entity] = {}\n", "\n", " if current_entity and attribute_name:\n", " if not entity[current_entity].get('Attributi'):\n", " entity[current_entity]['Attributi'] = []\n", " entity[current_entity]['Attributi'].append(attribute_name)\n", "\n", " if current_entity and same_as_list:\n", " entity[current_entity]['Sinonimi'] = [s.strip() for s in same_as_list]\n", "\n", " # Add subclass information\n", " for row in relations_local:\n", " entity1 = row.get(RELATION_FIRST_COLUMN_LABEL)\n", " entity2 = row.get(RELATION_SECOND_COLUMN_LABEL)\n", " label = row.get(RELATION_NAME_COLUMN_LABEL)\n", "\n", " if label == \"is_subclass_of\":\n", " if entity1 in entity:\n", " entity[entity1][\"Sottoclasse di\"] = entity2\n", "\n", " # Construct relations\n", " entity_relations = []\n", " for row in relations_local:\n", " if row[RELATION_NAME_COLUMN_LABEL] != \"is_subclass_of\":\n", " relation = {\n", " \"Entità 1\": row[RELATION_FIRST_COLUMN_LABEL],\n", " \"Entità 2\": row[RELATION_SECOND_COLUMN_LABEL],\n", " \"Etichetta\": row[RELATION_NAME_COLUMN_LABEL],\n", " \"Inversa\": row[INVERSE_RELATION_COLUMN_LABEL]\n", " }\n", " entity_relations.append(relation)\n", "\n", " # Create final object structure\n", " data = {\n", " \"Entità\": entity,\n", " \"Relazioni\": entity_relations\n", " }\n", "\n", " return data" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "# Do it!\n", "json_data = dict_lists_to_json(entities, relations)\n", "\n", "# Export data\n", "with open(OUTPUT_FOLDER + ONTO_FILENAME + '.json', 'w') as out_json:\n", " json.dump(json_data, out_json, indent=2, ensure_ascii=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### PARTE III: produzione del file RDF\n", "L'ultima parte dell'elaborazione usa l'oggetto strutturato prodotto dalla Parte II per creare un file RDF/XML valido (validato usando un'istanza pubblica di WEBVOWL) che descrive formalmente l'ontologia in corso di validazione.\n", "\n", "L'approccio è molto basico/ingenuo: si utilizza un template pre-generato (con l'aiuto del software Protegé) e si sostituiscono specifiche stringhe con le informazione ricavate dal JSON. L'idea era di produrre un esempio minimale funzionante, più che qualcosa di definitivo o particolarmente elaborato." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'#any'}\n" ] } ], "source": [ "# PART III: from the structured entity/relation data obtained from part II, generate a RDF file describing the ontology\n", "\n", "# Re-read the data and do a consistency check\n", "entity_set = set(json_data['Entità'].keys())\n", "\n", "entity_relations_set = {ent for rel in json_data['Relazioni'] for ent in [rel['Entità 1'], rel['Entità 2']]}\n", "\n", "# The check: ideally, all entities mentioned in the 'relations' object should be present in the 'entities' object, and the output of the following block should be null\n", "if not entity_relations_set.issubset(entity_set):\n", " print(entity_relations_set.difference(entity_set))\n", "\n", "# However, we actually used an extra placeholder in 'relations', '#any', which stands for any entity (any class); the expected output is thus a list containing only '#any'" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "# RDF Templates\n", "RDF_MAIN_TEMPLATE = 'template.rdf'\n", "with open(DATA_FOLDER + RDF_MAIN_TEMPLATE, 'r') as in_file:\n", " RAW_RDF = in_file.read()\n", "\n", "# RDF snippets; info will replace placeholder tags (in uppercase between '#')\n", "ENTITY_TEMPLATE = '''\n", " \n", "\n", " \n", " #LABEL#\n", " #PARENT#\n", " \n", "'''\n", "SUBCLASS_STRING = \" #PARENT#\\n\"\n", "\n", "OBJECT_PROPERTY_TEMPLATE = '''\n", " \n", "\n", " \n", " #LABEL#\n", " \n", " \n", " \n", "'''\n", "\n", "OBJECT_PROPERTY_INVERSE_TEMPLATE = '''\n", " \n", "\n", " \n", " #LABEL#\n", " \n", " \n", " \n", " \n", "'''\n", "\n", "DATATYPE_PROPERTY_TEMPLATE = '''\n", " \n", "\n", " \n", " #LABEL#\n", " \n", " \n", "'''" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "# Utility\n", "def normalize_label(label):\n", " return label.lower().replace(' ', '_').replace('à', 'a').replace('è', 'e').replace('é', 'e').replace('ì', 'i').replace('ò', 'o').replace('ù', 'u')\n", "\n", "\n", "# Main function for this part\n", "def create_rdf(data):\n", " entities_rdf_list = []\n", " datatype_properties_rdf_list = []\n", " for label, ent in data['Entità'].items():\n", " \n", " entity_name = normalize_label(label)\n", " entity_rdf = ENTITY_TEMPLATE.replace('#LABEL#', label).replace('#NAME#', entity_name)\n", "\n", " # Subclasses\n", " if 'Sottoclasse di' in ent.keys():\n", " parent = ent['Sottoclasse di']\n", " data['Relazioni'].append({\"Entità 1\": label,\n", " \"Entità 2\": parent,\n", " \"Etichetta\": \"is_subclass_of\", \"Inversa\": \"is_superclass_of\"})\n", " entity_rdf = entity_rdf.replace('#PARENT#', normalize_label(parent))\n", " else:\n", " entity_rdf = entity_rdf.replace(SUBCLASS_STRING, '')\n", "\n", " entities_rdf_list.append(entity_rdf)\n", " \n", " if not ent.get('Attributi'):\n", " continue\n", " for datatype_label in ent['Attributi']:\n", " datatype_name = normalize_label(datatype_label)\n", " datatype_properties_rdf_list.append(\n", " DATATYPE_PROPERTY_TEMPLATE.replace('#LABEL#', datatype_label).replace(\n", " '#NAME#', datatype_name\n", " ).replace('#DOMAIN#', entity_name)\n", " )\n", "\n", " relations_rdf_list = []\n", " for rel in data['Relazioni']:\n", " label = rel['Etichetta']\n", " inverse_label = rel['Inversa']\n", " domain = normalize_label(rel['Entità 1'])\n", " range1 = normalize_label(rel['Entità 2'])\n", " name = domain + '_' + normalize_label(label) + '_' + range1\n", " inverse_name = range1 + '_' + normalize_label(inverse_label) + '_' + domain\n", " #\n", " relation_rdf = OBJECT_PROPERTY_TEMPLATE.replace('#NAME#', name).replace('#LABEL#', label).replace('#DOMAIN#', domain).replace('#RANGE#', range1)\n", " #\n", " relation_inverse_rdf = OBJECT_PROPERTY_INVERSE_TEMPLATE.replace('#NAME#', inverse_name).replace('#LABEL#', inverse_label).replace('#DOMAIN#', range1).replace('#RANGE#', domain).replace('#INV#', name)\n", " #\n", " relation_full_rdf = relation_rdf + '\\n\\n\\n' + relation_inverse_rdf\n", " relations_rdf_list.append(relation_full_rdf)\n", " \n", " to_out = RAW_RDF.replace(ENTITY_TEMPLATE, '\\n\\n\\n'.join(entities_rdf_list)).replace(DATATYPE_PROPERTY_TEMPLATE, '\\n\\n\\n'.join(datatype_properties_rdf_list)\n", " ).replace(OBJECT_PROPERTY_INVERSE_TEMPLATE, '\\n\\n\\n'.join(relations_rdf_list))\n", "\n", " return to_out\n" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "# Do it!\n", "rdf_data = create_rdf(json_data)\n", "\n", "# Export\n", "with open(OUTPUT_FOLDER + ONTO_FILENAME + '.rdf', 'w') as out_file:\n", " out_file.write(rdf_data)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### PARTE IV: il file turtle\n", "Non abbiamo scritto codice per creare il .ttl -- questo resta tra i desiderata -- ma un file turtle può essere abbastanza facilmente generato usando software di terze parti, ad esempio l'istanza pubblica di WEBVOWL presente all'indirizzo\n", "\n", "https://service.tib.eu/webvowl/ ,\n", "\n", "il che serve anche per validare l'output prodotto." ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 2 }