diff --git a/authors-tools/README.md b/authors-tools/README.md old mode 100755 new mode 100644 index cd8fa97..bf0f5a4 --- a/authors-tools/README.md +++ b/authors-tools/README.md @@ -1,76 +1,94 @@ -# Authors-tools +# orcid-disambiguation -URL du webservice : . +Cette instance propose un outil de de désambiguisation d'auteur orcid -Elle offre plusieurs services autour des auteurs dans les notices bibliographiques: +## Description et utilisation du programme -- `orcid-disambiguation`: octroie un ORCID à un auteur si celui-ci existe dans - l'API ORCID, -- `ml-orcid`: utilise des méthodes de Machine Learning pour désambigüiser un - corpus co-écrit par un `nom d'auteurs` (à distinguer d'`auteur`). Pas encore - disponible. +- [v1/orcidDisambiguation](#v1%2forcidDisambiguation) -## Utilisation +Ce web service prend en entrée du JSON avec deux champs, `id` et `value`, et +renvoie un JSON avec un identifiant ORCID dans le champ `value`. +Le champ `value` doit contenir un json contenant au minimum les deux champs +suivants: -### v1/orcid-disambiguation +- `firstName` : Le prénom de la personne que l'on souhaite trouver +- `lastName` : Le nom de la personne que l'on souhaite trouver -### Fichier d'entrée +De plus, d'autres champs facultatifs (mais fortement recommandés d'utiliser pour +plus de précision) sont disponibles : -Un objet au format JSON représenté comme suit : +- `email` : Une liste `[]` d'emails de la personne que l'on souhaite trouver +- `titles` : Une liste `[]` de titres de publication scientifique de la personne + que l'on souhaite trouver +- `coAuthors` : Une liste `[]` de co-auteurs de la personne que l'on souhaite + trouver +- `affiliations` : Une liste `[]` d'affiliations (présentes ou passées) de la + personne que l'on souhaite trouver -```json -{ - "id" : "0000-0002-8279-9690", - "value" : { - "authorname" : "Laurent DUPONT", - "title" : "Applying a Living Lab Approach to Smart Grid Training Course Design", - "affiliation" : "Université de Lorraine ERPI / ENSGSI: NANCY, FR ", - "publication_date" : 2020 - } -} +Le programme fonctionne de la façon suivante : + +- Il fait une requète ORCID pour le `firstName` et `lastName` donnés récupérant + un nombre `nameDepth` de personnes, `nameDepth` étant un paramètre fixé à 20, + et pouvant être modifié via l'url. +- L'algorithme va ensuite prendre ces personnes une à une et effectuer dans cet + ordre : + - Si une liste d'emails a été fournie en entrée, il va effectuer une + comparaison avec les emails disponibles pour la personne. Il s'arrète si il + y en a un en commun, et renvoie l'orcid de la personne. +- L'algorithme va ensuite faire une requête afin de récupérer un nombre + `worksDepth` maximum de publications de la personne, `worksDepth` étant un + paramètre fixé à 20, et pouvant être modifié via l'url. De ces publications il + va extraire les titres ainsi que tous les co-auteurs disponibles. + - Si une liste de titres a été fournie en entrée, l'algorithme va ensuite + comparer les titres de ces publications avec la liste d'entrée. Si un titre + de la liste d'entrée correspond à plus de 70% avec un titre de la liste des + publications de la personne, l'algorithme s'arrète et renvoie l'orcid de + cette personne. + - Si une liste de co-auteurs a été fournie en entrée, l'algorithme va ensuite + comparer les co-auteurs de ces publications avec la liste d'entrée. Si un + co-auteur de la liste d'entrée correspond avec un co-auteur de la liste des + publications de la personne, l'algorithme s'arrête et renvoie l'orcid de + cette personne. + - Enfin si aucune des étapes précédentes n'est validée, et si une liste + d'affiliations a été fournie, l'algorithme va comparer la liste + d'affiliations d'entrée avec les affiliations présentes et passées de la + personne. Plus il y a d'affiliations en commun, plus la personne obtiendra + un score élévé et sera susceptible d'être retenue à la fin. + - Pour finir, des points sont également ajoutés au score si la personne a le + même nom, le même prénom ou la même initiale que le prénom de la personne + que l'on souhaite retrouver. Si au cour de cette boucle l'algorithme ne + s'est pas arrété suite à un email, titre ou co-auteur, il renverra la + personne ayant obtenu le plus gros score. + +Pour les combinaisons de prénom/nom très communes dans certains pays (par +exemple John Smith, Yue Chen), il est conseillé d'augmenter le paramètre +`nameDepth`. Cependant cela risque également d'augmenter le temps de calcul. +De plus l'algorithme renverra dans la majorité des cas un résultat, mais il est +possible que celui-ci soit incorrect si aucun des arguments d'entrée n'a aidé à +identifier la personne recherchée. + +Remarque : On ne pourra pas trouver une personne à l'aide de titres, co-auteurs, +emails et affiliations si cette personne n'a pas rentré ces données dans son +compte orcid, par conséquent une personne étant sur orcid mais n'ayant mis +aucune information à disposition peut ne pas être trouvée. + +## Exemple + +```bash +$ cat < **NB** : Seul le champ `authorname` est obligatoire. - -### Fichier de sortie - -On a : +Sortie ```json -{ - "id" : "000-0002-8279-9690", - "value" : { - "orcid": "000-0002-8279-9690" - "score" : 0.23076923076923078 - } -} +[{"id":"1","value":"0000-0002-6809-5654"}, + {"id":"2","value":"0000-0002-8416-869X"}] +EOF ``` - +Exemple d'url en modifiant les paramètres `nameDepth` et `worksDepth` : -## ML disambiguation - -**En cours d'élaboration** - - \ No newline at end of file + diff --git a/authors-tools/examples.http b/authors-tools/examples.http new file mode 100644 index 0000000..3e5086e --- /dev/null +++ b/authors-tools/examples.http @@ -0,0 +1,14 @@ +# Désambiguisation d'auteur orcid +POST https://geoEntity-tagger.services.inist.fr/v1/geoTagger/geoTagger?indent=true HTTP/1.1 #A modifier +Content-Type: application/json + +[ + { + "id":"1", + "value":[{"firstName" : "Pascal", "lastName" : "Cuxac", "email" : ["blabla@blabla.fr","pascal.cuxac@inist.fr"]}] + }, + { + "id":"2", + "value":[{"firstName" : "Rubén", "lastName" : "Vázquez-Cárdenas", "coAuthors" : ["Juan pablo Martínez-Pastor"]}] + } +] diff --git a/authors-tools/requirements.txt b/authors-tools/requirements.txt old mode 100755 new mode 100644 index b4e164d..54d013d --- a/authors-tools/requirements.txt +++ b/authors-tools/requirements.txt @@ -1 +1,3 @@ -git+http://vxgit.intra.inist.fr:60000/RichText/orcidAuthornameDisambiguation.git#egg=orcidDisambiguator \ No newline at end of file +pandas +requests +plac diff --git a/authors-tools/swagger.json b/authors-tools/swagger.json old mode 100755 new mode 100644 index 37de2c7..a1552ca --- a/authors-tools/swagger.json +++ b/authors-tools/swagger.json @@ -1,24 +1,20 @@ { - "info": { - "version": "1.0.0" - }, - "servers": [{ - "url": "{scheme}://{hostname}", - "variables": { - "scheme": { - "default": "https" - } - } - }], - "tags": [ - { - "name": "authors-tools", - "description": "Outils de traitements autour des auteurs", - "externalDocs": { - "description": "Plus de documentation", - "url": "https://gitbucket.inist.fr/tdm/web-services/tree/master/author-tools" - } - } - ] -} - + "servers": [{ + "url": "{scheme}://{hostname}", + "variables": { + "scheme": { + "default": "https" + } + } + }], + "tags": [ + { + "name": "orcidDisambiguation", + "description": "Désambiguisation d'auteur orcid", + "externalDocs": { + "description": "Plus de documentation", + "url": "https://gitbucket.inist.fr/tdm/web-services/tree/master/orcid-diambiguation" + } + } + ] +} \ No newline at end of file diff --git a/authors-tools/v1/orcid-disambiguation/disambiguate.py b/authors-tools/v1/orcid-disambiguation/disambiguate.py new file mode 100644 index 0000000..c36c0c8 --- /dev/null +++ b/authors-tools/v1/orcid-disambiguation/disambiguate.py @@ -0,0 +1,211 @@ +import requests +import pandas as pd +from io import StringIO +from difflib import SequenceMatcher +import urllib.parse +import time +import sys + +def getPoints(liste): + return liste[2] + + +class disambiguate: + def __init__(self, infoDic, nameDepth = 20, worksDepth = 20): + self.timeBetweenrequest = 0.01 + self.version = "v3.0" + self.nameDepth = nameDepth + self.worksDepth = worksDepth + self.infoDic = {} + self.extractInfoDic(infoDic) + + def getDfFromName(self,name): + url = "https://pub.orcid.org/v3.0/csv-search/?q="+urllib.parse.quote("(given-and-family-names:"+name+")") + response = requests.get(url,headers={'Accept':'text/csv'}) + df = pd.read_csv(StringIO(str(response.content,'utf-8'))) + return df + + def getWorksFromOrcid(self,orcid): + url = "https://pub.orcid.org/"+self.version+"/"+orcid+"/works" + response = requests.get(url,headers={'Accept':'application/orcid+json'}) + works = response.json().get("group") + return works + + def getCoAuthorsFromPutcode(self,putcode,orcid): + url = "https://pub.orcid.org/"+self.version+"/"+orcid+"/work/"+str(putcode) + response = requests.get(url,headers={'Accept':'application/orcid+json'}) + contributors = response.json().get("contributors") + try: + coAuthors = [contributor["credit-name"]['value'] for contributor in contributors.get("contributor")] + except: + coAuthors = "" + return coAuthors + + def extractInfoDic(self,dic): + self.infoDic["firstName"] = dic["firstName"] + self.infoDic["lastName"] = dic["lastName"] + if "email" in dic: + self.infoDic["email"] = dic["email"] + if "affiliations" in dic: + self.infoDic["affiliations"] = dic["affiliations"] + if "titles" in dic: + self.infoDic["titles"] = dic["titles"] + if "coAuthors" in dic: + self.infoDic["coAuthors"] = dic["coAuthors"] + + def extractDfInfos(self,df): + infos = [] + for i in range(self.nameDepth): + dic = {} + + dic["firstName"] = str(df["given-names"][i]) + dic["lastName"] = str(df["family-name"][i]) + dic["email"] = str(df["email"][i]) + + try: + dic["affiliations"] = df["current-institution-affiliation-name"][i].split(",") + except: + dic["affiliations"] = [] + + try: + dic["affiliations"] += df["past-institution-affiliation-name"][i].split(",") + except: + pass + + dic["orcid"] = df["orcid"][i] + infos.append(dic) + return infos + + def extractInfoFromWorks(self,works): + dic = { "putcode" : [], + "title" : [], + "publicationDate" : [], + "journal" : [], + "externalIds" : []} + + for i,work in enumerate(works): + if i > self.worksDepth: + break + workSummary = work.get('work-summary') + for ws in workSummary : + dic["putcode"].append(ws.get("put-code")) + dic["title"].append(ws.get("title").get('title').get("value")) + try: + dic["publicationDate"].append(ws.get("publication-date").get("year").get('value')) + except: + dic["publicationDate"].append("Err") + try: + dic["journal"].append(ws.get("journal-title")) + except: + dic["journal"].append("Err") + + try: + dic["externalIds"].append([external_id['external-id-value'] for external_id in ws.get("external-ids")["external-id"]]) + except: + dic["externalIds"].append(["Err"]) + return dic + + def splitFirstName(self,name): + choice = [] + nameSplit = name.lower().split(" ") + try: + while True: + nameSplit.remove("") + except ValueError: + pass + if len(nameSplit) > 2: + nameSplit = [" ".join(nameSplit[:-1]),nameSplit[-1]] + choice.append(name.lower()) + choice.append(nameSplit[1]+" "+nameSplit[0]) + fnSplit = nameSplit[0].split("-") #composed firstname with - in it + if len(fnSplit) > 1: #composed firstname with - in it + choice.append(nameSplit[1]+", "+fnSplit[0][0]+".-"+fnSplit[1][0]+".") + choice.append(nameSplit[1]+", "+fnSplit[0][0]+"."+fnSplit[1][0]+".") + else: #composed first name with space in it + fnSplit = nameSplit[0].split(" ") + if len(fnSplit) > 1: + choice.append(nameSplit[1]+", "+fnSplit[0]+".-"+fnSplit[1][0]+".") #lastname, Jaa.-P. + choice.append(nameSplit[1]+", "+fnSplit[0]+" "+fnSplit[1][0]+".")#lastname, Jaa P. + choice.append(nameSplit[1]+", "+fnSplit[0][0]+"."+fnSplit[1][0]+".") #lastname, Jaa.P. + choice.append(fnSplit[0]+" "+fnSplit[1][0]+". "+nameSplit[1])#Jaa P. lastname + choice.append(fnSplit[0][0]+"."+fnSplit[1][0]+". "+nameSplit[1])#J.P. lastname + choice.append(fnSplit[0][0]+fnSplit[1][0]+". "+nameSplit[1])#JP. lastname + else: + choice.append(nameSplit[1]+", "+nameSplit[0][0]+".") + + if len(fnSplit) == 1: #composed first name with space in it + fnSplit = nameSplit[0].split("-") + return choice + + def checkEmail(self,email): + for em in self.infoDic["email"]: + if em == email: + return True,em + return False,0 + + def disambiguation(self): + df = self.getDfFromName(self.infoDic["firstName"]+"+AND+"+self.infoDic["lastName"]) + time.sleep(self.timeBetweenrequest) + personsInfos = self.extractDfInfos(df) + for personInfos in personsInfos: + orcid = personInfos["orcid"] + matchArg = [] + points = 0 + + if "email" in self.infoDic: #check email + end,em = self.checkEmail(personInfos["email"]) + if end: + return [[orcid,["Email "+em],100]] + + works = self.getWorksFromOrcid(orcid) + time.sleep(self.timeBetweenrequest) + worksInfo = self.extractInfoFromWorks(works) + + if "titles" in self.infoDic: #check title + for title in self.infoDic["titles"]: + for tit in [title.lower() for title in worksInfo["title"]]: + ratio = SequenceMatcher(None, title.lower(), tit).ratio() #check similarity between title + if ratio > 0.7: + return [[orcid,["title "+tit],100]] + + if "coAuthors" in self.infoDic: #check coAuthors + authorsPutcode = [] + for putcode in worksInfo["putcode"]: + authorsPutcode += self.getCoAuthorsFromPutcode(putcode,orcid) + time.sleep(self.timeBetweenrequest) + authors = list(set(authorsPutcode)) + for author in self.infoDic["coAuthors"]: + choices = self.splitFirstName(author) + for choice in choices: + if choice in [auth.lower() for auth in authors]: + return [[orcid,["Co-authors "+choice],100]] + + if "affiliations" in self.infoDic: #check affiliations + for affiliation in self.infoDic["affiliations"]: + if affiliation.lower() in [aff.lower() for aff in personInfos["affiliations"]]: + matchArg.append("Affiliation "+affiliation) + points += 10 + + #check first and last name + if self.infoDic["lastName"].lower() == personInfos["lastName"].lower(): + if self.infoDic["firstName"].lower() == personInfos["firstName"].lower(): + points += 10 + matchArg.append("Match First+LastName ") + elif self.infoDic["firstName"].lower()[0] == personInfos["firstName"].lower()[0]: + points += 7 + matchArg.append("Match FirstName First Letter+LastName ") + else: + points += 5 + matchArg.append("Match LastName ") + + personInfos["points"] = points + personInfos["matchArg"] = matchArg + + #check first and last name + finalReturn = [] + for personInfos in personsInfos: + if personInfos["points"] != 0: + finalReturn.append([personInfos["orcid"],personInfos["matchArg"],personInfos["points"]]) + + finalReturn.sort(key=getPoints,reverse=True) + return finalReturn diff --git a/authors-tools/v1/orcid-disambiguation/orcid.ini b/authors-tools/v1/orcid-disambiguation/orcid.ini deleted file mode 100755 index 68dce84..0000000 --- a/authors-tools/v1/orcid-disambiguation/orcid.ini +++ /dev/null @@ -1,40 +0,0 @@ -# OpenAPI Documentation - JSON format (dot notation) -post.responses.default.description = Return all objects with enrich fields -post.responses.default.content.application/json.schema.$ref = #/components/schemas/JSONStream -post.summary = Enrich one field of each Object with a Python function -post.requestBody.required = true -post.requestBody.content.application/json.schema.$ref = #/components/schemas/JSONStream -post.parameters.0.in = query -post.parameters.0.name = path -post.parameters.0.schema.type = string -post.parameters.0.description = The path in each object to enrich with an Python script -post.parameters.1.in = query -post.parameters.1.name = indent -post.parameters.1.schema.type = boolean -post.parameters.1.description = Indent or not the JSON Result - - -[use] -plugin = @ezs/local -plugin = @ezs/basics -plugin = @ezs/storage -plugin = @ezs/analytics - -[JSONParse] -separator = * - -[expand] -path = env('path', 'value') -size = 100 - -# in production mode, uncomment the following line -# cache = boost - - -[expand/exec] -# command should be executable ! -command = ./v1/orcid-disambiguation/orcid.py - - -[dump] -indent = env('indent', false) diff --git a/authors-tools/v1/orcid-disambiguation/orcid.py b/authors-tools/v1/orcid-disambiguation/orcid.py deleted file mode 100755 index 55ada5f..0000000 --- a/authors-tools/v1/orcid-disambiguation/orcid.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/python3 -import sys -import json -from orcidDisambiguator.disambiguate import Disambiguator - - -for line in sys.stdin: - data = json.loads(line) - d = Disambiguator(**data["value"]) - res = d.get_best_orcid_from_df() - data["value"] = res - sys.stdout.write(json.dumps(data)) - sys.stdout.write('\n') \ No newline at end of file diff --git a/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.ini b/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.ini new file mode 100644 index 0000000..bfc5b83 --- /dev/null +++ b/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.ini @@ -0,0 +1,51 @@ +# OpenAPI Documentation - JSON format (dot notation) +post.responses.default.description = Return all objects with enrich fields +post.responses.default.content.application/json.schema.$ref = #/components/schemas/JSONStream +post.summary = Disambiguation of Orcid author +post.requestBody.required = true +post.requestBody.content.application/json.schema.$ref = #/components/schemas/JSONStream +post.parameters.0.in = query +post.parameters.0.name = path +post.parameters.0.schema.type = string +post.parameters.0.description = The path in each object to enrich with an Python script +post.parameters.1.in = query +post.parameters.1.name = indent +post.parameters.1.schema.type = boolean +post.parameters.1.description = Indent or not the JSON Result + +post.parameters.2.in = query +post.parameters.2.name = nameDepth +post.parameters.2.schema.type = int +post.parameters.2.description = Maximum number of people to check + +post.parameters.3.in = query +post.parameters.3.name = worksDepth +post.parameters.3.schema.type = int +post.parameters.3.description = Maximum number of works we take for a person + +[use] +plugin = @ezs/spawn +plugin = @ezs/basics +plugin = @ezs/storage +plugin = @ezs/analytics + +[JSONParse] +separator = * + +[expand] +path = env('path', 'value') +size = 100 +# in production mode, uncomment the following line +# cache = boost + +[expand/exec] +# command should be executable ! +command = ./v1/orcid-disambiguation/orcidDisambiguation.py +args = fix('-p') +args = env('nameDepth',20) +args = fix('-q') +args = env('worksDepth',20) +#command = ./expand.py + +[dump] +indent = env('indent', false) diff --git a/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.py b/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.py new file mode 100644 index 0000000..62639d6 --- /dev/null +++ b/authors-tools/v1/orcid-disambiguation/orcidDisambiguation.py @@ -0,0 +1,27 @@ +#!/usr/bin/python3 +from disambiguate import disambiguate +import sys +import json +import plac + +@plac.annotations( + nameDepth = ("Maximum number of people to check" ,"option", "p", int ), + worksDepth = ("Maximum number of works we take for a person" ,"option", "q", int ), +) + +def main(nameDepth = 20, worksDepth = 20): + for line in sys.stdin: + data = json.loads(line) + infos = data['value'] + info = infos[0].copy() + db = disambiguate(info,nameDepth=nameDepth, worksDepth=worksDepth ) + result = db.disambiguation() + if len(result)>0: + data['value'] = result[0][0] + else: + data['value'] = "None" + sys.stdout.write(json.dumps(data)) + sys.stdout.write('\n') + +if __name__ == "__main__": + plac.call(main) \ No newline at end of file diff --git a/orcid-disambiguation/README.md b/orcid-disambiguation/README.md deleted file mode 100644 index bf0f5a4..0000000 --- a/orcid-disambiguation/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# orcid-disambiguation - -Cette instance propose un outil de de désambiguisation d'auteur orcid - -## Description et utilisation du programme - -- [v1/orcidDisambiguation](#v1%2forcidDisambiguation) - -Ce web service prend en entrée du JSON avec deux champs, `id` et `value`, et -renvoie un JSON avec un identifiant ORCID dans le champ `value`. -Le champ `value` doit contenir un json contenant au minimum les deux champs -suivants: - -- `firstName` : Le prénom de la personne que l'on souhaite trouver -- `lastName` : Le nom de la personne que l'on souhaite trouver - -De plus, d'autres champs facultatifs (mais fortement recommandés d'utiliser pour -plus de précision) sont disponibles : - -- `email` : Une liste `[]` d'emails de la personne que l'on souhaite trouver -- `titles` : Une liste `[]` de titres de publication scientifique de la personne - que l'on souhaite trouver -- `coAuthors` : Une liste `[]` de co-auteurs de la personne que l'on souhaite - trouver -- `affiliations` : Une liste `[]` d'affiliations (présentes ou passées) de la - personne que l'on souhaite trouver - -Le programme fonctionne de la façon suivante : - -- Il fait une requète ORCID pour le `firstName` et `lastName` donnés récupérant - un nombre `nameDepth` de personnes, `nameDepth` étant un paramètre fixé à 20, - et pouvant être modifié via l'url. -- L'algorithme va ensuite prendre ces personnes une à une et effectuer dans cet - ordre : - - Si une liste d'emails a été fournie en entrée, il va effectuer une - comparaison avec les emails disponibles pour la personne. Il s'arrète si il - y en a un en commun, et renvoie l'orcid de la personne. -- L'algorithme va ensuite faire une requête afin de récupérer un nombre - `worksDepth` maximum de publications de la personne, `worksDepth` étant un - paramètre fixé à 20, et pouvant être modifié via l'url. De ces publications il - va extraire les titres ainsi que tous les co-auteurs disponibles. - - Si une liste de titres a été fournie en entrée, l'algorithme va ensuite - comparer les titres de ces publications avec la liste d'entrée. Si un titre - de la liste d'entrée correspond à plus de 70% avec un titre de la liste des - publications de la personne, l'algorithme s'arrète et renvoie l'orcid de - cette personne. - - Si une liste de co-auteurs a été fournie en entrée, l'algorithme va ensuite - comparer les co-auteurs de ces publications avec la liste d'entrée. Si un - co-auteur de la liste d'entrée correspond avec un co-auteur de la liste des - publications de la personne, l'algorithme s'arrête et renvoie l'orcid de - cette personne. - - Enfin si aucune des étapes précédentes n'est validée, et si une liste - d'affiliations a été fournie, l'algorithme va comparer la liste - d'affiliations d'entrée avec les affiliations présentes et passées de la - personne. Plus il y a d'affiliations en commun, plus la personne obtiendra - un score élévé et sera susceptible d'être retenue à la fin. - - Pour finir, des points sont également ajoutés au score si la personne a le - même nom, le même prénom ou la même initiale que le prénom de la personne - que l'on souhaite retrouver. Si au cour de cette boucle l'algorithme ne - s'est pas arrété suite à un email, titre ou co-auteur, il renverra la - personne ayant obtenu le plus gros score. - -Pour les combinaisons de prénom/nom très communes dans certains pays (par -exemple John Smith, Yue Chen), il est conseillé d'augmenter le paramètre -`nameDepth`. Cependant cela risque également d'augmenter le temps de calcul. -De plus l'algorithme renverra dans la majorité des cas un résultat, mais il est -possible que celui-ci soit incorrect si aucun des arguments d'entrée n'a aidé à -identifier la personne recherchée. - -Remarque : On ne pourra pas trouver une personne à l'aide de titres, co-auteurs, -emails et affiliations si cette personne n'a pas rentré ces données dans son -compte orcid, par conséquent une personne étant sur orcid mais n'ayant mis -aucune information à disposition peut ne pas être trouvée. - -## Exemple - -```bash -$ cat < diff --git a/orcid-disambiguation/examples.http b/orcid-disambiguation/examples.http deleted file mode 100644 index cee164b..0000000 --- a/orcid-disambiguation/examples.http +++ /dev/null @@ -1,14 +0,0 @@ -# Désambiguisation d'auteur orcid -POST https://geoEntity-tagger.services.inist.fr/v1/geoTagger/geoTagger?indent=true HTTP/1.1 -Content-Type: application/json - -[ - { - "id":"1", - "value":[{"firstName" : "Pascal", "lastName" : "Cuxac", "email" : ["blabla@blabla.fr","pascal.cuxac@inist.fr"]}] - }, - { - "id":"2", - "value":[{"firstName" : "Rubén", "lastName" : "Vázquez-Cárdenas", "coAuthors" : ["Juan pablo Martínez-Pastor"]}] - } -] \ No newline at end of file diff --git a/orcid-disambiguation/requirements.txt b/orcid-disambiguation/requirements.txt deleted file mode 100644 index 54d013d..0000000 --- a/orcid-disambiguation/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pandas -requests -plac diff --git a/orcid-disambiguation/swagger.json b/orcid-disambiguation/swagger.json deleted file mode 100644 index a1552ca..0000000 --- a/orcid-disambiguation/swagger.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "servers": [{ - "url": "{scheme}://{hostname}", - "variables": { - "scheme": { - "default": "https" - } - } - }], - "tags": [ - { - "name": "orcidDisambiguation", - "description": "Désambiguisation d'auteur orcid", - "externalDocs": { - "description": "Plus de documentation", - "url": "https://gitbucket.inist.fr/tdm/web-services/tree/master/orcid-diambiguation" - } - } - ] -} \ No newline at end of file diff --git a/orcid-disambiguation/v1/disambiguate.py b/orcid-disambiguation/v1/disambiguate.py deleted file mode 100644 index 612f1f7..0000000 --- a/orcid-disambiguation/v1/disambiguate.py +++ /dev/null @@ -1,212 +0,0 @@ -import requests -import pandas as pd -from io import StringIO -from difflib import SequenceMatcher -import urllib.parse -import time -import sys - -def getPoints(liste): - return liste[2] - - -class disambiguate: - def __init__(self, infoDic, nameDepth = 20, worksDepth = 20): - print(nameDepth, worksDepth,file=sys.stderr) - self.timeBetweenrequest = 0.01 - self.version = "v3.0" - self.nameDepth = nameDepth - self.worksDepth = worksDepth - self.infoDic = {} - self.extractInfoDic(infoDic) - - def getDfFromName(self,name): - url = "https://pub.orcid.org/v3.0/csv-search/?q="+urllib.parse.quote("(given-and-family-names:"+name+")") - response = requests.get(url,headers={'Accept':'text/csv'}) - df = pd.read_csv(StringIO(str(response.content,'utf-8'))) - return df - - def getWorksFromOrcid(self,orcid): - url = "https://pub.orcid.org/"+self.version+"/"+orcid+"/works" - response = requests.get(url,headers={'Accept':'application/orcid+json'}) - works = response.json().get("group") - return works - - def getCoAuthorsFromPutcode(self,putcode,orcid): - url = "https://pub.orcid.org/"+self.version+"/"+orcid+"/work/"+str(putcode) - response = requests.get(url,headers={'Accept':'application/orcid+json'}) - contributors = response.json().get("contributors") - try: - coAuthors = [contributor["credit-name"]['value'] for contributor in contributors.get("contributor")] - except: - coAuthors = "" - return coAuthors - - def extractInfoDic(self,dic): - self.infoDic["firstName"] = dic["firstName"] - self.infoDic["lastName"] = dic["lastName"] - if "email" in dic: - self.infoDic["email"] = dic["email"] - if "affiliations" in dic: - self.infoDic["affiliations"] = dic["affiliations"] - if "titles" in dic: - self.infoDic["titles"] = dic["titles"] - if "coAuthors" in dic: - self.infoDic["coAuthors"] = dic["coAuthors"] - - def extractDfInfos(self,df): - infos = [] - for i in range(self.nameDepth): - dic = {} - - dic["firstName"] = str(df["given-names"][i]) - dic["lastName"] = str(df["family-name"][i]) - dic["email"] = str(df["email"][i]) - - try: - dic["affiliations"] = df["current-institution-affiliation-name"][i].split(",") - except: - dic["affiliations"] = [] - - try: - dic["affiliations"] += df["past-institution-affiliation-name"][i].split(",") - except: - pass - - dic["orcid"] = df["orcid"][i] - infos.append(dic) - return infos - - def extractInfoFromWorks(self,works): - dic = { "putcode" : [], - "title" : [], - "publicationDate" : [], - "journal" : [], - "externalIds" : []} - - for i,work in enumerate(works): - if i > self.worksDepth: - break - workSummary = work.get('work-summary') - for ws in workSummary : - dic["putcode"].append(ws.get("put-code")) - dic["title"].append(ws.get("title").get('title').get("value")) - try: - dic["publicationDate"].append(ws.get("publication-date").get("year").get('value')) - except: - dic["publicationDate"].append("Err") - try: - dic["journal"].append(ws.get("journal-title")) - except: - dic["journal"].append("Err") - - try: - dic["externalIds"].append([external_id['external-id-value'] for external_id in ws.get("external-ids")["external-id"]]) - except: - dic["externalIds"].append(["Err"]) - return dic - - def splitFirstName(self,name): - choice = [] - nameSplit = name.lower().split(" ") - try: - while True: - nameSplit.remove("") - except ValueError: - pass - if len(nameSplit) > 2: - nameSplit = [" ".join(nameSplit[:-1]),nameSplit[-1]] - choice.append(name.lower()) - choice.append(nameSplit[1]+" "+nameSplit[0]) - fnSplit = nameSplit[0].split("-") #composed firstname with - in it - if len(fnSplit) > 1: #composed firstname with - in it - choice.append(nameSplit[1]+", "+fnSplit[0][0]+".-"+fnSplit[1][0]+".") - choice.append(nameSplit[1]+", "+fnSplit[0][0]+"."+fnSplit[1][0]+".") - else: #composed first name with space in it - fnSplit = nameSplit[0].split(" ") - if len(fnSplit) > 1: - choice.append(nameSplit[1]+", "+fnSplit[0]+".-"+fnSplit[1][0]+".") #lastname, Jaa.-P. - choice.append(nameSplit[1]+", "+fnSplit[0]+" "+fnSplit[1][0]+".")#lastname, Jaa P. - choice.append(nameSplit[1]+", "+fnSplit[0][0]+"."+fnSplit[1][0]+".") #lastname, Jaa.P. - choice.append(fnSplit[0]+" "+fnSplit[1][0]+". "+nameSplit[1])#Jaa P. lastname - choice.append(fnSplit[0][0]+"."+fnSplit[1][0]+". "+nameSplit[1])#J.P. lastname - choice.append(fnSplit[0][0]+fnSplit[1][0]+". "+nameSplit[1])#JP. lastname - else: - choice.append(nameSplit[1]+", "+nameSplit[0][0]+".") - - if len(fnSplit) == 1: #composed first name with space in it - fnSplit = nameSplit[0].split("-") - return choice - - def checkEmail(self,email): - for em in self.infoDic["email"]: - if em == email: - return True,em - return False,0 - - def disambiguation(self): - df = self.getDfFromName(self.infoDic["firstName"]+"+AND+"+self.infoDic["lastName"]) - time.sleep(self.timeBetweenrequest) - personsInfos = self.extractDfInfos(df) - for personInfos in personsInfos: - orcid = personInfos["orcid"] - matchArg = [] - points = 0 - - if "email" in self.infoDic: #check email - end,em = self.checkEmail(personInfos["email"]) - if end: - return [[orcid,["Email "+em],100]] - - works = self.getWorksFromOrcid(orcid) - time.sleep(self.timeBetweenrequest) - worksInfo = self.extractInfoFromWorks(works) - - if "titles" in self.infoDic: #check title - for title in self.infoDic["titles"]: - for tit in [title.lower() for title in worksInfo["title"]]: - ratio = SequenceMatcher(None, title.lower(), tit).ratio() #check similarity between title - if ratio > 0.7: - return [[orcid,["title "+tit],100]] - - if "coAuthors" in self.infoDic: #check coAuthors - authorsPutcode = [] - for putcode in worksInfo["putcode"]: - authorsPutcode += self.getCoAuthorsFromPutcode(putcode,orcid) - time.sleep(self.timeBetweenrequest) - authors = list(set(authorsPutcode)) - for author in self.infoDic["coAuthors"]: - choices = self.splitFirstName(author) - for choice in choices: - if choice in [auth.lower() for auth in authors]: - return [[orcid,["Co-authors "+choice],100]] - - if "affiliations" in self.infoDic: #check affiliations - for affiliation in self.infoDic["affiliations"]: - if affiliation.lower() in [aff.lower() for aff in personInfos["affiliations"]]: - matchArg.append("Affiliation "+affiliation) - points += 10 - - #check first and last name - if self.infoDic["lastName"].lower() == personInfos["lastName"].lower(): - if self.infoDic["firstName"].lower() == personInfos["firstName"].lower(): - points += 10 - matchArg.append("Match First+LastName ") - elif self.infoDic["firstName"].lower()[0] == personInfos["firstName"].lower()[0]: - points += 7 - matchArg.append("Match FirstName First Letter+LastName ") - else: - points += 5 - matchArg.append("Match LastName ") - - personInfos["points"] = points - personInfos["matchArg"] = matchArg - - #check first and last name - finalReturn = [] - for personInfos in personsInfos: - if personInfos["points"] != 0: - finalReturn.append([personInfos["orcid"],personInfos["matchArg"],personInfos["points"]]) - - finalReturn.sort(key=getPoints,reverse=True) - return finalReturn diff --git a/orcid-disambiguation/v1/orcidDisambiguation.ini b/orcid-disambiguation/v1/orcidDisambiguation.ini deleted file mode 100644 index b99bf30..0000000 --- a/orcid-disambiguation/v1/orcidDisambiguation.ini +++ /dev/null @@ -1,51 +0,0 @@ -# OpenAPI Documentation - JSON format (dot notation) -post.responses.default.description = Return all objects with enrich fields -post.responses.default.content.application/json.schema.$ref = #/components/schemas/JSONStream -post.summary = Disambiguation of Orcid author -post.requestBody.required = true -post.requestBody.content.application/json.schema.$ref = #/components/schemas/JSONStream -post.parameters.0.in = query -post.parameters.0.name = path -post.parameters.0.schema.type = string -post.parameters.0.description = The path in each object to enrich with an Python script -post.parameters.1.in = query -post.parameters.1.name = indent -post.parameters.1.schema.type = boolean -post.parameters.1.description = Indent or not the JSON Result - -post.parameters.2.in = query -post.parameters.2.name = nameDepth -post.parameters.2.schema.type = int -post.parameters.2.description = Maximum number of people to check - -post.parameters.3.in = query -post.parameters.3.name = worksDepth -post.parameters.3.schema.type = int -post.parameters.3.description = Maximum number of works we take for a person - -[use] -plugin = @ezs/spawn -plugin = @ezs/basics -plugin = @ezs/storage -plugin = @ezs/analytics - -[JSONParse] -separator = * - -[expand] -path = env('path', 'value') -size = 100 -# in production mode, uncomment the following line -# cache = boost - -[expand/exec] -# command should be executable ! -command = ./v1/orcidDisambiguation.py -args = fix('-p') -args = env('nameDepth',20) -args = fix('-q') -args = env('worksDepth',20) -#command = ./expand.py - -[dump] -indent = env('indent', false) \ No newline at end of file diff --git a/orcid-disambiguation/v1/orcidDisambiguation.py b/orcid-disambiguation/v1/orcidDisambiguation.py deleted file mode 100644 index 62639d6..0000000 --- a/orcid-disambiguation/v1/orcidDisambiguation.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python3 -from disambiguate import disambiguate -import sys -import json -import plac - -@plac.annotations( - nameDepth = ("Maximum number of people to check" ,"option", "p", int ), - worksDepth = ("Maximum number of works we take for a person" ,"option", "q", int ), -) - -def main(nameDepth = 20, worksDepth = 20): - for line in sys.stdin: - data = json.loads(line) - infos = data['value'] - info = infos[0].copy() - db = disambiguate(info,nameDepth=nameDepth, worksDepth=worksDepth ) - result = db.disambiguation() - if len(result)>0: - data['value'] = result[0][0] - else: - data['value'] = "None" - sys.stdout.write(json.dumps(data)) - sys.stdout.write('\n') - -if __name__ == "__main__": - plac.call(main) \ No newline at end of file