cosmoguard-bd/old/_old/scraper_cosing.py
2025-11-15 16:02:37 +01:00

182 lines
No EOL
6.3 KiB
Python

import json as js
import re
import requests as req
from typing import Union
#region Funzione che processa una lista di CAS presa da Cosing (Grazie Jem)
def parse_cas_numbers(cas_string:list) -> list:
# Siccome ci assicuriamo esternamente che esista almeno un cas possiamo prendere la stringa
cas_string = cas_string[0]
# Rimuoviamo parentesi e il loro contenuto
cas_string = re.sub(r"\([^)]*\)", "", cas_string)
# Eseguiamo uno split su vari possibili separatori
cas_parts = re.split(r"[/;,]", cas_string)
# Otteniamo una lista utilizzando i cas precedenti e rimuovendo spazi in eccesso
cas_list = [cas.strip() for cas in cas_parts]
# Alcuni cas sono divisi da un doppio dash (--) che dobbiamo rimuovere
# è però necessario farlo ora in seconda battuta
if len(cas_list) == 1 and "--" in cas_list[0]:
cas_list = [cas.strip() for cas in cas_list[0].split("--")]
# Siccome alcuni cas hanno valori non validi ("-") li cerchiamo e rimuoviamo
if cas_list:
while "-" in cas_list:
cas_list.remove("-")
return cas_list
#endregion
#region Funzione per eseguire una ricerca direttamente sul cosing
# Il primo argomento è la stringa da cercare, il secondo indica il tipo di ricerca
def cosing_search(text : str, mode : str = "name") -> Union[list,dict,None]:
cosing_post_req = "https://api.tech.ec.europa.eu/search-api/prod/rest/search?apiKey=285a77fd-1257-4271-8507-f0c6b2961203&text=*&pageSize=100&pageNumber=1"
agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
# La modalità di ricerca base è quella per nome, che sia INCI o di altro tipo
if mode == "name":
search_query = {"bool":
{"must":[
{"text":
{"query":f"{text}","fields":
["inciName.exact","inciUsaName","innName.exact","phEurName","chemicalName","chemicalDescription"],
"defaultOperator":"AND"}}]}}
# In caso di ricerca per numero cas o EC il payload della richiesta è diverso
elif mode in ["cas","ec"]:
search_query = {"bool": {"must": [{"text": {"query": f"*{text}*","fields": ["casNo", "ecNo"]}}]}}
# La ricerca per ID è necessaria dove serva recuperare gli identified ingredients
elif mode == "id":
search_query = {"bool":{"must":[{"term":{"substanceId":f"{text}"}}]}}
# Se la mode inserita non è prevista lancio un errore
else:
raise ValueError
# Creo il payload della mia request
files = {"query": ("query",js.dumps(search_query),"application/json")}
# Eseguo la post di ricerca
risposta = req.post(cosing_post_req,headers={"user_agent":agent,"Connection":"keep-alive"},files=files)
risposta = risposta.json()
if risposta["results"]:
return risposta["results"][0]["metadata"]
# La funzione ritorna None quando non ho risultati dalla mia ricerca
return risposta.status_code
#endregion
#region Funzione per pulire un json cosing e restituirlo
def clean_cosing(json : dict, full : bool = True) -> dict:
# Definisco i campi di nostro interesse e li divido in base al tipo di output finale che desidero
string_cols = ["itemType","nameOfCommonIngredientsGlossary","inciName","phEurName","chemicalName","innName","substanceId","refNo"]
list_cols = ["casNo","ecNo","functionName","otherRestrictions","sccsOpinion","sccsOpinionUrls","identifiedIngredient","annexNo","otherRegulations"]
# Creo una lista con tutti i campi su cui ciclare
total_keys = string_cols + list_cols
# Definisco la base dell"url che mi serve per ottenere il link cosing della sostanza
base_url = "https://ec.europa.eu/growth/tools-databases/cosing/details/"
clean_json = {}
# Ciclo su tutti i campi di interesse
for key in total_keys:
# Alcuni campi contengono una dicitura inutile che occupa solo spazio
# per cui provvedo a rimuoverla
while "<empty>" in json[key]:
json[key].remove("<empty>")
# Se il campo dovrà avere in output una lista, posso anche accettare le liste vuote del cosing come valore
if key in list_cols:
value = json[key]
# Il cas e l"ec sono casi speciali, per cui eseguo delle funzioni in più quando lo tratto
if key in ["casNo", "ecNo"]:
if value:
value = parse_cas_numbers(value)
# Completiamo direttamente il json di ritorno ove presenti degli identifiedIngredient,
# solo dove il flag "full" è true
elif key == "identifiedIngredient":
if full:
if value:
value = identified_ingredients(value)
clean_json[key] = value
else:
# Questo nome di campo era troppo lungo e ho preferito semplificarlo
if key == "nameOfCommonIngredientsGlossary":
nKey = "commonName"
# Dovendo rinominare alcuni campi, negli altri casi copio il nome iniziale
else:
nKey = key
# Siccome voglio in output una stringa semplice e se usassi uno slicer su di una lista vuota
# devo prima verificare che la lista cosing contenga dei valori
if json[key]:
clean_json[nKey] = json[key][0]
else:
clean_json[nKey] = ""
# Il campo cosingUrl non esiste ancora, vado a crearlo unendo il substance ID all"url base
clean_json["cosingUrl"] = f"{base_url}{json["substanceId"][0]}"
return clean_json
#endregion
#region Funzione per completare, se necessario, un json cosing
def identified_ingredients(id_list : list) -> list:
identified = []
# Per ognuno degli ingredienti in IdentifiedIngredients eseguo una ricerca
for id in id_list:
ingredient = cosing_search(id,"id")
if ingredient:
# Vado a pulire i json appena trovati
ingredient = clean_cosing(ingredient,full=False)
# Ora salvo nella lista il documento pulito
identified.append(ingredient)
# Finito di popolare la lista con gli oggetti degli identifiedIngredient, la ritorno
return identified
#endregion