Extraction de Données Chiffrées d'une API Chinoise de Qualité de l'Air

Cet article présente une méthode détaillée pour extraire des données chiffrées à partir d'une plateforme de surveillance de la qualité de l'air en ligne. La plateforme, accessible via https://www.aqistudy.cn/html/city_detail.html, affiche des informations essentielles telles que la température, l'humidité, la concentration de PM2.5 et l'indice de qualité de l'air (AQI) sous forme de graphiques interactifs. Le défi principal réside dans le fait que ces données sont chargées dynamiquement et transmises sous forme chiffrée, ce qui complique les méthodes d'extraction classiques basées sur l'interface utilisateur ou des requêtes directes.

Analyse du Trafic Réseau et Identification de l'API

L'observation du comportement du site web révèle que les données sont récupérées suite à une interaction utilisateur spécifique : la sélection d'une ville et d'une période, suivie d'un clic sur le bouton de rehcerche. En utilisant les outils de développement du navigateur (par exemple, l'onglet "Réseau"), on peut intercepter une requête AJAX de type POST. Cette requête est envoyée à une URL d'API et contient un paramètre unique, généralement nommé d, dont la valeur est une chaîne de caractères chiffrée. De manière similaire, la réponse du serveur à cette requête est également un texte chiffré.

La nature chiffrée et dynamique du paramètre d constitue le premier obstacle. Sa valeur change à chaque requête, empêchant une simple réutilisation. Le deuxième obstacle est la réponse chiffrée, qui nécessite un mécanisme de déchiffrement pour être interprétée. Ces observations nous orientent vers une stratégie d'ingénierie inverse du code JavaScript client pour comprendre comment ces opérations de chiffrement et de déchiffrement sont effectuées.

Ingénierie Inverse des Fonctions JavaScript

Le processus commence par l'analyse du code JavaScript exécuté lors de l'interaction utilisateur. En inspcetant le bouton de recherche via les outils de développement, il est possible de localiser le gestionnaire d'événements attaché à son clic.

  1. Le suivi du code JavaScript à partir de cet événement permet d'identifier la fonction principale déclenchée, souvent une fonction de haut niveau comme getData().
  2. En explorant la mise en œuvre de cette fonction, on découvre qu'elle délègue la gestion des requêtes à des fonctions internes. Ces dernières convergent vers une fonction clé, ici nommée getServerData(), qui est responsable de l'initiation de la requête AJAX. Cette fonction reçoit des paramètres tels que la méthode de l'API (par exemple, "GETCITYWEATHER") et un objet contenant les critères de recherche (ville, période).
  3. Un examen plus approfondi de getServerData() révèle qu'elle appelle d'autres fonctions pour préparer le corps de la requête et pour traiter la réponse.

Gestion de l'Obfuscation de Code

Il est courant que le code JavaScript des sites web soit obfusqué pour des raisons de performence ou de sécurité. Si les noms des fonctions comme getServerData et d'autres apparaissent sous une forme difficilement compréhensible (par exemple, a.b.c au lieu de getServerData), il peut être nécessaire d'utiliser un désobfuscateur JavaScript en ligne pour obtenir une version plus lisible du code source.

Découverte des Algorithmes de Chiffrement

Après avoir désobfusqué et analysé le code, deux fonctions essentielles sont identifiées :

  • getParam(api_method_str, query_object) : Cette fonction est invoquée pour générer le paramètre d chiffré. Elle prend en entrée une chaîne représentant la méthode d'API (par exemple, "GETCITYWEATHER") et un objet query_object qui contient les détails de la requête (city, type de données, startTime, endTime). Le code révèle que getParam applique une combinaison des algorithmes Base64 et AES pour chiffrer query_object et retourner la chaîne chiffrée.
  • decodeData(encrypted_response_str) : Cette fonction est le mécanisme de déchiffrement pour les données reçues du serveur. Elle est appelée dans la fonction de rappel de succès de la requête AJAX. decodeData prend la chaîne encrypted_response_str chiffrée et utilise une séquence de déchiffrement combinant Base64, AES et DES pour restituer les données brutes, au format JSON.

La connaissance de ces fonctions et de leurs mécanismes de chiffrement/déchiffrement est cruciale pour automatiser l'extraction des données.

Implémentation Python pour l'Extraction de Données

Pour reproduire le comportement du navigateur et interagir avec l'API, nous utiliserons la bibliothèque Python PyExecJS pour exécuter le code JavaScript trouvé et requests pour envoyer les requêtes HTTP.

Préparation de l'environnement

Assurez-vous que les bibliothèques nécessaires sont installées et qu'un runtime JavaScript (comme Node.js) est disponible pour PyExecJS :

pip install PyExecJS requests
# Si Node.js n'est pas installé, PyExecJS peut utiliser d'autres runtimes ou le demander.

Fichier JavaScript des Fonctions API

Créez un fichier nommé api_functions.js. Copiez-y le code JavaScript désobfusqué contenant les implémentations de getParam et decodeData, ainsi que toutes leurs dépendances. Ajoutez-y également une fonction d'aide pour simplifier la génération du payload chiffré depuis Python :

// api_functions.js
// ... Début du code désobfusqué (getParam, decodeData, et leurs fonctions auxiliaires) ...

function generateEncryptedPayload(apiMethod, targetCity, dataType, startDate, endDate) {
    var requestParams = {};
    requestParams.city = targetCity;
    requestParams.type = dataType;
    requestParams.startTime = startDate;
    requestParams.endTime = endDate;
    return getParam(apiMethod, requestParams);
}

// ... Fin du code JavaScript ...

Script Python d'Extraction

Voici un exemple de script Python qui utilise les fonctions JavaScript définies pour chiffrer la requête, envoyer une demande à l'API, puis déchiffrer la réponse :

import execjs
import requests
import json

def recuperer_donnees_aqi_chiffrees(nom_ville, date_debut_str, date_fin_str):
    """
    Récupère et déchiffre les données de qualité de l'air pour une ville et une période spécifiées.
    """
    js_runtime_env = execjs.get() # Obtient l'environnement d'exécution JavaScript

    # Paramètres de l'API
    methode_api = 'GETCITYWEATHER'
    type_donnees = 'HOUR' # Peut être 'DAY' ou 'MONTH' selon les besoins

    # Charge et compile le code JavaScript
    fichier_js = 'api_functions.js'
    try:
        with open(fichier_js, 'r', encoding='utf-8') as f:
            contexte_js = js_runtime_env.compile(f.read())
    except FileNotFoundError:
        print(f"Erreur: Le fichier '{fichier_js}' n'a pas été trouvé.")
        return None
    except Exception as e:
        print(f"Erreur lors de la compilation JavaScript: {e}")
        return None

    # Appelle la fonction JS pour générer le payload chiffré
    commande_js_chiffrer = (
        f'generateEncryptedPayload("{methode_api}", "{nom_ville}", "{type_donnees}", '
        f'"{date_debut_str}", "{date_fin_str}")'
    )
    payload_chiffre = contexte_js.eval(commande_js_chiffrer)

    # URL de l'API cible
    url_api = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
    
    # Envoie la requête POST à l'API
    try:
        reponse_http = requests.post(url_api, data={'d': payload_chiffre})
        reponse_http.raise_for_status() # Lève une exception pour les codes d'état HTTP d'erreur
        texte_reponse_chiffree = reponse_http.text
    except requests.exceptions.RequestException as e:
        print(f"Erreur réseau ou HTTP lors de la requête: {e}")
        return None

    # Appelle la fonction JS pour déchiffrer la réponse de l'API
    commande_js_dechiffrer = f'decodeData("{texte_reponse_chiffree}")'
    donnees_dechiffrees_str = contexte_js.eval(commande_js_dechiffrer)
    
    # Tente de parser la chaîne JSON déchiffrée
    try:
        donnees_finales = json.loads(donnees_dechiffrees_str)
        return donnees_finales
    except json.JSONDecodeError as e:
        print(f"Erreur lors du parsing JSON des données déchiffrées: {e}")
        return None

# Exemple d'utilisation du script
if __name__ == '__main__':
    ville_recherche = '北京' # Pékin
    heure_debut = '2018-01-25 00:00:00'
    heure_fin = '2018-01-25 23:00:00'
    
    resultats_aqi = recuperer_donnees_aqi_chiffrees(ville_recherche, heure_debut, heure_fin)
    
    if resultats_aqi:
        # Affichage des données déchiffrées, formaté pour la lisibilité
        print(json.dumps(resultats_aqi, indent=2, ensure_ascii=False))
        # Vous pouvez ensuite parcourir et traiter les 'rows' dans 'resultats_aqi['result']['data']['rows']'

L'exécution de ce script fournira un objet JSON contenant les données de qualité de l'air déchiffrées, prêtes à être analysées ou stockées.

Étiquettes: Python web scraping JavaScript PyExecJS requests

Publié le 24 juin à 01h52