Download Wikidata_Text_Parser.py from Jongmo/testing: direct link, hf CLI and curl.
- Browser
- Download file 40 kB
-
https://huggingface.co/spaces/Jongmo/testing/resolve/main/Wikidata_Text_Parser.py
- Command line
-
hf download hf://spaces/Jongmo/testing/Wikidata_Text_Parser.py
-
curl -L -o Wikidata_Text_Parser.py https://huggingface.co/spaces/Jongmo/testing/resolve/main/Wikidata_Text_Parser.py
40 kB
| import numpy as np | |
| from tqdm import tqdm | |
| import pandas as pd | |
| import os, sqlite3, traceback, ast, requests, fasttext, re, time, string, spacy, pysbd | |
| from requests.exceptions import ReadTimeout, TooManyRedirects, ConnectionError, ConnectTimeout, InvalidSchema, InvalidURL | |
| from qwikidata.linked_data_interface import get_entity_dict_from_api | |
| from datetime import datetime | |
| import utils.wikidata_utils as wdutils | |
| from importlib import reload | |
| from urllib.parse import urlparse, unquote | |
| from urllib import parse | |
| from bs4 import BeautifulSoup | |
| from IPython.display import clear_output | |
| from os.path import exists | |
| from pathlib import Path | |
| from nltk.tokenize import sent_tokenize | |
| from sentence_splitter import SentenceSplitter, split_text_into_sentences | |
| import nltk | |
| nltk.download('punkt') | |
| class DatabaseExtractor(): | |
| def __init__(self, dbname='wikidata_claims_refs_parsed.db'): | |
| self.dbname = dbname | |
| self.prepare_extraction() | |
| def finish_extraction(self): | |
| self.db.commit() | |
| def prepare_extraction(self): | |
| self.db = sqlite3.connect(self.dbname) | |
| self.cursor = self.db.cursor() | |
| self.cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS claims( | |
| entity_id TEXT, | |
| claim_id TEXT, | |
| rank TEXT, | |
| property_id TEXT, | |
| datatype TEXT, | |
| datavalue TEXT, | |
| PRIMARY KEY ( | |
| claim_id | |
| ) | |
| )''') | |
| self.cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS claims_refs( | |
| claim_id TEXT, | |
| reference_id TEXT, | |
| PRIMARY KEY ( | |
| claim_id, | |
| reference_id | |
| ) | |
| )''') | |
| self.cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS refs( | |
| reference_id TEXT, | |
| reference_property_id TEXT, | |
| reference_index TEXT, | |
| reference_datatype TEXT, | |
| reference_value TEXT, | |
| PRIMARY KEY ( | |
| reference_id, | |
| reference_property_id, | |
| reference_index | |
| ) | |
| )''') | |
| self.db.commit() | |
| def extract_claim(self, entity_id, claim): | |
| if claim['mainsnak']['snaktype'] == 'value': | |
| value = str(claim['mainsnak']['datavalue']) | |
| else: | |
| value = claim['mainsnak']['snaktype'] | |
| try: | |
| self.cursor.execute(''' | |
| INSERT INTO claims(entity_id, claim_id, rank, property_id, datatype, datavalue) | |
| VALUES($var,$var,$var,$var,$var,$var)'''.replace('$var','?'), ( | |
| entity_id,claim['id'],claim['rank'], | |
| claim['mainsnak']['property'],claim['mainsnak']['datatype'],value | |
| )) | |
| except UnicodeEncodeError: | |
| print(entity_id,claim['id'],claim['rank'], | |
| claim['mainsnak']['property'],claim['mainsnak']['datatype'],value) | |
| raise | |
| except sqlite3.IntegrityError as err: | |
| #self.db.rollback() | |
| self.cursor.execute( | |
| '''SELECT * | |
| FROM claims | |
| WHERE claim_id=$var | |
| '''.replace('$var','?'), (claim['id'],) | |
| ) | |
| conflicted_value = self.cursor.fetchone() | |
| if conflicted_value == (entity_id,claim['id'],claim['rank'], | |
| claim['mainsnak']['property'],claim['mainsnak']['datatype'],value): | |
| pass | |
| else: | |
| print(err, claim['id']) | |
| traceback.print_exc() | |
| raise err | |
| finally: | |
| #self.db.commit() | |
| pass | |
| def extract_reference(self, ref): | |
| for snaks in ref['snaks'].values(): | |
| for i, snak in enumerate(snaks): | |
| if snak['snaktype'] == 'value': | |
| value = str(snak['datavalue']) | |
| else: | |
| value = snak['snaktype'] | |
| try: | |
| self.cursor.execute(''' | |
| INSERT INTO refs(reference_id, reference_property_id, reference_index, | |
| reference_datatype, reference_value) | |
| VALUES($var,$var,$var,$var,$var)'''.replace('$var','?'), ( | |
| ref['hash'],snak['property'],str(i),snak['datatype'],value | |
| )) | |
| except sqlite3.IntegrityError as err: | |
| #self.db.rollback() | |
| self.cursor.execute(# WE DONT USE THE INDEX HERE, THEY TEND TO COME SHUFFLED FROM API AND SORTING TAKES TOO LONG | |
| '''SELECT reference_id, reference_property_id, reference_datatype, reference_value | |
| FROM refs | |
| WHERE reference_id = $var | |
| AND reference_property_id = $var | |
| '''.replace('$var','?'), (ref['hash'],snak['property']) | |
| ) | |
| conflicted_values = self.cursor.fetchall() | |
| if (ref['hash'],snak['property'],snak['datatype'],value) in conflicted_values: | |
| pass | |
| else: | |
| print(err, ref['hash'],snak['property'],i) | |
| print('trying to insert:',(ref['hash'],snak['property'],str(i),snak['datatype'],value)) | |
| traceback.print_exc() | |
| raise err | |
| finally: | |
| #self.db.commit() | |
| pass | |
| def extract_claim_reference(self, claim, ref): | |
| claim['id'],ref['hash'] | |
| try: | |
| self.cursor.execute(''' | |
| INSERT INTO claims_refs(claim_id, reference_id) | |
| VALUES($var,$var)'''.replace('$var','?'), ( | |
| claim['id'],ref['hash'] | |
| )) | |
| except sqlite3.IntegrityError as err: | |
| #db.rollback() | |
| pass | |
| finally: | |
| #self.db.commit() | |
| pass | |
| def extract_entity(self, e): | |
| for outgoing_property_id in e['claims'].values(): | |
| for claim in outgoing_property_id: | |
| self.extract_claim(e['id'],claim) | |
| if 'references' in claim: | |
| for ref in claim['references']: | |
| self.extract_claim_reference(claim, ref) | |
| self.extract_reference(ref) | |
| def claimParser(QID): | |
| entity_id = QID | |
| print('Setting up database ...') | |
| extractor = DatabaseExtractor() | |
| print('Fetching entity from API ...') | |
| entity = get_entity_dict_from_api(entity_id) | |
| if entity: | |
| print(f'Parsing entity: {entity_id}') | |
| extractor.extract_entity(entity) | |
| else: | |
| print(f'Failed to fetch entity: {entity_id}') | |
| extractor.finish_extraction() | |
| def propertyFiltering(QID): | |
| reload(wdutils) | |
| DB_PATH = 'wikidata_claims_refs_parsed.db' | |
| claims_columns = ['entity_id','claim_id','rank','property_id','datatype','datavalue'] | |
| properties_to_remove = { | |
| 'general':[ | |
| 'P31', # - instance of | |
| 'P279',# - subclass of | |
| 'P373',# - commons category | |
| 'P910',# - Topic's main category | |
| 'P7561',# - category for the interior of the item | |
| 'P5008',# - on focus list of Wikimedia project | |
| 'P2670',# - has parts of the class | |
| 'P1740',# - category for films shot at this location | |
| 'P1612',# - Commons Institution page | |
| 'P8989',# - category for the view of the item | |
| 'P2959',# - permanent duplicated item | |
| 'P7867',# - category for maps | |
| 'P935' ,# - Commons gallery | |
| 'P1472',# - Commons Creator page | |
| 'P8596',# category for the exterior of the item | |
| 'P5105',# Deutsche Bahn station category | |
| 'P8933',# category for the view from the item | |
| 'P642',# of | |
| 'P3876',# category for alumni of educational institution | |
| 'P1791',# category of people buried here | |
| 'P7084',# related category | |
| 'P1465',# category for people who died here | |
| 'P1687',# Wikidata property | |
| 'P6104',# maintained by WikiProject | |
| 'P4195',# category for employees of the organization | |
| 'P1792',# category of associated people | |
| 'P5869',# model item | |
| 'P1659',# see also | |
| 'P1464',# category for people born here | |
| 'P2354',# has list | |
| 'P1424',# topic's main template | |
| 'P7782',# category for ship name | |
| 'P179',# part of the series | |
| 'P7888',# merged into | |
| 'P6365',# member category | |
| 'P8464',# content partnership category | |
| 'P360',# is a list of | |
| 'P805',# statement is subject of | |
| 'P8703',# entry in abbreviations table | |
| 'P1456',# list of monuments | |
| 'P1012',# including | |
| 'P1151',# topic's main Wikimedia portal | |
| 'P2490',# page at OSTIS Belarus Wiki | |
| 'P593',# HomoloGene ID | |
| 'P8744',# economy of topic | |
| 'P2614',# World Heritage criteria | |
| 'P2184',# history of topic | |
| 'P9241',# demographics of topic | |
| 'P487',#Unicode character | |
| 'P1754',#category related to list | |
| 'P2559',#Wikidata usage instructions | |
| 'P2517',#category for recipients of this award | |
| 'P971',#category combines topics | |
| 'P6112',# category for members of a team | |
| 'P4224',#category contains | |
| 'P301',#category's main topic | |
| 'P1753',#list related to category | |
| 'P1423',#template has topic | |
| 'P1204',#Wikimedia portal's main topic | |
| 'P3921',#Wikidata SPARQL query equivalent | |
| 'P1963',#properties for this type | |
| 'P5125',#Wikimedia outline | |
| 'P3176',#uses property | |
| 'P8952',#inappropriate property for this type | |
| 'P2306',#property | |
| 'P5193',#Wikidata property example for forms | |
| 'P5977',#Wikidata property example for senses | |
| ], | |
| 'specific': {} | |
| } | |
| db = sqlite3.connect(DB_PATH) | |
| cursor = db.cursor() | |
| # To see how many out of the total number of stored claims we are excluding by removing the general properties | |
| sql_query = "select count(*) from claims where property_id in $1;" | |
| sql_query = sql_query.replace('$1', '(' + ','.join([('"' + e + '"') for e in properties_to_remove['general']]) + ')') | |
| cursor.execute(sql_query) | |
| print('Removing the',len(properties_to_remove['general']),'properties deemed as ontological or unverbalisable') | |
| cursor = db.cursor() | |
| sql_query = "select * from claims where entity_id in $1;" | |
| sql_query = sql_query.replace('$1', '(' + ','.join([('"' + e + '"') for e in [QID]]) + ')') | |
| cursor.execute(sql_query) | |
| theme_df = pd.DataFrame(cursor.fetchall()) | |
| theme_df.columns = claims_columns | |
| original_theme_df_size = theme_df.shape[0] | |
| last_stage_theme_df_size = original_theme_df_size | |
| print('- Removing deprecated') | |
| # Remove deprecated | |
| theme_df = theme_df[theme_df['rank'] != 'deprecated'].reset_index(drop=True) | |
| print( | |
| ' - Percentage of deprecated:', | |
| round((last_stage_theme_df_size-theme_df.shape[0])/original_theme_df_size*100, 2), '%' | |
| ) | |
| last_stage_theme_df_size = theme_df.shape[0] | |
| print('- Removing bad datatypes') | |
| # Remove external_ids, commonsMedia (e.g. photos), globe-coordinates, urls | |
| bad_datatypes = ['commonsMedia','external-id','globe-coordinate','url', 'wikibase-form', | |
| 'geo-shape', 'math', 'musical-notation', 'tabular-data', 'wikibase-sense'] | |
| theme_df = theme_df[ | |
| theme_df['datatype'].apply( | |
| lambda x : x not in bad_datatypes | |
| ) | |
| ].reset_index(drop=True) | |
| print( | |
| ' - Percentage of bad datatypes:', | |
| round((last_stage_theme_df_size-theme_df.shape[0])/original_theme_df_size*100, 2), '%' | |
| ) | |
| last_stage_theme_df_size = theme_df.shape[0] | |
| print('- Removing bad properties') | |
| # Remove specific properties such as P31 and P279 | |
| theme_df = theme_df[ | |
| theme_df['property_id'].apply( | |
| lambda x : (x not in properties_to_remove['general'])) | |
| ].reset_index(drop=True) | |
| print( | |
| ' - Percentage of ontology (non-domain) properties:', | |
| round((last_stage_theme_df_size-theme_df.shape[0])/original_theme_df_size*100, 2), '%' | |
| ) | |
| last_stage_theme_df_size = theme_df.shape[0] | |
| print('- Removing somevalue/novalue') | |
| # Remove novalue and somevalue | |
| theme_df = theme_df[ | |
| theme_df['datavalue'].apply( | |
| lambda x : x not in ['somevalue', 'novalue'] | |
| ) | |
| ].reset_index(drop=True) | |
| print( | |
| ' - Percentage of somevalue/novalue:', | |
| round((last_stage_theme_df_size-theme_df.shape[0])/original_theme_df_size*100, 2), '%' | |
| ) | |
| last_stage_theme_df_size = theme_df.shape[0] | |
| print( | |
| 'After all removals, we keep', | |
| round(last_stage_theme_df_size/original_theme_df_size*100, 2), | |
| ) | |
| theme_df.to_sql('claims', db, if_exists='replace', index=False) | |
| return theme_df | |
| def get_object_label_given_datatype(row): | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| def turn_to_century_or_millennium(y, mode): | |
| y = str(y) | |
| if mode == 'C': | |
| div = 100 | |
| group = int(y.rjust(3, '0')[:-2]) | |
| mode_name = 'century' | |
| elif mode == 'M': | |
| div = 1000 | |
| group = int(y.rjust(4, '0')[:-3]) | |
| mode_name = 'millenium' | |
| else: | |
| raise ValueError('Use mode = C for century and M for millennium') | |
| if int(y)%div != 0: | |
| group += 1 | |
| group = str(group) | |
| group_suffix = ( | |
| 'st' if group[-1] == '1' else ( | |
| 'nd' if group[-1] == '2' else ( | |
| 'rd' if group[-1] == '3' else 'th' | |
| ) | |
| ) | |
| ) | |
| return ' '.join([group+group_suffix, mode_name]) | |
| dt = row['datatype'] | |
| dv = row['datavalue'] | |
| dt_types = ['wikibase-item', 'monolingualtext', 'quantity', 'time', 'string'] | |
| if dt not in dt_types: | |
| print(dt) | |
| raise ValueError | |
| else: | |
| try: | |
| if dt == dt_types[0]: | |
| return Wd_API.get_label(ast.literal_eval(dv)['value']['id'], True) #get label here | |
| elif dt == dt_types[1]: | |
| dv = ast.literal_eval(dv) | |
| return (dv['value']['text'], dv['value']['language']) | |
| elif dt == dt_types[2]: | |
| dv = ast.literal_eval(dv) | |
| amount, unit = dv['value']['amount'], dv['value']['unit'] | |
| if amount[0] == '+': | |
| amount = amount[1:] | |
| if str(unit) == '1': | |
| return (str(amount), 'en') | |
| else: | |
| unit_entity_id = unit.split('/')[-1] | |
| unit = Wd_API.get_label(unit_entity_id, True)#get label here | |
| return (' '.join([amount, unit[0]]), unit[1]) | |
| elif dt == dt_types[3]: | |
| dv = ast.literal_eval(dv) | |
| time = dv['value']['time'] | |
| timezone = dv['value']['timezone'] | |
| precision = dv['value']['precision'] | |
| assert dv['value']['after'] == 0 and dv['value']['before'] == 0 | |
| sufix = 'BC' if time[0] == '-' else '' | |
| time = time[1:] | |
| if precision == 11: #date | |
| return (datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%d/%m/%Y') + sufix, 'en') | |
| elif precision == 10: #month | |
| try: | |
| return (datetime.strptime(time, '%Y-%m-00T00:00:%SZ').strftime("%B of %Y") + sufix, 'en') | |
| except ValueError: | |
| return (datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime("%B of %Y") + sufix, 'en') | |
| elif precision == 9: #year | |
| try: | |
| return (datetime.strptime(time, '%Y-00-00T00:00:%SZ').strftime('%Y') + sufix, 'en') | |
| except ValueError: | |
| return (datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%Y') + sufix, 'en') | |
| elif precision == 8: #decade | |
| try: | |
| return (datetime.strptime(time, '%Y-00-00T00:00:%SZ').strftime('%Y')[:-1] +'0s' + sufix, 'en') | |
| except ValueError: | |
| return (datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%Y')[:-1] +'0s' + sufix, 'en') | |
| elif precision == 7: #century | |
| try: | |
| parsed_time = datetime.strptime(time, '%Y-00-00T00:00:%SZ') | |
| except ValueError: | |
| parsed_time = datetime.strptime(time, '%Y-%m-%dT00:00:%SZ') | |
| finally: | |
| return (turn_to_century_or_millennium( | |
| parsed_time.strftime('%Y'), mode='C' | |
| ) + sufix, 'en') | |
| elif precision == 6: #millennium | |
| try: | |
| parsed_time = datetime.strptime(time, '%Y-00-00T00:00:%SZ') | |
| except ValueError: | |
| parsed_time = datetime.strptime(time, '%Y-%m-%dT00:00:%SZ') | |
| finally: | |
| return (turn_to_century_or_millennium( | |
| parsed_time.strftime('%Y'), mode='M' | |
| ) + sufix, 'en') | |
| elif precision == 4: #hundred thousand years | |
| timeint = int(datetime.strptime(time, '%Y-00-00T00:00:%SZ').strftime('%Y')) | |
| timeint = round(timeint/1e5,1) | |
| return (str(timeint) + 'hundred thousand years' + sufix, 'en') | |
| elif precision == 3: #million years | |
| timeint = int(datetime.strptime(time, '%Y-00-00T00:00:%SZ').strftime('%Y')) | |
| timeint = round(timeint/1e6,1) | |
| return (str(timeint) + 'million years' + sufix, 'en') | |
| elif precision == 0: #billion years | |
| timeint = int(datetime.strptime(time, '%Y-00-00T00:00:%SZ').strftime('%Y')) | |
| timeint = round(timeint/1e9,1) | |
| return (str(timeint) + 'billion years' +sufix, 'en') | |
| elif dt == dt_types[4]: | |
| return (ast.literal_eval(dv)['value'], 'en') | |
| except ValueError as e: | |
| #pdb.set_trace() | |
| raise e | |
| def get_object_desc_given_datatype(row): | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| dt = row['datatype'] | |
| dv = row['datavalue'] | |
| dt_types = ['wikibase-item', 'monolingualtext', 'quantity', 'time', 'string'] | |
| if dt not in dt_types: | |
| print(dt) | |
| raise ValueError | |
| else: | |
| try: | |
| if dt == dt_types[0]: | |
| return Wd_API.get_desc(ast.literal_eval(dv)['value']['id']) #get label here | |
| elif dt == dt_types[1]: | |
| return ('no-desc', 'none') | |
| elif dt == dt_types[2]: | |
| dv = ast.literal_eval(dv) | |
| amount, unit = dv['value']['amount'], dv['value']['unit'] | |
| if amount[0] == '+': | |
| amount = amount[1:] | |
| if str(unit) == '1': | |
| return ('no-desc', 'none') | |
| else: | |
| unit_entity_id = unit.split('/')[-1] | |
| return Wd_API.get_desc(unit_entity_id) | |
| elif dt == dt_types[3]: | |
| return ('no-desc', 'none') | |
| elif dt == dt_types[4]: | |
| return ('no-desc', 'none') | |
| except ValueError as e: | |
| #pdb.set_trace() | |
| raise e | |
| def get_object_alias_given_datatype(row): | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| dt = row['datatype'] | |
| dv = row['datavalue'] | |
| dt_types = ['wikibase-item', 'monolingualtext', 'quantity', 'time', 'string'] | |
| if dt not in dt_types: | |
| print(dt) | |
| raise ValueError | |
| else: | |
| try: | |
| if dt == dt_types[0]: | |
| return Wd_API.get_alias(ast.literal_eval(dv)['value']['id']) #get label here | |
| elif dt == dt_types[1]: | |
| return ('no-alias', 'none') | |
| elif dt == dt_types[2]: | |
| dv = ast.literal_eval(dv) | |
| amount, unit = dv['value']['amount'], dv['value']['unit'] | |
| if amount[0] == '+': | |
| amount = amount[1:] | |
| if str(unit) == '1': | |
| return ('no-alias', 'none') | |
| else: | |
| unit_entity_id = unit.split('/')[-1] | |
| return Wd_API.get_alias(unit_entity_id) | |
| elif dt == dt_types[3]: | |
| dv = ast.literal_eval(dv) | |
| time = dv['value']['time'] | |
| timezone = dv['value']['timezone'] | |
| precision = dv['value']['precision'] | |
| assert dv['value']['after'] == 0 and dv['value']['before'] == 0 | |
| sufix = 'BC' if time[0] == '-' else '' | |
| time = time[1:] | |
| if precision == 11: #date | |
| return ([ | |
| datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%-d of %B, %Y') + sufix, | |
| datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%d/%m/%Y (dd/mm/yyyy)') + sufix, | |
| datetime.strptime(time, '%Y-%m-%dT00:00:%SZ').strftime('%b %-d, %Y') + sufix | |
| ], 'en') | |
| else: #month | |
| return ('no-alias', 'none') | |
| elif dt == dt_types[4]: | |
| return ('no-alias', 'none') | |
| except ValueError as e: | |
| #pdb.set_trace() | |
| raise e | |
| def textualAugmentation(filtered_df): | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| filtered_df['entity_label'] = filtered_df['entity_id'].apply(lambda x: Wd_API.get_label(x, True)) | |
| filtered_df['entity_desc'] = filtered_df['entity_id'].apply(lambda x: Wd_API.get_desc(x)) | |
| filtered_df['entity_alias'] = filtered_df['entity_id'].apply(lambda x: Wd_API.get_alias(x)) | |
| print(' - Predicate augmentation...') | |
| filtered_df['property_label'] = filtered_df['property_id'].apply(lambda x: Wd_API.get_label(x, True)) | |
| filtered_df['property_desc'] = filtered_df['property_id'].apply(lambda x: Wd_API.get_desc(x)) | |
| filtered_df['property_alias'] = filtered_df['property_id'].apply(lambda x: Wd_API.get_alias(x)) | |
| print(' - Object augmentation...') | |
| filtered_df['object_label'] = filtered_df.apply(get_object_label_given_datatype, axis=1) | |
| filtered_df['object_desc'] = filtered_df.apply(get_object_desc_given_datatype, axis=1) | |
| filtered_df['object_alias'] = filtered_df.apply(get_object_alias_given_datatype, axis=1) | |
| no_subject_label_perc = filtered_df[filtered_df['entity_label'].apply(lambda x: x[0] == 'no-label')].shape[0] / filtered_df.shape[0] * 100 | |
| print(' - No subject label %:', no_subject_label_perc, '%') | |
| no_predicate_label_perc = filtered_df[filtered_df['property_label'].apply(lambda x: x[0] == 'no-label')].shape[0] / filtered_df.shape[0] * 100 | |
| print(' - No predicate label %:', no_predicate_label_perc, '%') | |
| no_object_label_perc = filtered_df[filtered_df['object_label'].apply(lambda x: x[0] == 'no-label')].shape[0] / filtered_df.shape[0] * 100 | |
| print(' - No object label %:', no_object_label_perc, '%') | |
| return filtered_df | |
| def urlParser(target_QID): | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| db = sqlite3.connect('wikidata_claims_refs_parsed.db') | |
| cursor = db.cursor() | |
| refs_columns = ['reference_id','reference_property_id', 'reference_index', 'reference_datatype', 'reference_value'] | |
| cursor.execute('select * from refs where reference_datatype="url";') | |
| url_df = pd.DataFrame(cursor.fetchall()) | |
| url_df.columns = refs_columns | |
| def reference_value_to_url(reference_value): | |
| if reference_value in ['novalue','somevalue']: | |
| return reference_value | |
| reference_value = ast.literal_eval(reference_value) | |
| assert reference_value['type'] == 'string' | |
| return reference_value['value'] | |
| def reference_value_to_external_id(reference_value): | |
| if reference_value in ['novalue','somevalue']: | |
| return reference_value | |
| reference_value = ast.literal_eval(reference_value) | |
| assert reference_value['type'] == 'string' | |
| return reference_value['value'] | |
| def get_formatter_url(entity_id): | |
| try: | |
| sparql_query = ''' | |
| SELECT ?item ?itemLabel | |
| WHERE | |
| { | |
| wd:$1 wdt:P1630 ?item. | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } | |
| } | |
| '''.replace('$1',entity_id) | |
| sparql_results = Wd_API.query_sparql_endpoint(sparql_query) | |
| if len(sparql_results['results']['bindings']) > 0: | |
| return sparql_results['results']['bindings'][0]['item']['value'] | |
| else: | |
| return 'no_formatter_url' | |
| except Exception: | |
| print(entity_id) | |
| print(sparql_results) | |
| raise | |
| url_df['url'] = url_df.reference_value.apply(reference_value_to_url) | |
| cursor.execute('select * from refs where reference_datatype="url";') | |
| ext_id_df = pd.DataFrame(cursor.fetchall()) | |
| ext_id_df.columns = refs_columns | |
| ext_id_df['ext_id'] = ext_id_df.reference_value.apply(reference_value_to_external_id) | |
| ext_id_df['formatter_url'] = ext_id_df['reference_property_id'].apply(get_formatter_url) | |
| ext_id_df['url'] = ext_id_df.apply(lambda x : x['formatter_url'].replace('$1', x['ext_id']), axis=1) | |
| columns_for_join = ['reference_id', 'reference_property_id','reference_index','reference_datatype','url'] | |
| url_df_pre_join = url_df[columns_for_join] | |
| ext_id_df_pre_join = ext_id_df[columns_for_join] | |
| all_url_df = pd.concat([url_df_pre_join,ext_id_df_pre_join]) | |
| all_url_df = all_url_df.sort_values(['reference_id','reference_index']) | |
| # drop those with url = 'no_formatter_url' | |
| all_url_df = all_url_df[all_url_df['url'] != 'no_formatter_url'].reset_index(drop=True) | |
| # drop those with url = somevalue and novalue | |
| all_url_df = all_url_df[~all_url_df['url'].isin(['somevalue','novalue'])] | |
| reference_id_counts = all_url_df.reference_id.value_counts().reset_index() | |
| reference_id_counts.columns = ['reference_id', 'counts'] | |
| reference_id_counts_equal_1 = reference_id_counts[reference_id_counts['counts'] == 1].reference_id.tolist() | |
| all_url_df_eq1 = all_url_df[all_url_df.reference_id.isin(reference_id_counts_equal_1)] | |
| all_url_df_eq1 = all_url_df_eq1.reset_index(drop=True).drop('reference_index', axis=1) | |
| return all_url_df_eq1 | |
| def htmlParser(url_set, qid): | |
| text_reference_sampled_df = url_set | |
| _RE_COMBINE_WHITESPACE = re.compile(r"\s+") | |
| text_reference_sampled_df['html'] = None | |
| for i, row in text_reference_sampled_df.iterrows(): | |
| print(i, row.url) | |
| try: | |
| response = requests.get(row.url, timeout=10) | |
| if response.status_code == 200: | |
| html = response.text | |
| text_reference_sampled_df.loc[i, 'html'] = html | |
| else: | |
| print(f"not response, {response.status_code}") | |
| text_reference_sampled_df.loc[i, 'html'] = response.status_code | |
| except requests.exceptions.Timeout: | |
| print("Timeout occurred while fetching the URL:", row.url) | |
| text_reference_sampled_df.loc[i, 'html'] = 'TimeOut' | |
| pass | |
| except Exception as e: | |
| print("An error occurred:", str(e)) | |
| pass | |
| text_reference_sampled_df_html = text_reference_sampled_df.copy() | |
| text_reference_sampled_df_html['entity_id'] = qid | |
| return text_reference_sampled_df_html | |
| def claim2text(html_set): | |
| text_reference_sampled_df_html = html_set | |
| Wd_API = wdutils.CachedWikidataAPI() | |
| Wd_API.languages = ['en'] | |
| db = sqlite3.connect('wikidata_claims_refs_parsed.db') | |
| cursor = db.cursor() | |
| claims_columns = ['entity_id','claim_id','rank','property_id','datatype','datavalue'] | |
| refs_columns = ['reference_id', 'reference_property_id', 'reference_index', 'reference_datatype', 'reference_value'] | |
| def reference_id_to_claim_id(reference_id): | |
| cursor.execute(f'select claim_id from claims_refs where reference_id="{reference_id}"') | |
| sql_result = cursor.fetchall() | |
| #return sql_result | |
| randomly_chosen_claim_id = np.array(sql_result).reshape(-1) | |
| return randomly_chosen_claim_id | |
| def reference_id_to_claim_data(reference_id): | |
| claim_ids = reference_id_to_claim_id(reference_id) | |
| r = [] | |
| for claim_id in claim_ids: | |
| #print(claim_id) | |
| cursor.execute(f'select * from claims where claim_id="{claim_id}";') | |
| d = cursor.fetchall() | |
| r = r + d | |
| return r | |
| claim_data = [] | |
| for reference_id in text_reference_sampled_df_html.reference_id: | |
| data = reference_id_to_claim_data(reference_id) | |
| #print(data) | |
| data = [(reference_id,) + t for t in data] | |
| claim_data = claim_data + data | |
| #break | |
| claim_df = pd.DataFrame(claim_data, columns = ['reference_id'] + claims_columns) | |
| claim_df | |
| def claim_id_to_claim_url(claim_id): | |
| claim_id_parts = claim_id.split('$') | |
| return f'https://www.wikidata.org/wiki/{claim_id_parts[0]}#{claim_id}' | |
| BAD_DATATYPES = ['external-id','commonsMedia','url', 'globe-coordinate', 'wikibase-lexeme', 'wikibase-property'] | |
| assert claim_df[~claim_df.datatype.isin(BAD_DATATYPES)].reference_id.unique().shape\ | |
| == claim_df.reference_id.unique().shape | |
| print(claim_df.reference_id.unique().shape[0]) | |
| claim_df = claim_df[~claim_df.datatype.isin(BAD_DATATYPES)].reset_index(drop=True) | |
| from tqdm.auto import tqdm | |
| tqdm.pandas() | |
| claim_df[['entity_label','entity_label_lan']] = pd.DataFrame( | |
| claim_df.entity_id.progress_apply(Wd_API.get_label, non_language_set=True).tolist() | |
| ) | |
| claim_df[['property_label','property_label_lan']] = pd.DataFrame( | |
| claim_df.property_id.progress_apply(Wd_API.get_label, non_language_set=True).tolist() | |
| ) | |
| claim_df[['entity_alias','entity_alias_lan']] = pd.DataFrame( | |
| claim_df.entity_id.progress_apply(Wd_API.get_alias, non_language_set=True).tolist() | |
| ) | |
| claim_df[['property_alias','property_alias_lan']] = pd.DataFrame( | |
| claim_df.property_id.progress_apply(Wd_API.get_alias, non_language_set=True).tolist() | |
| ) | |
| claim_df[['entity_desc','entity_desc_lan']] = pd.DataFrame( | |
| claim_df.entity_id.progress_apply(Wd_API.get_desc, non_language_set=True).tolist() | |
| ) | |
| claim_df[['property_desc','property_desc_lan']] = pd.DataFrame( | |
| claim_df.property_id.progress_apply(Wd_API.get_desc, non_language_set=True).tolist() | |
| ) | |
| claim_df['object_label'] = claim_df.apply(get_object_label_given_datatype, axis=1) | |
| claim_df['object_alias'] = claim_df.apply(get_object_alias_given_datatype, axis=1) | |
| claim_df['object_desc'] = claim_df.apply(get_object_desc_given_datatype, axis=1) | |
| claim_df['object_label'], claim_df['object_label_lan'] = zip(*claim_df['object_label'].apply(lambda x: x if isinstance(x, tuple) else (x, ''))) | |
| claim_df['object_alias'], claim_df['object_alias_lan'] = zip(*claim_df['object_alias'].apply(lambda x: x if isinstance(x, tuple) else (x, ''))) | |
| claim_df['object_desc'], claim_df['object_desc_lan'] = zip(*claim_df['object_desc'].apply(lambda x: x if isinstance(x, tuple) else (x, ''))) | |
| # Removing bad object labels | |
| claim_df = claim_df[claim_df['object_label_lan'] != 'none'].reset_index(drop=True) | |
| return claim_df | |
| def html2text(html_set): | |
| reference_html_df = html_set | |
| _RE_COMBINE_WHITESPACE = re.compile(r"\s+") | |
| ft_model = fasttext.load_model('base/lid.176.ftz') | |
| def predict_language(text, k=20): | |
| ls, scores = ft_model.predict(text, k=k) # top 20 matching languages | |
| ls = [l.replace('__label__','') for l in ls] | |
| return list(zip(ls,scores)) | |
| def get_url_language(html): | |
| try: | |
| soup = BeautifulSoup(html, "lxml") | |
| [s.decompose() for s in soup("script")] # remove <script> elements | |
| if soup.body == None: | |
| return ('no body', None) | |
| body_text = _RE_COMBINE_WHITESPACE.sub(" ", soup.body.get_text(' ')).strip() | |
| return predict_language(body_text, k=1)[0] | |
| except Exception: | |
| raise | |
| def get_text_p_tags(soup): | |
| p_tags = soup.find_all('p') | |
| text = [p.getText().strip() for p in p_tags if p.getText()] | |
| return '\n'.join(text) | |
| def clean_text_line_by_line(text, join=True, ch_join = ' ', verb=True): | |
| # text = soup.body.get_text() | |
| # break into lines and remove leading and trailing space on each | |
| lines = list(text.splitlines()) | |
| lines = (line.strip() for line in lines) | |
| # for each line, lets correct double spaces into single space | |
| lines = (re.sub(r' {2,}', ' ', line) for line in lines) | |
| # for each line, lets correct punctuation spaced to the left | |
| lines = (re.sub(r' ([.,:;!?\\-])', r'\1', line) for line in lines) | |
| # put periods if missing | |
| lines = [line+'.' if line and line[-1] not in string.punctuation else line for i, line in enumerate(lines)] | |
| if verb: | |
| for i, line in enumerate(lines): | |
| print(i,line) | |
| # drop blank lines | |
| if join: | |
| return ch_join.join([line for line in lines if line]) | |
| else: | |
| return [line for line in lines if line] | |
| def apply_manual_rules(text): | |
| # RULE: A line ending with a ':' followed by whitespaces and a newline is likely a continuing line and should be joined | |
| #text = re.sub( | |
| # r':\s*\n', | |
| # r': ', | |
| # text | |
| #) | |
| # RULE: Remove [1] reference numbers | |
| text = re.sub(r'\[[0-9]+\]', '', text) | |
| return text | |
| def retrieve_text_from_html(html, soup_parser = 'lxml', verb=True, join=True): | |
| if not isinstance(html, str) or 'DOCTYPE html' not in html: | |
| return 'No body' | |
| soup = BeautifulSoup(html, soup_parser) | |
| for script in soup(["script", "style"]): | |
| script.decompose() | |
| if soup.body == None: | |
| return 'No body' | |
| [s.unwrap() for s in soup.body.find_all('strong')] | |
| for p in soup.body.find_all('p'): | |
| p.string = _RE_COMBINE_WHITESPACE.sub(" ", p.get_text('')).strip() | |
| #DECOMPOSE ALL BAD TAGS | |
| #-------------- | |
| #for c in ['warningbox', 'metadata', 'references', 'navbox', 'toc', 'catlinks']: | |
| # for e in soup.body.find_all(class_=c): | |
| # print('decomposed',e) | |
| # e.decompose() | |
| # DECOMPOSE INVISIBLE ELEMENTS | |
| #for e in soup.body.find_all(): | |
| # if e.hidden: | |
| # print('decomposed',e) | |
| # e.decompose() | |
| # else: | |
| # if e.attrs is not None: | |
| # #print(e) | |
| # #print('-') | |
| # style = e.get('style') | |
| # if style and 'display' in style and 'none' in style: | |
| # print('decomposed',e) | |
| # e.decompose() | |
| # #print(e, style) | |
| #-------------- | |
| #print(soup.body) | |
| # BOILERPLATE REMOVAL OPTIONS | |
| #1. jusText | |
| #text = justext.justext(html, justext.get_stoplist("English")) | |
| #text = '\n'.join([paragraph.text for paragraph in text if not paragraph.is_boilerplate]) | |
| #2. boilerpy3 | |
| #html = soup.body | |
| #text = extractor.get_content(soup.prettify()) | |
| #3. Just extracting from 'text tags' like p | |
| #simple rules (does not work depending on website, like on artgallery.yale, anything without clear paragraphic style) | |
| #text = get_text_p_tags(soup) | |
| #4. NONE | |
| text = soup.body.get_text(' ').strip() # NOT GETTING FROM THE WHOLE SOUP, JUST BODY TO AVOID TITLES | |
| #POST PROCESSING | |
| text = apply_manual_rules(text) | |
| text = clean_text_line_by_line(text, ch_join = ' ', verb=verb, join=join) | |
| if not text: | |
| return 'No extractable text' if join else ['No extractable text'] | |
| else: | |
| return text | |
| i=0 | |
| print(i) | |
| print(reference_html_df.url.iloc[i]) | |
| reference_html_df['extracted_sentences'] = reference_html_df.html.progress_apply(retrieve_text_from_html, join=False, verb=False) | |
| join_ch = ' ' | |
| reference_html_df['extracted_text'] = reference_html_df.extracted_sentences.apply(lambda x : join_ch.join(x)) | |
| splitter = SentenceSplitter(language='en') | |
| seg = pysbd.Segmenter(language="en", clean=False) | |
| if not spacy.util.is_package("en_core_web_lg"): | |
| os.system("python -m spacy download en_core_web_lg") | |
| nlp = spacy.load("en_core_web_lg") | |
| text = reference_html_df.loc[0,'extracted_text'] | |
| # OPTION 1 | |
| # This gets some things wrong, such as Smt.=Shrimati ending a sentence, or any | |
| # initials like P. N. Nampoothiri or Lt. Col. | |
| #sents = sent_tokenize(text) | |
| # OPTION 2 | |
| # Also breaks titles and initials like above, but additionally gets parenthesis wrong, like | |
| # Amma Maharani [break](queen mother) [break] of Travancore. | |
| #sents = seg.segment(text) | |
| # OPTION 3 | |
| # Same as above plus new ones, like breaking contractions (like m. for married) | |
| #sents = splitter.split(text) | |
| # OPTION 4 | |
| # By far the best option, makes way less of the mistakes above, but not none. So let's adopt a strategy so ease this. | |
| sents = [s for s in nlp(text).sents] | |
| reference_html_df['nlp_sentences'] = reference_html_df.extracted_text.progress_apply(lambda x : [str(s) for s in nlp(x).sents]) | |
| reference_html_df['nlp_sentences_slide_2'] = reference_html_df['nlp_sentences'].progress_apply( | |
| lambda x : [' '.join([a,b]) for a,b in list(zip(x,x[1:]+['']))] | |
| ) | |
| assert type(reference_html_df.loc[0,'nlp_sentences']) == list | |
| assert type(reference_html_df.loc[0,'nlp_sentences'][0]) == str | |
| assert type(reference_html_df.loc[0,'nlp_sentences_slide_2']) == list | |
| assert type(reference_html_df.loc[0,'nlp_sentences_slide_2'][0]) == str | |
| return reference_html_df | |
| if __name__ == '__main__': | |
| conn = sqlite3.connect('wikidata_claims_refs_parsed.db') | |
| target_QID = 'Q3621696' | |
| claimParser(target_QID) #save results in .db | |
| filtered_df = propertyFiltering(target_QID) #update db and return dataframe after filtering | |
| url_set = urlParser(target_QID) #from ref table in .db | |
| html_set = htmlParser(url_set, target_QID) #Original html docs collection | |
| try: | |
| claim_text = claim2text(html_set) #Claims generation | |
| html_text = html2text(html_set) | |
| claim_text = claim_text.astype(str) | |
| html_text = html_text.astype(str) | |
| claim_text.to_sql('claim_text', conn, if_exists='replace', index=False) | |
| html_text.to_sql('html_text', conn, if_exists='replace', index=False) | |
| except Exception as e: | |
| print(f"No accessible html documents") | |
| conn.commit() | |
| conn.close() | |
| #augmented_df = textualAugmentation(filtered_df) #textual information augmentation including label, desc, and alias | |