Les API sont partout de nos jours. Imaginez : vous souhaitez recueillir des informations sur vos prospects à partir de leurs emails. Hé bien il y a une API pour ceci. Vous avez besoin de géocoder une adresse postale mal formatée ? Il existe aussi une API pour cela. Enfin, vous cherchez à effectuer un paiement de façon automatisée ? De nombreuses API font le job bien entendu. En tant que développeur, je suis régulièrement amené à intégrer des API externes à mon système, en Python ou en Go. Les deux méthodes sont assez différentes. Comparons-les sur un cas un peu “borderline” : l’envoi de données JSON encodées dans le body d’une requête POST.
Un exemple de la vie réelle
Récemment, j’ai utilisé l’API NameAPI.org dans le but de découper un nom entier en prénom et nom de famille, et déterminer le genre de la personne.
Leur API attend que vous lui envoyiez les données en JSON encodées dans le body d’une requête POST. De plus, le Content-Type
de la requête doit être application/json
et non pas multipart/form-data
. Il s’agit d’un cas un peu piégeux puisqu’en général les données POST sont envoyées à travers le header de la requête, et si l’on décide de les envoyer dans le body de la requête (dans le cas de données JSON complexes par exemple) le Content-Type
standard est multipart/form-data
.
Voici le JSON que l’on veut envoyer :
{
"inputPerson" : {
"type" : "NaturalInputPerson",
"personName" : {
"nameFields" : [ {
"string" : "Petra",
"fieldType" : "GIVENNAME"
}, {
"string" : "Meyer",
"fieldType" : "SURNAME"
} ]
},
"gender" : "UNKNOWN"
}
}
On peut le faire facilement via cURL :
curl -H "Content-Type: application/json" \
-X POST \
-d '{"inputPerson":{"type":"NaturalInputPerson","personName":{"nameFields":[{"string":"Petra Meyer","fieldType":"FULLNAME"}]}}}' \
http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?apiKey=<API-KEY>
Et voici la réponse JSON de NameAPI.org :
{
"matches" : [ {
"parsedPerson" : {
"personType" : "NATURAL",
"personRole" : "PRIMARY",
"mailingPersonRoles" : [ "ADDRESSEE" ],
"gender" : {
"gender" : "MALE",
"confidence" : 0.9111111111111111
},
"addressingGivenName" : "Petra",
"addressingSurname" : "Meyer",
"outputPersonName" : {
"terms" : [ {
"string" : "Petra",
"termType" : "GIVENNAME"
},{
"string" : "Meyer",
"termType" : "SURNAME"
} ]
}
},
"parserDisputes" : [ ],
"likeliness" : 0.926699401733102,
"confidence" : 0.7536487758945387
}
Maintenant voyons comment faire cela en Go et en Python !
Réalisation en Go
Code
/*
Fetch the NameAPI.org REST API and turn JSON response into a Go struct.
Sent data have to be JSON data encoded into request body.
Send request headers must be set to 'application/json'.
*/
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strings"
)
// url of the NameAPI.org endpoint:
const (
url = "http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?" +
"apiKey=<API-KEY>"
)
func main() {
// JSON string to be sent to NameAPI.org:
jsonString := `{
"inputPerson": {
"type": "NaturalInputPerson",
"personName": {
"nameFields": [
{
"string": "Petra",
"fieldType": "GIVENNAME"
}, {
"string": "Meyer",
"fieldType": "SURNAME"
}
]
},
"gender": "UNKNOWN"
}
}`
// Convert JSON string to NewReader (expected by NewRequest)
jsonBody := strings.NewReader(jsonString)
// Need to create a client in order to modify headers
// and set content-type to 'application/json':
client := &http.Client{}
req, err := http.NewRequest("POST", url, jsonBody)
if err != nil {
log.Println(err)
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
// Proceed only if no error:
switch {
default:
// Create a struct dedicated to receiving the fetched
// JSON content:
type Level5 struct {
String string `json:"string"`
TermType string `json:"termType"`
}
type Level41 struct {
Gender string `json:"gender"`
Confidence float64 `json:"confidence"`
}
type Level42 struct {
Terms []Level5 `json:"terms"`
}
type Level3 struct {
Gender Level41 `json:"gender"`
OutputPersonName Level42 `json:"outputPersonName"`
}
type Level2 struct {
ParsedPerson Level3 `json:"parsedPerson"`
}
type RespContent struct {
Matches []Level2 `json:"matches"`
}
// Decode fetched JSON and put it into respContent:
respContentBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
}
var respContent RespContent
err = json.Unmarshal(respContentBytes, &respContent)
if err != nil {
log.Println(err)
}
log.Println(respContent)
case err != nil:
log.Println("Network error:", err)
case resp.StatusCode != 200:
log.Println("Bad HTTP status code:", err)
}
}
Explications
Comme vous pouvez le voir, nous faisons face à 2 problèmes désagréables :
- la bibliothèque
http
n’est pas si simple d’utilisation lorsqu’il s’agit d’encoder des données JSON au sein du body et de changer le headerContent-Type
. La doc de Go n’est pas très claire à ce sujet. Ainsi nous ne pouvons pas invoquerhttp.Post
qui est plus simple d’utilisation et nous sommes contraints de créer unhttp.Client
puis par la suite d’utiliser la fonctionNewRequest()
que l’on déclenche avecclient.Do(req)
. C’est l’unique façon de créer unContent-Type
personnalisé dans notre cas :req.Header.Add("Content-Type", "application/json")
- décoder le JSON reçu de NameAPI en Go est assez long et fastidieux (procédé appelé Unmarshalling en Go). Cela vient du fait que, Go étant un langage statique, nous avons besoin de savoir à l’avance à quoi ressemblera le JSON final. Par conséquent nous avons besoin de créer une
struct
dédiée dont la structure sera la même que celle du JSON et qui recevra les données. En cas de JSON imbriqué tel que celui retourné par NameAPI.org, qui mélange desarray
et desmaps
, cela devient très délicat. Heureusement, notrestruct
n’a pas besoin de mapper tout le JSON mais seulement les champs qui nous intéressent. Une autre approche, si nous n’avons aucune idée de la structure de notre JSON, serait de deviner les types de données. Voici un bon article sur le sujet.
L’input jsonString
est déjà une string
ici. Mais pour une comparaison encore plus rigoureuse avec Python, cela aurait dû être une struct
que l’on aurait par la suite convertie en string
. Toutefois cela aurait rallongé l’article inutilement.
Réalisation en Python
Code
"""
Fetch the NameAPI.org REST API and turn JSON response into Python dict.
Sent data have to be JSON data encoded into request body.
Send request headers must be set to 'application/json'.
"""
import requests
# url of the NameAPI.org endpoint:
url = (
"http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?"
"apiKey=<API-KEY>"
)
# Dict of data to be sent to NameAPI.org:
payload = {
"inputPerson": {
"type": "NaturalInputPerson",
"personName": {
"nameFields": [
{
"string": "Petra",
"fieldType": "GIVENNAME"
}, {
"string": "Meyer",
"fieldType": "SURNAME"
}
]
},
"gender": "UNKNOWN"
}
}
# Proceed, only if no error:
try:
# Send request to NameAPI.org by doing the following:
# - make a POST HTTP request
# - encode the Python payload dict to JSON
# - pass the JSON to request body
# - set header's 'Content-Type' to 'application/json' instead of
# default 'multipart/form-data'
resp = requests.post(url, json=payload)
resp.raise_for_status()
# Decode JSON response into a Python dict:
resp_dict = resp.json()
print(resp_dict)
except requests.exceptions.HTTPError as e:
print("Bad HTTP status code:", e)
except requests.exceptions.RequestException as e:
print("Network error:", e)
Explications
La bibliothèque Request est une bibliothèque incroyablement pratique qui nous épargne beaucoup d’efforts comparé à Go ! En une ligne, resp = requests.post(url, json=payload)
, presque tout est fait :
- construire une requête POST HTTP
- convertir le dictionnaire Python en JSON
- passer le JSON au body de la requête
- passer le
'Content-Type'
du header de'multipart/form-data'
à'application/json'
grâce au kwargjson
- envoyer la requête
Décoder le JSON retourné par NameAPI se fait aussi en une ligne : resp_dict = resp.json()
. Nul besoin de créer une structure de données compliquée à l’avance ici !
Conclusion
Python est clairement le gagnant. La simplicité de Python combinée à sa quantité de bibliothèques disponibles nous épargne pas mal de temps de développement !
Nous n’abordons pas la question de la performance ici. Si vous recherchez une intégration d’API hautement performante utilisant la concurrence, Go pourrait être un excellent choix. Mais simplicité et performance vont rarement de paire…
N’hésitez pas à commenter, je serais heureux d’avoir votre avis sur ce comparatif.